diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx index eb30b393f4..f119165500 100644 --- a/src/Kestrel.Core/CoreStrings.resx +++ b/src/Kestrel.Core/CoreStrings.resx @@ -566,4 +566,10 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l An error occured after the response headers were sent, a reset is being sent. + + A new stream was refused because this connection has reached its stream limit. + + + A value greater than zero is required. + \ No newline at end of file diff --git a/src/Kestrel.Core/Http2Limits.cs b/src/Kestrel.Core/Http2Limits.cs new file mode 100644 index 0000000000..34a4ef36f1 --- /dev/null +++ b/src/Kestrel.Core/Http2Limits.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core +{ + /// + /// Limits only applicable to HTTP/2 connections. + /// + public class Http2Limits + { + private int _maxStreamsPerConnection = 100; + + /// + /// Limits the number of concurrent request streams per HTTP/2 connection. Excess streams will be refused. + /// + /// Defaults to 100 + /// + /// + public int MaxStreamsPerConnection + { + get => _maxStreamsPerConnection; + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanZeroRequired); + } + _maxStreamsPerConnection = value; + } + } + } +} diff --git a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs index 47e2605578..69defdbc1b 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs @@ -88,6 +88,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _context = context; _frameWriter = new Http2FrameWriter(context.Transport.Output, context.Application.Input, _outputFlowControl, this, context.ConnectionId, context.ServiceContext.Log); _hpackDecoder = new HPackDecoder((int)_serverSettings.HeaderTableSize); + _serverSettings.MaxConcurrentStreams = (uint)context.ServiceContext.ServerOptions.Limits.Http2.MaxStreamsPerConnection; } public string ConnectionId => _context.ConnectionId; @@ -98,6 +99,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public IFeatureCollection ConnectionFeatures => _context.ConnectionFeatures; + internal Http2PeerSettings ServerSettings => _serverSettings; + public void OnInputOrOutputCompleted() { lock (_stateLock) @@ -172,7 +175,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 if (_state != Http2ConnectionState.Closed) { - await _frameWriter.WriteSettingsAsync(_serverSettings); + await _frameWriter.WriteSettingsAsync(_serverSettings.GetNonProtocolDefaults()); } while (_state != Http2ConnectionState.Closed) @@ -617,10 +620,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 try { - // ParseFrame will not parse an InitialWindowSize > int.MaxValue. + // int.MaxValue is the largest allowed windows size. var previousInitialWindowSize = (int)_clientSettings.InitialWindowSize; - _clientSettings.ParseFrame(_incomingFrame); + _clientSettings.Update(_incomingFrame.GetSettings()); var ackTask = _frameWriter.WriteSettingsAckAsync(); // Ack before we update the windows, they could send data immediately. @@ -838,6 +841,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2ErrorMissingMandatoryPseudoHeaderFields, Http2ErrorCode.PROTOCOL_ERROR); } + if (_streams.Count >= _serverSettings.MaxConcurrentStreams) + { + throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2ErrorMaxStreams, Http2ErrorCode.REFUSED_STREAM); + } + // This must be initialized before we offload the request or else we may start processing request body frames without it. _currentHeadersStream.InputRemaining = _currentHeadersStream.RequestHeaders.ContentLength; diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs index 04cc78b209..c3da784e01 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs @@ -1,42 +1,67 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; +using System.Buffers.Binary; +using System.Collections.Generic; using System.Linq; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { public partial class Http2Frame { + private const int SettingSize = 6; // 2 bytes for the id, 4 bytes for the value. + public Http2SettingsFrameFlags SettingsFlags { get => (Http2SettingsFrameFlags)Flags; set => Flags = (byte)value; } - public void PrepareSettings(Http2SettingsFrameFlags flags, Http2PeerSettings settings = null) + public int SettingsCount { - var settingCount = settings?.Count() ?? 0; + get => Length / SettingSize; + set => Length = value * SettingSize; + } - Length = 6 * settingCount; + public IList GetSettings() + { + var settings = new Http2PeerSetting[SettingsCount]; + for (int i = 0; i < settings.Length; i++) + { + settings[i] = GetSetting(i); + } + return settings; + } + + private Http2PeerSetting GetSetting(int index) + { + var offset = index * SettingSize; + var payload = Payload.Slice(offset); + var id = (Http2SettingsParameter)BinaryPrimitives.ReadUInt16BigEndian(payload); + var value = BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(2)); + + return new Http2PeerSetting(id, value); + } + + public void PrepareSettings(Http2SettingsFrameFlags flags, IList settings = null) + { + var settingCount = settings?.Count ?? 0; + SettingsCount = settingCount; Type = Http2FrameType.SETTINGS; SettingsFlags = flags; StreamId = 0; - - if (settings != null) + for (int i = 0; i < settingCount; i++) { - Span payload = Payload; - foreach (var setting in settings) - { - payload[0] = (byte)((ushort)setting.Parameter >> 8); - payload[1] = (byte)(ushort)setting.Parameter; - payload[2] = (byte)(setting.Value >> 24); - payload[3] = (byte)(setting.Value >> 16); - payload[4] = (byte)(setting.Value >> 8); - payload[5] = (byte)setting.Value; - payload = payload.Slice(6); - } + SetSetting(i, settings[i]); } } + + private void SetSetting(int index, Http2PeerSetting setting) + { + var offset = index * SettingSize; + var payload = Payload.Slice(offset); + BinaryPrimitives.WriteUInt16BigEndian(payload, (ushort)setting.Parameter); + BinaryPrimitives.WriteUInt32BigEndian(payload.Slice(2), setting.Value); + } } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs index a0a679d15b..f867f36490 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs @@ -270,12 +270,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - public Task WriteSettingsAsync(Http2PeerSettings settings) + public Task WriteSettingsAsync(IList settings) { lock (_writeLock) { - // TODO: actually send settings - _outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.NONE); + _outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.NONE, settings); return WriteFrameUnsynchronizedAsync(); } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2PeerSettings.cs b/src/Kestrel.Core/Internal/Http2/Http2PeerSettings.cs index fcf78c4b42..eeb13bb808 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2PeerSettings.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2PeerSettings.cs @@ -1,13 +1,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Collections; using System.Collections.Generic; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { - public class Http2PeerSettings : IEnumerable + public class Http2PeerSettings { + // Note these are protocol defaults, not Kestrel defaults. public const uint DefaultHeaderTableSize = 4096; public const bool DefaultEnablePush = true; public const uint DefaultMaxConcurrentStreams = uint.MaxValue; @@ -28,20 +28,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public uint MaxHeaderListSize { get; set; } = DefaultMaxHeaderListSize; - public void ParseFrame(Http2Frame frame) + // TODO: Return the diff so we can react + public void Update(IList settings) { - var settingsCount = frame.Length / 6; - - for (var i = 0; i < settingsCount; i++) + foreach (var setting in settings) { - var offset = i * 6; - var id = (Http2SettingsParameter)((frame.Payload[offset] << 8) | frame.Payload[offset + 1]); - var value = (uint)((frame.Payload[offset + 2] << 24) - | (frame.Payload[offset + 3] << 16) - | (frame.Payload[offset + 4] << 8) - | frame.Payload[offset + 5]); + var value = setting.Value; - switch (id) + switch (setting.Parameter) { case Http2SettingsParameter.SETTINGS_HEADER_TABLE_SIZE: HeaderTableSize = value; @@ -91,16 +85,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - public IEnumerator GetEnumerator() + // Gets the settings that are different from the protocol defaults (as opposed to the server defaults). + internal IList GetNonProtocolDefaults() { - yield return new Http2PeerSetting(Http2SettingsParameter.SETTINGS_HEADER_TABLE_SIZE, HeaderTableSize); - yield return new Http2PeerSetting(Http2SettingsParameter.SETTINGS_ENABLE_PUSH, EnablePush ? 1u : 0); - yield return new Http2PeerSetting(Http2SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS, MaxConcurrentStreams); - yield return new Http2PeerSetting(Http2SettingsParameter.SETTINGS_INITIAL_WINDOW_SIZE, InitialWindowSize); - yield return new Http2PeerSetting(Http2SettingsParameter.SETTINGS_MAX_FRAME_SIZE, MaxFrameSize); - yield return new Http2PeerSetting(Http2SettingsParameter.SETTINGS_MAX_HEADER_LIST_SIZE, MaxHeaderListSize); - } + var list = new List(1); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + if (HeaderTableSize != DefaultHeaderTableSize) + { + list.Add(new Http2PeerSetting(Http2SettingsParameter.SETTINGS_HEADER_TABLE_SIZE, HeaderTableSize)); + } + + if (EnablePush != DefaultEnablePush) + { + list.Add(new Http2PeerSetting(Http2SettingsParameter.SETTINGS_ENABLE_PUSH, EnablePush ? 1u : 0)); + } + + if (MaxConcurrentStreams != DefaultMaxConcurrentStreams) + { + list.Add(new Http2PeerSetting(Http2SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS, MaxConcurrentStreams)); + } + + if (InitialWindowSize != DefaultInitialWindowSize) + { + list.Add(new Http2PeerSetting(Http2SettingsParameter.SETTINGS_INITIAL_WINDOW_SIZE, InitialWindowSize)); + } + + if (MaxFrameSize != DefaultMaxFrameSize) + { + list.Add(new Http2PeerSetting(Http2SettingsParameter.SETTINGS_MAX_FRAME_SIZE, MaxFrameSize)); + } + + if (MaxHeaderListSize != DefaultMaxHeaderListSize) + { + list.Add(new Http2PeerSetting(Http2SettingsParameter.SETTINGS_MAX_HEADER_LIST_SIZE, MaxHeaderListSize)); + } + + return list; + } } } diff --git a/src/Kestrel.Core/KestrelServerLimits.cs b/src/Kestrel.Core/KestrelServerLimits.cs index f2f8e773ac..e7ddc479df 100644 --- a/src/Kestrel.Core/KestrelServerLimits.cs +++ b/src/Kestrel.Core/KestrelServerLimits.cs @@ -251,6 +251,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } } + /// + /// Limits only applicable to HTTP/2 connections. + /// + public Http2Limits Http2 { get; } = new Http2Limits(); + /// /// Gets or sets the request body minimum data rate in bytes/second. /// Setting this property to null indicates no minimum data rate should be enforced. diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs index fdc5d3fd3a..2d24e413e8 100644 --- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -2100,6 +2100,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatHttp2StreamErrorAfterHeaders() => GetString("Http2StreamErrorAfterHeaders"); + /// + /// A new stream was refused because this connection has reached its stream limit. + /// + internal static string Http2ErrorMaxStreams + { + get => GetString("Http2ErrorMaxStreams"); + } + + /// + /// A new stream was refused because this connection has reached its stream limit. + /// + internal static string FormatHttp2ErrorMaxStreams() + => GetString("Http2ErrorMaxStreams"); + + /// + /// A value greater than zero is required. + /// + internal static string GreaterThanZeroRequired + { + get => GetString("GreaterThanZeroRequired"); + } + + /// + /// A value greater than zero is required. + /// + internal static string FormatGreaterThanZeroRequired() + => GetString("GreaterThanZeroRequired"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs b/test/Kestrel.Core.Tests/Http2ConnectionTests.cs index a41526e333..182b7418d9 100644 --- a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs +++ b/test/Kestrel.Core.Tests/Http2ConnectionTests.cs @@ -1439,6 +1439,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); } + [Fact] + public async Task HEADERS_OverMaxStreamLimit_Refused() + { + _connection.ServerSettings.MaxConcurrentStreams = 1; + + var requestBlocker = new TaskCompletionSource(); + await InitializeConnectionAsync(context => requestBlocker.Task); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + await StartStreamAsync(3, _browserRequestHeaders, endStream: true); + + await WaitForStreamErrorAsync(3, Http2ErrorCode.REFUSED_STREAM, CoreStrings.Http2ErrorMaxStreams); + + requestBlocker.SetResult(0); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 55, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + } + [Fact] public async Task HEADERS_Received_StreamIdZero_ConnectionError() { @@ -2198,6 +2226,66 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.RST_STREAM, streamId: 1, headersStreamId: 1)); } + [Fact] + public async Task SETTINGS_KestrelDefaults_Sent() + { + _connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(_noopApplication)); + + await SendPreambleAsync().ConfigureAwait(false); + await SendSettingsAsync(); + + var frame = await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 6, + withFlags: 0, + withStreamId: 0); + + // Only non protocol defaults are sent + Assert.Equal(1, frame.SettingsCount); + var settings = frame.GetSettings(); + Assert.Equal(1, settings.Count); + var setting = settings[0]; + Assert.Equal(Http2SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS, setting.Parameter); + Assert.Equal(100u, setting.Value); + + await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 0, + withFlags: (byte)Http2SettingsFrameFlags.ACK, + withStreamId: 0); + + await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + } + + [Fact] + public async Task SETTINGS_Custom_Sent() + { + _connection.ServerSettings.MaxConcurrentStreams = 1; + + _connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(_noopApplication)); + + await SendPreambleAsync().ConfigureAwait(false); + await SendSettingsAsync(); + + var frame = await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 6, + withFlags: 0, + withStreamId: 0); + + // Only non protocol defaults are sent + Assert.Equal(1, frame.SettingsCount); + var settings = frame.GetSettings(); + Assert.Equal(1, settings.Count); + var setting = settings[0]; + Assert.Equal(Http2SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS, setting.Parameter); + Assert.Equal(1u, setting.Value); + + await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 0, + withFlags: (byte)Http2SettingsFrameFlags.ACK, + withStreamId: 0); + + await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + } + [Fact] public async Task SETTINGS_Received_Sends_ACK() { @@ -3499,7 +3587,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendSettingsAsync(); await ExpectAsync(Http2FrameType.SETTINGS, - withLength: 0, + withLength: 6, withFlags: 0, withStreamId: 0); @@ -3637,7 +3725,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests private Task SendSettingsAsync() { var frame = new Http2Frame(); - frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings); + frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); return SendAsync(frame.Raw); } @@ -3652,7 +3740,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests private Task SendSettingsWithInvalidStreamIdAsync(int streamId) { var frame = new Http2Frame(); - frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings); + frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); frame.StreamId = streamId; return SendAsync(frame.Raw); } @@ -3660,7 +3748,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests private Task SendSettingsWithInvalidLengthAsync(int length) { var frame = new Http2Frame(); - frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings); + frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); frame.Length = length; return SendAsync(frame.Raw); } diff --git a/test/Kestrel.Core.Tests/Http2StreamTests.cs b/test/Kestrel.Core.Tests/Http2StreamTests.cs index 3331767990..ead884a765 100644 --- a/test/Kestrel.Core.Tests/Http2StreamTests.cs +++ b/test/Kestrel.Core.Tests/Http2StreamTests.cs @@ -1868,7 +1868,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendSettingsAsync(); await ExpectAsync(Http2FrameType.SETTINGS, - withLength: 0, + withLength: 6, withFlags: 0, withStreamId: 0); @@ -1937,7 +1937,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests private Task SendSettingsAsync() { var frame = new Http2Frame(); - frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings); + frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); return SendAsync(frame.Raw); } diff --git a/test/Kestrel.InMemory.FunctionalTests/HttpProtocolSelectionTests.cs b/test/Kestrel.InMemory.FunctionalTests/HttpProtocolSelectionTests.cs index 4af9d531a3..4ad2c374dc 100644 --- a/test/Kestrel.InMemory.FunctionalTests/HttpProtocolSelectionTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/HttpProtocolSelectionTests.cs @@ -37,8 +37,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests [Fact] public Task Server_Http2Only_Cleartext_Success() { - // Expect a SETTINGS frame (type 0x4) with no payload and no flags - return TestSuccess(HttpProtocols.Http2, Encoding.ASCII.GetString(Http2Connection.ClientPreface), "\x00\x00\x00\x04\x00\x00\x00\x00\x00"); + // Expect a SETTINGS frame (type 0x4) with default settings + return TestSuccess(HttpProtocols.Http2, Encoding.ASCII.GetString(Http2Connection.ClientPreface), + "\x00\x00\x06\x04\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x64"); } private async Task TestSuccess(HttpProtocols serverProtocols, string request, string expectedResponse)