|
5 | 5 | using System.Buffers.Binary; |
6 | 6 | using System.Diagnostics; |
7 | 7 | using System.IO.Pipelines; |
| 8 | +using System.Net.Http; |
8 | 9 | using System.Net.Http.HPack; |
9 | 10 | using System.Threading.Channels; |
10 | 11 | using Microsoft.AspNetCore.Connections; |
@@ -73,6 +74,8 @@ internal sealed class Http2FrameWriter |
73 | 74 |
|
74 | 75 | private int _maxFrameSize = Http2PeerSettings.MinAllowedMaxFrameSize; |
75 | 76 | private readonly ArrayBufferWriter<byte> _headerEncodingBuffer; |
| 77 | + private readonly int? _maxResponseHeadersTotalSize; |
| 78 | + private int _currentResponseHeadersTotalSize; |
76 | 79 | private long _unflushedBytes; |
77 | 80 |
|
78 | 81 | private bool _completed; |
@@ -110,7 +113,7 @@ public Http2FrameWriter( |
110 | 113 | _headerEncodingBuffer = new ArrayBufferWriter<byte>(_maxFrameSize); |
111 | 114 |
|
112 | 115 | _scheduleInline = serviceContext.Scheduler == PipeScheduler.Inline; |
113 | | - |
| 116 | + _maxResponseHeadersTotalSize = _http2Connection.Limits.MaxResponseHeadersTotalSize; |
114 | 117 | _hpackEncoder = new DynamicHPackEncoder(serviceContext.ServerOptions.AllowResponseHeaderCompression); |
115 | 118 |
|
116 | 119 | _maximumFlowControlQueueSize = AppContextMaximumFlowControlQueueSize is null |
@@ -484,25 +487,32 @@ private void WriteResponseHeadersUnsynchronized(int streamId, int statusCode, Ht |
484 | 487 | { |
485 | 488 | try |
486 | 489 | { |
| 490 | + // In the case of the headers, there is always a status header to be returned, so BeginEncodeHeaders will not return BufferTooSmall. |
487 | 491 | _headersEnumerator.Initialize(headers); |
488 | 492 | _outgoingFrame.PrepareHeaders(headerFrameFlags, streamId); |
489 | 493 | _headerEncodingBuffer.ResetWrittenCount(); |
490 | 494 | var buffer = _headerEncodingBuffer.GetSpan(_maxFrameSize)[0.._maxFrameSize]; // GetSpan might return more data that can result in a less deterministic behavior on the way headers are split into frames. |
491 | 495 | var done = HPackHeaderWriter.BeginEncodeHeaders(statusCode, _hpackEncoder, _headersEnumerator, buffer, out var payloadLength); |
492 | 496 | Debug.Assert(done != HPackHeaderWriter.HeaderWriteResult.BufferTooSmall, "Oversized frames should not be returned, beucase this always writes the status."); |
| 497 | + if (_maxResponseHeadersTotalSize.HasValue && payloadLength > _maxResponseHeadersTotalSize.Value) |
| 498 | + { |
| 499 | + ThrowResponseHeadersLimitException(); |
| 500 | + } |
| 501 | + _currentResponseHeadersTotalSize = payloadLength; |
493 | 502 | if (done == HPackHeaderWriter.HeaderWriteResult.Done) |
494 | 503 | { |
495 | | - // Fast path |
| 504 | + // Fast path, only a single HEADER frame. |
496 | 505 | _outgoingFrame.PayloadLength = payloadLength; |
497 | 506 | _outgoingFrame.HeadersFlags |= Http2HeadersFrameFlags.END_HEADERS; |
498 | 507 | WriteHeaderUnsynchronized(); |
499 | 508 | _outputWriter.Write(buffer[0..payloadLength]); |
500 | 509 | } |
501 | 510 | else |
502 | 511 | { |
503 | | - // Slow path |
| 512 | + // More headers sent in CONTINUATION frames. |
504 | 513 | _headerEncodingBuffer.Advance(payloadLength); |
505 | | - FinishWritingHeaders(streamId, payloadLength, done); |
| 514 | + SplitHeaderFramesToOutput(streamId, done, isFramePrepared: true); |
| 515 | + FinishWritingHeaders(streamId); |
506 | 516 | } |
507 | 517 | } |
508 | 518 | // Any exception from the HPack encoder can leave the dynamic table in a corrupt state. |
@@ -540,19 +550,46 @@ private ValueTask<FlushResult> WriteDataAndTrailersAsync(Http2Stream stream, in |
540 | 550 |
|
541 | 551 | try |
542 | 552 | { |
| 553 | + // In the case of the trailers, there is no status header to be written, so even the first call to BeginEncodeHeaders can return BufferTooSmall. |
543 | 554 | _outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.END_STREAM, streamId); |
544 | | - var done = HPackHeaderWriter.HeaderWriteResult.MoreHeaders; |
545 | | - int payloadLength; |
| 555 | + var bufferSize = _headerEncodingBuffer.Capacity; |
| 556 | + HPackHeaderWriter.HeaderWriteResult done; |
546 | 557 | do |
547 | 558 | { |
548 | 559 | _headersEnumerator.Initialize(headers); |
549 | 560 | _headerEncodingBuffer.ResetWrittenCount(); |
550 | | - var bufferSize = done == HPackHeaderWriter.HeaderWriteResult.BufferTooSmall ? _headerEncodingBuffer.Capacity * 2 : _headerEncodingBuffer.Capacity; |
551 | 561 | var buffer = _headerEncodingBuffer.GetSpan(bufferSize)[0..bufferSize]; // GetSpan might return more data that can result in a less deterministic behavior on the way headers are split into frames. |
552 | | - done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out payloadLength); |
| 562 | + done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out var payloadLength); |
| 563 | + if (done == HPackHeaderWriter.HeaderWriteResult.Done) |
| 564 | + { |
| 565 | + if (_maxResponseHeadersTotalSize.HasValue && _currentResponseHeadersTotalSize + payloadLength > _maxResponseHeadersTotalSize.Value) |
| 566 | + { |
| 567 | + ThrowResponseHeadersLimitException(); |
| 568 | + } |
| 569 | + _headerEncodingBuffer.Advance(payloadLength); |
| 570 | + SplitHeaderFramesToOutput(streamId, done, isFramePrepared: true); |
| 571 | + } |
| 572 | + else if (done == HPackHeaderWriter.HeaderWriteResult.MoreHeaders) |
| 573 | + { |
| 574 | + // More headers sent in CONTINUATION frames. |
| 575 | + _currentResponseHeadersTotalSize += payloadLength; |
| 576 | + if (_maxResponseHeadersTotalSize.HasValue && _currentResponseHeadersTotalSize > _maxResponseHeadersTotalSize.Value) |
| 577 | + { |
| 578 | + ThrowResponseHeadersLimitException(); |
| 579 | + } |
| 580 | + _headerEncodingBuffer.Advance(payloadLength); |
| 581 | + SplitHeaderFramesToOutput(streamId, done, isFramePrepared: true); |
| 582 | + FinishWritingHeaders(streamId); |
| 583 | + } |
| 584 | + else |
| 585 | + { |
| 586 | + if (_maxResponseHeadersTotalSize.HasValue && _currentResponseHeadersTotalSize + bufferSize > _maxResponseHeadersTotalSize.Value) |
| 587 | + { |
| 588 | + ThrowResponseHeadersLimitException(); |
| 589 | + } |
| 590 | + bufferSize *= 2; |
| 591 | + } |
553 | 592 | } while (done == HPackHeaderWriter.HeaderWriteResult.BufferTooSmall); |
554 | | - _headerEncodingBuffer.Advance(payloadLength); |
555 | | - FinishWritingHeaders(streamId, payloadLength, done); |
556 | 593 | } |
557 | 594 | // Any exception from the HPack encoder can leave the dynamic table in a corrupt state. |
558 | 595 | // Since we allow custom header encoders we don't know what type of exceptions to expect. |
@@ -594,18 +631,36 @@ private void SplitHeaderFramesToOutput(int streamId, HPackHeaderWriter.HeaderWri |
594 | 631 | } |
595 | 632 | } |
596 | 633 |
|
597 | | - private void FinishWritingHeaders(int streamId, int payloadLength, HPackHeaderWriter.HeaderWriteResult done) |
| 634 | + private void FinishWritingHeaders(int streamId) |
598 | 635 | { |
599 | | - SplitHeaderFramesToOutput(streamId, done, isFramePrepared: true); |
600 | | - while (done != HPackHeaderWriter.HeaderWriteResult.Done) |
| 636 | + HPackHeaderWriter.HeaderWriteResult done; |
| 637 | + var bufferSize = _headerEncodingBuffer.Capacity; |
| 638 | + do |
601 | 639 | { |
602 | 640 | _headerEncodingBuffer.ResetWrittenCount(); |
603 | | - var bufferSize = done == HPackHeaderWriter.HeaderWriteResult.BufferTooSmall ? _headerEncodingBuffer.Capacity * 2 : _headerEncodingBuffer.Capacity; |
604 | 641 | var buffer = _headerEncodingBuffer.GetSpan(bufferSize)[0..bufferSize]; |
605 | | - done = HPackHeaderWriter.ContinueEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out payloadLength); |
606 | | - _headerEncodingBuffer.Advance(payloadLength); |
607 | | - SplitHeaderFramesToOutput(streamId, done, isFramePrepared: false); |
608 | | - } |
| 642 | + done = HPackHeaderWriter.ContinueEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out var payloadLength); |
| 643 | + |
| 644 | + if (done == HPackHeaderWriter.HeaderWriteResult.BufferTooSmall) |
| 645 | + { |
| 646 | + if (_maxResponseHeadersTotalSize.HasValue && _currentResponseHeadersTotalSize + bufferSize > _maxResponseHeadersTotalSize.Value) |
| 647 | + { |
| 648 | + ThrowResponseHeadersLimitException(); |
| 649 | + } |
| 650 | + bufferSize *= 2; |
| 651 | + } |
| 652 | + else |
| 653 | + { |
| 654 | + // In case of Done or MoreHeaders: write to output. |
| 655 | + _currentResponseHeadersTotalSize += payloadLength; |
| 656 | + if (_maxResponseHeadersTotalSize.HasValue && _currentResponseHeadersTotalSize > _maxResponseHeadersTotalSize.Value) |
| 657 | + { |
| 658 | + ThrowResponseHeadersLimitException(); |
| 659 | + } |
| 660 | + _headerEncodingBuffer.Advance(payloadLength); |
| 661 | + SplitHeaderFramesToOutput(streamId, done, isFramePrepared: false); |
| 662 | + } |
| 663 | + } while (done != HPackHeaderWriter.HeaderWriteResult.Done); |
609 | 664 | } |
610 | 665 |
|
611 | 666 | /* Padding is not implemented |
@@ -1031,4 +1086,6 @@ private void EnqueueWaitingForMoreConnectionWindow(Http2OutputProducer producer) |
1031 | 1086 | _http2Connection.Abort(new ConnectionAbortedException("HTTP/2 connection exceeded the outgoing flow control maximum queue size.")); |
1032 | 1087 | } |
1033 | 1088 | } |
| 1089 | + |
| 1090 | + private void ThrowResponseHeadersLimitException() => throw new HPackEncodingException(SR.Format(SR.net_http_headers_exceeded_length, _maxResponseHeadersTotalSize!)); |
1034 | 1091 | } |
0 commit comments