Disable request body data rate limits per HTTP/2 stream (#10355)
This commit is contained in:
parent
2e89aa4aa6
commit
8d83e5352d
|
|
@ -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>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)]);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ namespace CodeGenerator
|
|||
"IHttpRequestLifetimeFeature",
|
||||
"IHttpConnectionFeature",
|
||||
"IHttpMaxRequestBodySizeFeature",
|
||||
"IHttpMinRequestBodyDataRateFeature",
|
||||
"IHttpBodyControlFeature",
|
||||
"IHttpResponseStartFeature",
|
||||
"IRouteValuesFeature",
|
||||
|
|
|
|||
Loading…
Reference in New Issue