Limit concurrent HTTP/2 Streams per connection #2815

This commit is contained in:
Chris Ross (ASP.NET) 2018-08-14 16:25:04 -07:00
parent 43398482a5
commit 0c2923135b
11 changed files with 267 additions and 53 deletions

View File

@ -566,4 +566,10 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="Http2StreamErrorAfterHeaders" xml:space="preserve">
<value>An error occured after the response headers were sent, a reset is being sent.</value>
</data>
<data name="Http2ErrorMaxStreams" xml:space="preserve">
<value>A new stream was refused because this connection has reached its stream limit.</value>
</data>
<data name="GreaterThanZeroRequired" xml:space="preserve">
<value>A value greater than zero is required.</value>
</data>
</root>

View File

@ -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
{
/// <summary>
/// Limits only applicable to HTTP/2 connections.
/// </summary>
public class Http2Limits
{
private int _maxStreamsPerConnection = 100;
/// <summary>
/// Limits the number of concurrent request streams per HTTP/2 connection. Excess streams will be refused.
/// <para>
/// Defaults to 100
/// </para>
/// </summary>
public int MaxStreamsPerConnection
{
get => _maxStreamsPerConnection;
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanZeroRequired);
}
_maxStreamsPerConnection = value;
}
}
}
}

View File

@ -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;

View File

@ -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<Http2PeerSetting> 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<Http2PeerSetting> 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<byte> 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);
}
}
}

View File

@ -270,12 +270,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
}
public Task WriteSettingsAsync(Http2PeerSettings settings)
public Task WriteSettingsAsync(IList<Http2PeerSetting> settings)
{
lock (_writeLock)
{
// TODO: actually send settings
_outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.NONE);
_outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.NONE, settings);
return WriteFrameUnsynchronizedAsync();
}
}

View File

@ -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<Http2PeerSetting>
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<Http2PeerSetting> 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<Http2PeerSetting> GetEnumerator()
// Gets the settings that are different from the protocol defaults (as opposed to the server defaults).
internal IList<Http2PeerSetting> 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<Http2PeerSetting>(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;
}
}
}

View File

@ -251,6 +251,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
}
}
/// <summary>
/// Limits only applicable to HTTP/2 connections.
/// </summary>
public Http2Limits Http2 { get; } = new Http2Limits();
/// <summary>
/// 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.

View File

@ -2100,6 +2100,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatHttp2StreamErrorAfterHeaders()
=> GetString("Http2StreamErrorAfterHeaders");
/// <summary>
/// A new stream was refused because this connection has reached its stream limit.
/// </summary>
internal static string Http2ErrorMaxStreams
{
get => GetString("Http2ErrorMaxStreams");
}
/// <summary>
/// A new stream was refused because this connection has reached its stream limit.
/// </summary>
internal static string FormatHttp2ErrorMaxStreams()
=> GetString("Http2ErrorMaxStreams");
/// <summary>
/// A value greater than zero is required.
/// </summary>
internal static string GreaterThanZeroRequired
{
get => GetString("GreaterThanZeroRequired");
}
/// <summary>
/// A value greater than zero is required.
/// </summary>
internal static string FormatGreaterThanZeroRequired()
=> GetString("GreaterThanZeroRequired");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -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<object>();
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);
}

View File

@ -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);
}

View File

@ -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)