From 64127e6c766b221cf147383c16079d3b7aad2ded Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 20 Aug 2018 21:13:37 -0700 Subject: [PATCH] Implement MaxFrameSize and HeaderTableSize for HTTP/2 --- src/Kestrel.Core/CoreStrings.resx | 3 + src/Kestrel.Core/Http2Limits.cs | 48 ++++++++ .../Internal/Http2/Http2Connection.cs | 22 ++-- .../Internal/Http2/Http2Frame.Continuation.cs | 2 +- .../Internal/Http2/Http2Frame.Data.cs | 2 +- .../Internal/Http2/Http2Frame.Headers.cs | 2 +- src/Kestrel.Core/Internal/Http2/Http2Frame.cs | 12 +- .../Internal/Http2/Http2FrameWriter.cs | 14 ++- .../Internal/Http2/Http2PeerSettings.cs | 14 ++- .../Properties/CoreStrings.Designer.cs | 14 +++ .../KestrelServerLimitsTests.cs | 31 ++++++ .../Http2/Http2ConnectionTests.cs | 104 ++++++++++++++++-- .../Http2/Http2TestBase.cs | 82 +++++++------- .../Http2/TlsTests.cs | 2 +- 14 files changed, 280 insertions(+), 72 deletions(-) diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx index 6d72c97ca4..c40ba8d917 100644 --- a/src/Kestrel.Core/CoreStrings.resx +++ b/src/Kestrel.Core/CoreStrings.resx @@ -575,4 +575,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l The frame is too short to contain the fields indicated by the given flags. + + A value between {min} and {max} is required. + \ No newline at end of file diff --git a/src/Kestrel.Core/Http2Limits.cs b/src/Kestrel.Core/Http2Limits.cs index 34a4ef36f1..f4b05c4abb 100644 --- a/src/Kestrel.Core/Http2Limits.cs +++ b/src/Kestrel.Core/Http2Limits.cs @@ -11,6 +11,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core public class Http2Limits { private int _maxStreamsPerConnection = 100; + private int _headerTableSize = MaxAllowedHeaderTableSize; + private int _maxFrameSize = MinAllowedMaxFrameSize; + + // These are limits defined by the RFC https://tools.ietf.org/html/rfc7540#section-4.2 + public const int MaxAllowedHeaderTableSize = 4096; + public const int MinAllowedMaxFrameSize = 16 * 1024; + public const int MaxAllowedMaxFrameSize = 16 * 1024 * 1024 - 1; /// /// Limits the number of concurrent request streams per HTTP/2 connection. Excess streams will be refused. @@ -27,8 +34,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core { throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanZeroRequired); } + _maxStreamsPerConnection = value; } } + + /// + /// Limits the size of the header compression table, in octets, the HPACK decoder on the server can use. + /// + /// Defaults to 4096 + /// + /// + public int HeaderTableSize + { + get => _headerTableSize; + set + { + if (value <= 0 || value > MaxAllowedHeaderTableSize) + { + throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.FormatArgumentOutOfRange(0, MaxAllowedHeaderTableSize)); + } + + _headerTableSize = value; + } + } + + /// + /// Indicates the size of the largest frame payload that is allowed to be received, in octets. The size must be between 2^14 and 2^24-1. + /// + /// Defaults to 2^14 (16,384) + /// + /// + public int MaxFrameSize + { + get => _maxFrameSize; + set + { + if (value < MinAllowedMaxFrameSize || value > MaxAllowedMaxFrameSize) + { + throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.FormatArgumentOutOfRange(MinAllowedMaxFrameSize, MaxAllowedMaxFrameSize)); + } + + _maxFrameSize = value; + } + } } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs index 3bd7aa7c29..c33674d645 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs @@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private readonly Http2PeerSettings _serverSettings = new Http2PeerSettings(); private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); - private readonly Http2Frame _incomingFrame = new Http2Frame(); + private readonly Http2Frame _incomingFrame; private Http2Stream _currentHeadersStream; private RequestHeaderParsingState _requestHeaderParsingState; @@ -87,8 +87,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { _context = context; _frameWriter = new Http2FrameWriter(context.Transport.Output, context.ConnectionContext, _outputFlowControl, this, context.ConnectionId, context.ServiceContext.Log); - _hpackDecoder = new HPackDecoder((int)_serverSettings.HeaderTableSize); _serverSettings.MaxConcurrentStreams = (uint)context.ServiceContext.ServerOptions.Limits.Http2.MaxStreamsPerConnection; + _serverSettings.MaxFrameSize = (uint)context.ServiceContext.ServerOptions.Limits.Http2.MaxFrameSize; + _serverSettings.HeaderTableSize = (uint)context.ServiceContext.ServerOptions.Limits.Http2.HeaderTableSize; + _hpackDecoder = new HPackDecoder((int)_serverSettings.HeaderTableSize); + _incomingFrame = new Http2Frame(_serverSettings.MaxFrameSize); } public string ConnectionId => _context.ConnectionId; @@ -601,7 +604,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return Task.CompletedTask; } - private Task ProcessSettingsFrameAsync() + private async Task ProcessSettingsFrameAsync() { if (_currentHeadersStream != null) { @@ -620,7 +623,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorSettingsAckLengthNotZero, Http2ErrorCode.FRAME_SIZE_ERROR); } - return Task.CompletedTask; + return; } if (_incomingFrame.PayloadLength % 6 != 0) @@ -632,10 +635,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { // int.MaxValue is the largest allowed windows size. var previousInitialWindowSize = (int)_clientSettings.InitialWindowSize; + var previousMaxFrameSize = _clientSettings.MaxFrameSize; _clientSettings.Update(_incomingFrame.GetSettings()); - var ackTask = _frameWriter.WriteSettingsAckAsync(); // Ack before we update the windows, they could send data immediately. + // Ack before we update the windows, they could send data immediately. + await _frameWriter.WriteSettingsAckAsync(); + + if (_clientSettings.MaxFrameSize != previousMaxFrameSize) + { + _frameWriter.UpdateMaxFrameSize(_clientSettings.MaxFrameSize); + } // This difference can be negative. var windowSizeDifference = (int)_clientSettings.InitialWindowSize - previousInitialWindowSize; @@ -653,8 +663,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } } - - return ackTask; } catch (Http2SettingsParameterOutOfRangeException ex) { diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Continuation.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Continuation.cs index 26f3afe92b..56712f5289 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Continuation.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Continuation.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public void PrepareContinuation(Http2ContinuationFrameFlags flags, int streamId) { - PayloadLength = MinAllowedMaxFrameSize - HeaderLength; + PayloadLength = (int)_maxFrameSize; Type = Http2FrameType.CONTINUATION; ContinuationFlags = flags; StreamId = streamId; diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Data.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Data.cs index b2df5c4bfd..3bc53971e5 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Data.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Data.cs @@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { var padded = padLength != null; - PayloadLength = MinAllowedMaxFrameSize; + PayloadLength = (int)_maxFrameSize; Type = Http2FrameType.DATA; DataFlags = padded ? Http2DataFrameFlags.PADDED : Http2DataFrameFlags.NONE; StreamId = streamId; diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Headers.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Headers.cs index e89a2e1675..3028e27200 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Headers.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Headers.cs @@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public void PrepareHeaders(Http2HeadersFrameFlags flags, int streamId) { - PayloadLength = MinAllowedMaxFrameSize - HeaderLength; + PayloadLength = (int)_maxFrameSize; Type = Http2FrameType.HEADERS; HeadersFlags = flags; StreamId = streamId; diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.cs index f5f447ae7f..f9d0723bdc 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.cs @@ -18,8 +18,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 */ public partial class Http2Frame { - public const int MinAllowedMaxFrameSize = 16 * 1024; - public const int MaxAllowedMaxFrameSize = 16 * 1024 * 1024 - 1; public const int HeaderLength = 9; private const int LengthOffset = 0; @@ -28,7 +26,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private const int StreamIdOffset = 5; private const int PayloadOffset = 9; - private readonly byte[] _data = new byte[HeaderLength + MinAllowedMaxFrameSize]; + private uint _maxFrameSize; + + private readonly byte[] _data; + + public Http2Frame(uint maxFrameSize) + { + _maxFrameSize = maxFrameSize; + _data = new byte[HeaderLength + _maxFrameSize]; + } public Span Raw => new Span(_data, 0, HeaderLength + PayloadLength); diff --git a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs index 1d101d1591..35def3fc16 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { @@ -22,7 +21,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 // Literal Header Field without Indexing - Indexed Name (Index 8 - :status) private static readonly byte[] _continueBytes = new byte[] { 0x08, 0x03, (byte)'1', (byte)'0', (byte)'0' }; - private readonly Http2Frame _outgoingFrame = new Http2Frame(); + private uint _maxFrameSize = Http2Limits.MinAllowedMaxFrameSize; + private Http2Frame _outgoingFrame; private readonly object _writeLock = new object(); private readonly HPackEncoder _hpackEncoder = new HPackEncoder(); private readonly PipeWriter _outputWriter; @@ -49,6 +49,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _connectionId = connectionId; _log = log; _flusher = new StreamSafePipeFlusher(_outputWriter, timeoutControl); + _outgoingFrame = new Http2Frame(_maxFrameSize); + } + + public void UpdateMaxFrameSize(uint maxFrameSize) + { + lock (_writeLock) + { + _maxFrameSize = maxFrameSize; + _outgoingFrame = new Http2Frame(maxFrameSize); + } } public void Complete() diff --git a/src/Kestrel.Core/Internal/Http2/Http2PeerSettings.cs b/src/Kestrel.Core/Internal/Http2/Http2PeerSettings.cs index eeb13bb808..e821daac13 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2PeerSettings.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2PeerSettings.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public const bool DefaultEnablePush = true; public const uint DefaultMaxConcurrentStreams = uint.MaxValue; public const uint DefaultInitialWindowSize = 65535; - public const uint DefaultMaxFrameSize = 16384; + public const uint DefaultMaxFrameSize = Http2Limits.MinAllowedMaxFrameSize; public const uint DefaultMaxHeaderListSize = uint.MaxValue; public const uint MaxWindowSize = int.MaxValue; @@ -38,6 +38,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 switch (setting.Parameter) { case Http2SettingsParameter.SETTINGS_HEADER_TABLE_SIZE: + if (value > Http2Limits.MaxAllowedHeaderTableSize) + { + throw new Http2SettingsParameterOutOfRangeException(Http2SettingsParameter.SETTINGS_HEADER_TABLE_SIZE, + lowerBound: 0, + upperBound: Http2Limits.MaxAllowedHeaderTableSize); + } HeaderTableSize = value; break; case Http2SettingsParameter.SETTINGS_ENABLE_PUSH: @@ -64,11 +70,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 InitialWindowSize = value; break; case Http2SettingsParameter.SETTINGS_MAX_FRAME_SIZE: - if (value < Http2Frame.MinAllowedMaxFrameSize || value > Http2Frame.MaxAllowedMaxFrameSize) + if (value < Http2Limits.MinAllowedMaxFrameSize || value > Http2Limits.MaxAllowedMaxFrameSize) { throw new Http2SettingsParameterOutOfRangeException(Http2SettingsParameter.SETTINGS_MAX_FRAME_SIZE, - lowerBound: Http2Frame.MinAllowedMaxFrameSize, - upperBound: Http2Frame.MaxAllowedMaxFrameSize); + lowerBound: Http2Limits.MinAllowedMaxFrameSize, + upperBound: Http2Limits.MaxAllowedMaxFrameSize); } MaxFrameSize = value; diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs index 77e6f5c678..06945a5160 100644 --- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -2142,6 +2142,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatHttp2FrameMissingFields() => GetString("Http2FrameMissingFields"); + /// + /// A value between {min} and {max} is required. + /// + internal static string ArgumentOutOfRange + { + get => GetString("ArgumentOutOfRange"); + } + + /// + /// A value between {min} and {max} is required. + /// + internal static string FormatArgumentOutOfRange(object min, object max) + => string.Format(CultureInfo.CurrentCulture, GetString("ArgumentOutOfRange", "min", "max"), min, max); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/test/Kestrel.Core.Tests/KestrelServerLimitsTests.cs b/test/Kestrel.Core.Tests/KestrelServerLimitsTests.cs index d71642f9dd..cf46717f85 100644 --- a/test/Kestrel.Core.Tests/KestrelServerLimitsTests.cs +++ b/test/Kestrel.Core.Tests/KestrelServerLimitsTests.cs @@ -308,6 +308,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(TimeSpan.FromSeconds(5), new KestrelServerLimits().MinResponseDataRate.GracePeriod); } + [Fact] + public void Http2MaxFrameSizeDefault() + { + Assert.Equal(1 << 14, new KestrelServerLimits().Http2.MaxFrameSize); + } + + [Theory] + [InlineData(1 << 14 - 1)] + [InlineData(1 << 24)] + [InlineData(-1)] + public void Http2MaxFrameSizeInvalid(int value) + { + var ex = Assert.Throws(() => new KestrelServerLimits().Http2.MaxFrameSize = value); + Assert.Contains("A value between", ex.Message); + } + + [Fact] + public void Http2HeaderTableSizeDefault() + { + Assert.Equal(4096, new KestrelServerLimits().Http2.HeaderTableSize); + } + + [Theory] + [InlineData(4097)] + [InlineData(-1)] + public void Http2HeaderTableSizeInvalid(int value) + { + var ex = Assert.Throws(() => new KestrelServerLimits().Http2.MaxFrameSize = value); + Assert.Contains("A value between", ex.Message); + } + public static TheoryData TimeoutValidData => new TheoryData { TimeSpan.FromTicks(1), diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index c4b6c3ab44..f876a1f01b 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -77,7 +77,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests private static readonly byte[] _worldBytes = Encoding.ASCII.GetBytes("world"); private static readonly byte[] _helloWorldBytes = Encoding.ASCII.GetBytes("hello, world"); private static readonly byte[] _noData = new byte[0]; - private static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', Http2Frame.MinAllowedMaxFrameSize)); + private static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', Http2Limits.MinAllowedMaxFrameSize)); [Fact] public async Task Frame_Received_OverMaxSize_FrameError() @@ -85,20 +85,47 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await InitializeConnectionAsync(_echoApplication); await StartStreamAsync(1, _browserRequestHeaders, endStream: false); - // Manually craft a frame where the size is too large. Our own frame class won't allow this. - // See Http2Frame.Length - var length = Http2Frame.MinAllowedMaxFrameSize + 1; // Too big - var frame = new byte[9 + length]; - frame[0] = (byte)((length & 0x00ff0000) >> 16); - frame[1] = (byte)((length & 0x0000ff00) >> 8); - frame[2] = (byte)(length & 0x000000ff); - await SendAsync(frame); + uint length = Http2Limits.MinAllowedMaxFrameSize + 1; + await SendDataAsync(1, new byte[length].AsSpan(), endStream: true); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, - expectedErrorMessage: CoreStrings.FormatHttp2ErrorFrameOverLimit(length, Http2Frame.MinAllowedMaxFrameSize)); + expectedErrorMessage: CoreStrings.FormatHttp2ErrorFrameOverLimit(length, Http2Limits.MinAllowedMaxFrameSize)); + } + + [Fact] + public async Task ServerSettings_ChangesRequestMaxFrameSize() + { + var length = Http2Limits.MinAllowedMaxFrameSize + 10; + _connectionContext.ServiceContext.ServerOptions.Limits.Http2.MaxFrameSize = length; + _connection = new Http2Connection(_connectionContext); + + await InitializeConnectionAsync(_echoApplication, expectedSettingsLegnth: 12); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: false); + await SendDataAsync(1, new byte[length].AsSpan(), endStream: true); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 37, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + // The client's settings is still defaulted to Http2PeerSettings.MinAllowedMaxFrameSize so the echo response will come back in two separate frames + await ExpectAsync(Http2FrameType.DATA, + withLength: Http2Limits.MinAllowedMaxFrameSize, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: length - Http2Limits.MinAllowedMaxFrameSize, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); } [Fact] @@ -2042,7 +2069,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { await InitializeConnectionAsync(_noopApplication); - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareSettings(Http2SettingsFrameFlags.ACK); await SendAsync(frame.Raw); @@ -2073,6 +2100,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [InlineData(Http2SettingsParameter.SETTINGS_MAX_FRAME_SIZE, 16 * 1024 - 1, Http2ErrorCode.PROTOCOL_ERROR)] [InlineData(Http2SettingsParameter.SETTINGS_MAX_FRAME_SIZE, 16 * 1024 * 1024, Http2ErrorCode.PROTOCOL_ERROR)] [InlineData(Http2SettingsParameter.SETTINGS_MAX_FRAME_SIZE, uint.MaxValue, Http2ErrorCode.PROTOCOL_ERROR)] + [InlineData(Http2SettingsParameter.SETTINGS_HEADER_TABLE_SIZE, 4097, Http2ErrorCode.PROTOCOL_ERROR)] public async Task SETTINGS_Received_InvalidParameterValue_ConnectionError(Http2SettingsParameter parameter, uint value, Http2ErrorCode expectedErrorCode) { await InitializeConnectionAsync(_noopApplication); @@ -2160,6 +2188,60 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests expectedErrorMessage: CoreStrings.Http2ErrorInitialWindowSizeInvalid); } + [Fact] + public async Task SETTINGS_Received_ChangesAllowedResponseMaxFrameSize() + { + // This includes the default response headers such as :status, etc + var defaultResponseHeaderLength = 37; + var headerValueLength = Http2Limits.MinAllowedMaxFrameSize; + // First byte is always 0 + // Second byte is the length of header name which is 1 + // Third byte is the header name which is A/B + // Next three bytes are the 7-bit integer encoding representation of the header length which is 16*1024 + var encodedHeaderLength = 1 + 1 + 1 + 3 + headerValueLength; + // Adding 10 additional bytes for encoding overhead + var payloadLength = defaultResponseHeaderLength + encodedHeaderLength; + + await InitializeConnectionAsync(context => + { + context.Response.Headers["A"] = new string('a', headerValueLength); + context.Response.Headers["B"] = new string('b', headerValueLength); + return context.Response.Body.WriteAsync(new byte[payloadLength], 0, payloadLength); + }); + + // Update client settings + _clientSettings.MaxFrameSize = (uint)payloadLength; + await SendSettingsAsync(); + + // ACK + await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 0, + withFlags: (byte)Http2SettingsFrameFlags.ACK, + withStreamId: 0); + + // Start request + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: defaultResponseHeaderLength + encodedHeaderLength, + withFlags: (byte)Http2HeadersFrameFlags.NONE, + withStreamId: 1); + await ExpectAsync(Http2FrameType.CONTINUATION, + withLength: encodedHeaderLength, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: payloadLength, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + } + [Fact] public async Task PUSH_PROMISE_Received_ConnectionError() { diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs index 4fa61a11df..7449a22f2f 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -141,7 +141,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _echoApplication = async context => { - var buffer = new byte[Http2Frame.MinAllowedMaxFrameSize]; + var buffer = new byte[Http2Limits.MinAllowedMaxFrameSize]; var received = 0; while ((received = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0) @@ -152,7 +152,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _echoWaitForAbortApplication = async context => { - var buffer = new byte[Http2Frame.MinAllowedMaxFrameSize]; + var buffer = new byte[Http2Limits.MinAllowedMaxFrameSize]; var received = 0; while ((received = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0) @@ -307,7 +307,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters(); } - protected async Task InitializeConnectionAsync(RequestDelegate application) + protected async Task InitializeConnectionAsync(RequestDelegate application, int expectedSettingsLegnth = 6) { _connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(application)); @@ -315,7 +315,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendSettingsAsync(); await ExpectAsync(Http2FrameType.SETTINGS, - withLength: 6, + withLength: expectedSettingsLegnth, withFlags: 0, withStreamId: 0); @@ -330,7 +330,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _runningStreams[streamId] = tcs; - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); var done = _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); frame.PayloadLength = length; @@ -367,7 +367,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _runningStreams[streamId] = tcs; - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PADDED, streamId); frame.HeadersPadLength = padLength; @@ -390,7 +390,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _runningStreams[streamId] = tcs; - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PRIORITY, streamId); frame.HeadersPriorityWeight = priority; frame.HeadersStreamDependency = streamDependency; @@ -412,7 +412,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _runningStreams[streamId] = tcs; - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PADDED | Http2HeadersFrameFlags.PRIORITY, streamId); frame.HeadersPadLength = padLength; frame.HeadersPriorityWeight = priority; @@ -452,14 +452,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendSettingsAsync() { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); return SendAsync(frame.Raw); } protected Task SendSettingsAckWithInvalidLengthAsync(int length) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareSettings(Http2SettingsFrameFlags.ACK); frame.PayloadLength = length; return SendAsync(frame.Raw); @@ -467,7 +467,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendSettingsWithInvalidStreamIdAsync(int streamId) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); frame.StreamId = streamId; return SendAsync(frame.Raw); @@ -475,7 +475,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendSettingsWithInvalidLengthAsync(int length) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); frame.PayloadLength = length; return SendAsync(frame.Raw); @@ -483,7 +483,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendSettingsWithInvalidParameterValueAsync(Http2SettingsParameter parameter, uint value) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareSettings(Http2SettingsFrameFlags.NONE); frame.PayloadLength = 6; @@ -499,7 +499,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendPushPromiseFrameAsync() { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PayloadLength = 0; frame.Type = Http2FrameType.PUSH_PROMISE; frame.StreamId = 1; @@ -508,7 +508,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected async Task SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, IEnumerable> headers) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareHeaders(flags, streamId); var done = _hpackEncoder.BeginEncode(headers, frame.Payload, out var length); @@ -521,7 +521,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, byte[] headerBlock) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareHeaders(flags, streamId); frame.PayloadLength = headerBlock.Length; @@ -534,7 +534,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { Assert.True(padLength >= payloadLength, $"{nameof(padLength)} must be greater than or equal to {nameof(payloadLength)} to create an invalid frame."); - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareHeaders(Http2HeadersFrameFlags.PADDED, streamId); frame.Payload[0] = padLength; @@ -547,7 +547,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendIncompleteHeadersFrameAsync(int streamId) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, streamId); frame.PayloadLength = 3; @@ -563,7 +563,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected async Task SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareContinuation(flags, streamId); var done = _hpackEncoder.Encode(frame.Payload, out var length); @@ -576,7 +576,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected async Task SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags, byte[] payload) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareContinuation(flags, streamId); frame.PayloadLength = payload.Length; @@ -587,7 +587,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendEmptyContinuationFrameAsync(int streamId, Http2ContinuationFrameFlags flags) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareContinuation(flags, streamId); frame.PayloadLength = 0; @@ -597,7 +597,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendIncompleteContinuationFrameAsync(int streamId) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareContinuation(Http2ContinuationFrameFlags.END_HEADERS, streamId); frame.PayloadLength = 3; @@ -613,7 +613,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendDataAsync(int streamId, Span data, bool endStream) { - var frame = new Http2Frame(); + var frame = new Http2Frame((uint)data.Length); frame.PrepareData(streamId); frame.PayloadLength = data.Length; @@ -625,7 +625,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendDataWithPaddingAsync(int streamId, Span data, byte padLength, bool endStream) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareData(streamId, padLength); frame.PayloadLength = data.Length + 1 + padLength; @@ -643,7 +643,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { Assert.True(padLength >= frameLength, $"{nameof(padLength)} must be greater than or equal to {nameof(frameLength)} to create an invalid frame."); - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareData(streamId); frame.DataFlags = Http2DataFrameFlags.PADDED; @@ -657,14 +657,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendPingAsync(Http2PingFrameFlags flags) { - var pingFrame = new Http2Frame(); + var pingFrame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); pingFrame.PreparePing(flags); return SendAsync(pingFrame.Raw); } protected Task SendPingWithInvalidLengthAsync(int length) { - var pingFrame = new Http2Frame(); + var pingFrame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); pingFrame.PreparePing(Http2PingFrameFlags.NONE); pingFrame.PayloadLength = length; return SendAsync(pingFrame.Raw); @@ -674,7 +674,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { Assert.NotEqual(0, streamId); - var pingFrame = new Http2Frame(); + var pingFrame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); pingFrame.PreparePing(Http2PingFrameFlags.NONE); pingFrame.StreamId = streamId; return SendAsync(pingFrame.Raw); @@ -682,14 +682,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendPriorityAsync(int streamId, int streamDependency = 0) { - var priorityFrame = new Http2Frame(); + var priorityFrame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); priorityFrame.PreparePriority(streamId, streamDependency: streamDependency, exclusive: false, weight: 0); return SendAsync(priorityFrame.Raw); } protected Task SendInvalidPriorityFrameAsync(int streamId, int length) { - var priorityFrame = new Http2Frame(); + var priorityFrame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); priorityFrame.PreparePriority(streamId, streamDependency: 0, exclusive: false, weight: 0); priorityFrame.PayloadLength = length; return SendAsync(priorityFrame.Raw); @@ -697,14 +697,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendRstStreamAsync(int streamId) { - var rstStreamFrame = new Http2Frame(); + var rstStreamFrame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); rstStreamFrame.PrepareRstStream(streamId, Http2ErrorCode.CANCEL); return SendAsync(rstStreamFrame.Raw); } protected Task SendInvalidRstStreamFrameAsync(int streamId, int length) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareRstStream(streamId, Http2ErrorCode.CANCEL); frame.PayloadLength = length; return SendAsync(frame.Raw); @@ -712,14 +712,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendGoAwayAsync() { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareGoAway(0, Http2ErrorCode.NO_ERROR); return SendAsync(frame.Raw); } protected Task SendInvalidGoAwayFrameAsync() { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareGoAway(0, Http2ErrorCode.NO_ERROR); frame.StreamId = 1; return SendAsync(frame.Raw); @@ -727,14 +727,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendWindowUpdateAsync(int streamId, int sizeIncrement) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareWindowUpdate(streamId, sizeIncrement); return SendAsync(frame.Raw); } protected Task SendInvalidWindowUpdateAsync(int streamId, int sizeIncrement, int length) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.PrepareWindowUpdate(streamId, sizeIncrement); frame.PayloadLength = length; return SendAsync(frame.Raw); @@ -742,16 +742,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendUnknownFrameTypeAsync(int streamId, int frameType) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); frame.StreamId = streamId; frame.Type = (Http2FrameType)frameType; frame.PayloadLength = 0; return SendAsync(frame.Raw); } - protected async Task ReceiveFrameAsync() + protected async Task ReceiveFrameAsync(uint maxFrameSize = Http2PeerSettings.DefaultMaxFrameSize) { - var frame = new Http2Frame(); + var frame = new Http2Frame(maxFrameSize); while (true) { @@ -764,7 +764,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { Assert.True(buffer.Length > 0); - if (Http2FrameReader.ReadFrame(buffer, frame, 16_384, out consumed, out examined)) + if (Http2FrameReader.ReadFrame(buffer, frame, maxFrameSize, out consumed, out examined)) { return frame; } @@ -783,7 +783,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected async Task ExpectAsync(Http2FrameType type, int withLength, byte withFlags, int withStreamId) { - var frame = await ReceiveFrameAsync(); + var frame = await ReceiveFrameAsync((uint)withLength); Assert.Equal(type, frame.Type); Assert.Equal(withLength, frame.PayloadLength); diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs index fda221726e..93718e2c34 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs @@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.Http2 private async Task ReceiveFrameAsync(PipeReader reader) { - var frame = new Http2Frame(); + var frame = new Http2Frame(Http2Limits.MinAllowedMaxFrameSize); while (true) {