Disable request body data rate limits per HTTP/2 stream (#10355)

This commit is contained in:
Stephen Halter 2019-05-17 18:25:41 -07:00 committed by GitHub
parent 2e89aa4aa6
commit 8d83e5352d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 130 additions and 35 deletions

View File

@ -599,4 +599,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="StartAsyncBeforeGetMemory" xml:space="preserve">
<value>Cannot call GetMemory() until response has started. Call HttpResponse.StartAsync() before calling GetMemory().</value>
</data>
<data name="Http2MinDataRateNotSupported" xml:space="preserve">
<value>This feature is not supported for HTTP/2 requests except to disable it entirely by setting the rate to null.</value>
</data>
</root>

View File

@ -5,8 +5,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
{
/// <summary>
/// Feature to set the minimum data rate at which the the request body must be sent by the client.
/// This feature is not available for HTTP/2 requests. Instead, use <see cref="KestrelServerLimits.MinRequestBodyDataRate"/>
/// for server-wide configuration which applies to both HTTP/2 and HTTP/1.x.
/// This feature is not supported for HTTP/2 requests except to disable it entirely by setting <see cref="MinDataRate"/> to <see langword="null"/>
/// Instead, use <see cref="KestrelServerLimits.MinRequestBodyDataRate"/> for server-wide configuration which applies to both HTTP/2 and HTTP/1.x.
/// </summary>
public interface IHttpMinRequestBodyDataRateFeature
{
@ -14,8 +14,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
/// The minimum data rate in bytes/second at which the request body must be sent by the client.
/// Setting this property to null indicates no minimum data rate should be enforced.
/// This limit has no effect on upgraded connections which are always unlimited.
/// This feature is not available for HTTP/2 requests. Instead, use <see cref="KestrelServerLimits.MinRequestBodyDataRate"/>
/// for server-wide configuration which applies to both HTTP/2 and HTTP/1.x.
/// This feature is not supported for HTTP/2 requests except to disable it entirely by setting <see cref="MinDataRate"/> to <see langword="null"/>
/// Instead, use <see cref="KestrelServerLimits.MinRequestBodyDataRate"/> for server-wide configuration which applies to both HTTP/2 and HTTP/1.x.
/// </summary>
MinDataRate MinDataRate { get; set; }
}

View File

@ -1,18 +1,12 @@
// 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.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
internal partial class Http1Connection : IHttpMinRequestBodyDataRateFeature,
IHttpMinResponseDataRateFeature
IHttpMinResponseDataRateFeature
{
MinDataRate IHttpMinRequestBodyDataRateFeature.MinDataRate
{

View File

@ -62,8 +62,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public bool RequestTimedOut => _requestTimedOut;
public MinDataRate MinRequestBodyDataRate { get; set; }
public MinDataRate MinResponseDataRate { get; set; }
public MemoryPool<byte> MemoryPool { get; }
@ -542,7 +540,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_remainingRequestHeadersBytesAllowed = ServerOptions.Limits.MaxRequestHeadersTotalSize + 2;
_requestCount++;
MinRequestBodyDataRate = ServerOptions.Limits.MinRequestBodyDataRate;
MinResponseDataRate = ServerOptions.Limits.MinResponseDataRate;
}

View File

@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected readonly Http1Connection _context;
protected Http1MessageBody(Http1Connection context)
: base(context, context.MinRequestBodyDataRate)
: base(context)
{
_context = context;
}

View File

@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpRequestLifetimeFeature = this;
_currentIHttpConnectionFeature = this;
_currentIHttpMaxRequestBodySizeFeature = this;
_currentIHttpMinRequestBodyDataRateFeature = this;
_currentIHttpBodyControlFeature = this;
_currentIHttpResponseStartFeature = this;
_currentIRouteValuesFeature = this;
@ -100,7 +101,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentITlsConnectionFeature = null;
_currentIHttpWebSocketFeature = null;
_currentISessionFeature = null;
_currentIHttpMinRequestBodyDataRateFeature = null;
_currentIHttpMinResponseDataRateFeature = null;
_currentIHttpSendFileFeature = null;
}

View File

@ -101,6 +101,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public string ConnectionIdFeature { get; set; }
public bool HasStartedConsumingRequestBody { get; set; }
public long? MaxRequestBodySize { get; set; }
public MinDataRate MinRequestBodyDataRate { get; set; }
public bool AllowSynchronousIO { get; set; }
/// <summary>
@ -340,6 +341,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
HasStartedConsumingRequestBody = false;
MaxRequestBodySize = ServerOptions.Limits.MaxRequestBodySize;
MinRequestBodyDataRate = ServerOptions.Limits.MinRequestBodyDataRate;
AllowSynchronousIO = ServerOptions.AllowSynchronousIO;
TraceIdentifier = null;
Method = HttpMethod.None;

View File

@ -15,7 +15,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly MessageBody _zeroContentLengthKeepAlive = new ZeroContentLengthMessageBody(keepAlive: true);
private readonly HttpProtocol _context;
private readonly MinDataRate _minRequestBodyDataRate;
private bool _send100Continue = true;
private long _consumedBytes;
@ -25,10 +24,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected bool _backpressure;
protected long _alreadyTimedBytes;
protected MessageBody(HttpProtocol context, MinDataRate minRequestBodyDataRate)
protected MessageBody(HttpProtocol context)
{
_context = context;
_minRequestBodyDataRate = minRequestBodyDataRate;
}
public static MessageBody ZeroContentLengthClose => _zeroContentLengthClose;
@ -98,10 +96,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
Log.RequestBodyStart(_context.ConnectionIdFeature, _context.TraceIdentifier);
if (_minRequestBodyDataRate != null)
if (_context.MinRequestBodyDataRate != null)
{
_timingEnabled = true;
_context.TimeoutControl.StartRequestBody(_minRequestBodyDataRate);
_context.TimeoutControl.StartRequestBody(_context.MinRequestBodyDataRate);
}
}

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
internal sealed class ZeroContentLengthMessageBody : MessageBody
{
public ZeroContentLengthMessageBody(bool keepAlive)
: base(null, null)
: base(null)
{
RequestKeepAlive = keepAlive;
}

View File

@ -16,8 +16,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
private ReadResult _readResult;
private long _alreadyExaminedInNextReadResult;
private Http2MessageBody(Http2Stream context, MinDataRate minRequestBodyDataRate)
: base(context, minRequestBodyDataRate)
private Http2MessageBody(Http2Stream context)
: base(context)
{
_context = context;
}
@ -47,14 +47,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
AddAndCheckConsumedBytes(bytesRead);
}
public static MessageBody For(Http2Stream context, MinDataRate minRequestBodyDataRate)
public static MessageBody For(Http2Stream context)
{
if (context.ReceivedEmptyRequestBody)
{
return ZeroContentLengthClose;
}
return new Http2MessageBody(context, minRequestBodyDataRate);
return new Http2MessageBody(context);
}
public override void AdvanceTo(SequencePosition consumed)

View File

@ -1,6 +1,7 @@
// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
@ -8,7 +9,10 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
internal partial class Http2Stream : IHttp2StreamIdFeature, IHttpResponseTrailersFeature
internal partial class Http2Stream : IHttp2StreamIdFeature,
IHttpMinRequestBodyDataRateFeature,
IHttpResponseTrailersFeature
{
internal HttpResponseTrailers Trailers { get; set; }
private IHeaderDictionary _userTrailers;
@ -30,5 +34,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
int IHttp2StreamIdFeature.StreamId => _context.StreamId;
MinDataRate IHttpMinRequestBodyDataRateFeature.MinDataRate
{
get => throw new NotSupportedException(CoreStrings.Http2MinDataRateNotSupported);
set
{
if (value != null)
{
throw new NotSupportedException(CoreStrings.Http2MinDataRateNotSupported);
}
MinRequestBodyDataRate = value;
}
}
}
}

View File

@ -125,7 +125,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
=> StringUtilities.ConcatAsHexSuffix(ConnectionId, ':', (uint)StreamId);
protected override MessageBody CreateMessageBody()
=> Http2MessageBody.For(this, ServerOptions.Limits.MinRequestBodyDataRate);
=> Http2MessageBody.For(this);
// Compare to Http1Connection.OnStartLine
protected override bool TryParseRequest(ReadResult result, out bool endConnection)

View File

@ -2254,6 +2254,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatStartAsyncBeforeGetMemory()
=> GetString("StartAsyncBeforeGetMemory");
/// <summary>
/// This feature is not supported for HTTP/2 requests except to disable it entirely by setting the rate to null.
/// </summary>
internal static string Http2MinDataRateNotSupported
{
get => GetString("Http2MinDataRateNotSupported");
}
/// <summary>
/// This feature is not supported for HTTP/2 requests except to disable it entirely by setting the rate to null.
/// </summary>
internal static string FormatHttp2MinDataRateNotSupported()
=> GetString("Http2MinDataRateNotSupported");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
private class MockMessageBody : MessageBody
{
public MockMessageBody(bool upgradeable = false)
: base(null, null)
: base(null)
{
RequestUpgrade = upgradeable;
}

View File

@ -888,7 +888,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var buffer = new byte[10];
await context.Response.Body.WriteAsync(buffer, 0, 10);
});
var mockMessageBody = new Mock<MessageBody>(null, null);
var mockMessageBody = new Mock<MessageBody>(null);
_http1Connection.NextMessageBody = mockMessageBody.Object;
var requestProcessingTask = _http1Connection.ProcessRequestsAsync(httpApplication);

View File

@ -159,12 +159,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
[Fact]
public void Http2StreamFeatureCollectionDoesNotIncludeMinRateFeatures()
public void Http2StreamFeatureCollectionDoesNotIncludeIHttpMinResponseDataRateFeature()
{
Assert.Null(_http2Collection.Get<IHttpMinRequestBodyDataRateFeature>());
Assert.Null(_http2Collection.Get<IHttpMinResponseDataRateFeature>());
Assert.NotNull(_collection.Get<IHttpMinRequestBodyDataRateFeature>());
Assert.NotNull(_collection.Get<IHttpMinResponseDataRateFeature>());
}
@ -177,6 +174,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.False(upgradeFeature.IsUpgradableRequest);
}
[Fact]
public void Http2StreamFeatureCollectionDoesIncludeIHttpMinRequestBodyDataRateFeature()
{
var minRateFeature = _http2Collection.Get<IHttpMinRequestBodyDataRateFeature>();
Assert.NotNull(minRateFeature);
Assert.Throws<NotSupportedException>(() => minRateFeature.MinDataRate);
Assert.Throws<NotSupportedException>(() => minRateFeature.MinDataRate = new MinDataRate(1, TimeSpan.FromSeconds(2)));
// You can set the MinDataRate to null though.
minRateFeature.MinDataRate = null;
// But you still cannot read the property;
Assert.Throws<NotSupportedException>(() => minRateFeature.MinDataRate);
}
private void CompareGenericGetterToIndexer()
{
Assert.Same(_collection.Get<IHttpRequestFeature>(), _collection[typeof(IHttpRequestFeature)]);

View File

@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockBodyControl = new Mock<IHttpBodyControlFeature>();
mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(() => allowSynchronousIO);
var mockMessageBody = new Mock<MessageBody>(null, null);
var mockMessageBody = new Mock<MessageBody>(null);
mockMessageBody.Setup(m => m.ReadAsync(CancellationToken.None)).Returns(new ValueTask<ReadResult>(new ReadResult(default, isCanceled: false, isCompleted: true)));
var pipeReader = new HttpRequestPipeReader();

View File

@ -798,6 +798,60 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_mockConnectionContext.VerifyNoOtherCalls();
}
[Fact]
public async Task DATA_Received_SlowlyWhenRateLimitDisabledPerRequest_DoesNotAbortConnection()
{
var mockSystemClock = _serviceContext.MockSystemClock;
var limits = _serviceContext.ServerOptions.Limits;
// Use non-default value to ensure the min request and response rates aren't mixed up.
limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));
_timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks);
await InitializeConnectionAsync(context =>
{
// Completely disable rate limiting for this stream.
context.Features.Get<IHttpMinRequestBodyDataRateFeature>().MinDataRate = null;
return _readRateApplication(context);
});
// _helloWorldBytes is 12 bytes, and 12 bytes / 240 bytes/sec = .05 secs which is far below the grace period.
await StartStreamAsync(1, ReadRateRequestHeaders(_helloWorldBytes.Length), endStream: false);
await SendDataAsync(1, _helloWorldBytes, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 1,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 1);
// Don't send any more data and advance just to and then past the grace period.
AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod);
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
AdvanceClock(TimeSpan.FromTicks(1));
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
await SendDataAsync(1, _helloWorldBytes, endStream: true);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
_mockTimeoutHandler.VerifyNoOtherCalls();
_mockConnectionContext.VerifyNoOtherCalls();
}
[Fact]
public async Task DATA_Received_SlowlyDueToConnectionFlowControl_DoesNotAbortConnection()
{

View File

@ -71,6 +71,7 @@ namespace CodeGenerator
"IHttpRequestLifetimeFeature",
"IHttpConnectionFeature",
"IHttpMaxRequestBodySizeFeature",
"IHttpMinRequestBodyDataRateFeature",
"IHttpBodyControlFeature",
"IHttpResponseStartFeature",
"IRouteValuesFeature",