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