|
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