diff --git a/KestrelHttpServer.sln b/KestrelHttpServer.sln
index b5b6dc71f3..c1ba2b1753 100644
--- a/KestrelHttpServer.sln
+++ b/KestrelHttpServer.sln
@@ -25,7 +25,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{0EF2AC
test\shared\KestrelTestLoggerProvider.cs = test\shared\KestrelTestLoggerProvider.cs
test\shared\LifetimeNotImplemented.cs = test\shared\LifetimeNotImplemented.cs
test\shared\MockConnectionInformation.cs = test\shared\MockConnectionInformation.cs
- test\shared\MockFrameControl.cs = test\shared\MockFrameControl.cs
test\shared\MockLogger.cs = test\shared\MockLogger.cs
test\shared\MockSystemClock.cs = test\shared\MockSystemClock.cs
test\shared\StringExtensions.cs = test\shared\StringExtensions.cs
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx b/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx
index 4ccc38f2c0..72d2a8aa5f 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx
@@ -1,17 +1,17 @@
-
@@ -327,4 +327,10 @@
The maximum request body size cannot be modified after the request has been upgraded.
-
\ No newline at end of file
+
+ Value must be a positive TimeSpan.
+
+
+ Value must be a non-negative TimeSpan.
+
+
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpRequestBodyMinimumDataRateFeature.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpRequestBodyMinimumDataRateFeature.cs
new file mode 100644
index 0000000000..9190d73d18
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpRequestBodyMinimumDataRateFeature.cs
@@ -0,0 +1,18 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
+{
+ ///
+ /// Represents a minimum data rate for the request body of an HTTP request.
+ ///
+ public interface IHttpRequestBodyMinimumDataRateFeature
+ {
+ ///
+ /// The minimum data rate in bytes/second at which the request body should be received.
+ /// 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.
+ ///
+ MinimumDataRate MinimumDataRate { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/MinimumDataRate.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/MinimumDataRate.cs
new file mode 100644
index 0000000000..c294d2df3d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/MinimumDataRate.cs
@@ -0,0 +1,41 @@
+// 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.Features
+{
+ public class MinimumDataRate
+ {
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The minimum rate in bytes/second at which data should be processed.
+ /// The amount of time to delay enforcement of .
+ public MinimumDataRate(double rate, TimeSpan gracePeriod)
+ {
+ if (rate <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(rate), CoreStrings.PositiveNumberRequired);
+ }
+
+ if (gracePeriod < TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(nameof(gracePeriod), CoreStrings.NonNegativeTimeSpanRequired);
+ }
+
+ Rate = rate;
+ GracePeriod = gracePeriod;
+ }
+
+ ///
+ /// The minimum rate in bytes/second at which data should be processed.
+ ///
+ public double Rate { get; }
+
+ ///
+ /// The amount of time to delay enforcement of .
+ ///
+ public TimeSpan GracePeriod { get; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs
index dad0f21cd2..9b222a755b 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs
@@ -29,6 +29,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private long _timeoutTimestamp = long.MaxValue;
private TimeoutAction _timeoutAction;
+ private object _readTimingLock = new object();
+ private bool _readTimingEnabled;
+ private bool _readTimingPauseRequested;
+ private long _readTimingElapsedTicks;
+ private long _readTimingBytesRead;
+
private Task _lifetimeTask;
public FrameConnection(FrameConnectionContext context)
@@ -36,6 +42,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_context = context;
}
+ // For testing
+ internal Frame Frame => _frame;
+
+ public bool TimedOut { get; private set; }
+
public string ConnectionId => _context.ConnectionId;
public IPipeWriter Input => _context.Input.Writer;
public IPipeReader Output => _context.Output.Reader;
@@ -91,15 +102,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
}
// _frame must be initialized before adding the connection to the connection manager
- _frame = new Frame(application, new FrameContext
- {
- ConnectionId = _context.ConnectionId,
- ConnectionInformation = _context.ConnectionInformation,
- ServiceContext = _context.ServiceContext,
- TimeoutControl = this,
- Input = input,
- Output = output
- });
+ CreateFrame(application, input, output);
// Do this before the first await so we don't yield control to the transport until we've
// added the connection to the connection manager
@@ -140,9 +143,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
}
}
+ internal void CreateFrame(IHttpApplication application, IPipeReader input, IPipe output)
+ {
+ _frame = new Frame(application, new FrameContext
+ {
+ ConnectionId = _context.ConnectionId,
+ ConnectionInformation = _context.ConnectionInformation,
+ ServiceContext = _context.ServiceContext,
+ TimeoutControl = this,
+ Input = input,
+ Output = output
+ });
+ }
+
public void OnConnectionClosed(Exception ex)
{
- Debug.Assert(_frame != null, $"nameof({_frame}) is null");
+ Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
// Abort the connection (if not already aborted)
_frame.Abort(ex);
@@ -152,7 +168,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public Task StopAsync()
{
- Debug.Assert(_frame != null, $"nameof({_frame}) is null");
+ Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
_frame.Stop();
@@ -161,7 +177,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public void Abort(Exception ex)
{
- Debug.Assert(_frame != null, $"nameof({_frame}) is null");
+ Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
// Abort the connection (if not already aborted)
_frame.Abort(ex);
@@ -169,7 +185,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public Task AbortAsync(Exception ex)
{
- Debug.Assert(_frame != null, $"nameof({_frame}) is null");
+ Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
// Abort the connection (if not already aborted)
_frame.Abort(ex);
@@ -177,16 +193,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
return _lifetimeTask;
}
- public void Timeout()
+ public void SetTimeoutResponse()
{
- Debug.Assert(_frame != null, $"nameof({_frame}) is null");
+ Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
_frame.SetBadRequestState(RequestRejectionReason.RequestTimeout);
}
+ public void Timeout()
+ {
+ Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
+
+ TimedOut = true;
+ _readTimingEnabled = false;
+ _frame.Stop();
+ }
+
private async Task ApplyConnectionAdaptersAsync()
{
- Debug.Assert(_frame != null, $"nameof({_frame}) is null");
+ Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
var features = new FeatureCollection();
var connectionAdapters = _context.ConnectionAdapters;
@@ -231,7 +256,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public void Tick(DateTimeOffset now)
{
- Debug.Assert(_frame != null, $"nameof({_frame}) is null");
+ Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
var timestamp = now.Ticks;
@@ -242,10 +267,41 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
if (_timeoutAction == TimeoutAction.SendTimeoutResponse)
{
- Timeout();
+ SetTimeoutResponse();
}
- _frame.Stop();
+ Timeout();
+ }
+ else
+ {
+ lock (_readTimingLock)
+ {
+ if (_readTimingEnabled)
+ {
+ _readTimingElapsedTicks += timestamp - _lastTimestamp;
+
+ if (_frame.RequestBodyMinimumDataRate?.Rate > 0 && _readTimingElapsedTicks > _frame.RequestBodyMinimumDataRate.GracePeriod.Ticks)
+ {
+ var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond;
+ var rate = Interlocked.Read(ref _readTimingBytesRead) / elapsedSeconds;
+
+ if (rate < _frame.RequestBodyMinimumDataRate.Rate)
+ {
+ Log.RequestBodyMininumDataRateNotSatisfied(_context.ConnectionId, _frame.TraceIdentifier, _frame.RequestBodyMinimumDataRate.Rate);
+ Timeout();
+ }
+ }
+
+ // PauseTimingReads() cannot just set _timingReads to false. It needs to go through at least one tick
+ // before pausing, otherwise _readTimingElapsed might never be updated if PauseTimingReads() is always
+ // called before the next tick.
+ if (_readTimingPauseRequested)
+ {
+ _readTimingEnabled = false;
+ _readTimingPauseRequested = false;
+ }
+ }
+ }
}
Interlocked.Exchange(ref _lastTimestamp, timestamp);
@@ -275,5 +331,47 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
Interlocked.Exchange(ref _timeoutTimestamp, _lastTimestamp + ticks + Heartbeat.Interval.Ticks);
}
+
+ public void StartTimingReads()
+ {
+ lock (_readTimingLock)
+ {
+ _readTimingElapsedTicks = 0;
+ _readTimingBytesRead = 0;
+ _readTimingEnabled = true;
+ }
+ }
+
+ public void StopTimingReads()
+ {
+ lock (_readTimingLock)
+ {
+ _readTimingEnabled = false;
+ }
+ }
+
+ public void PauseTimingReads()
+ {
+ lock (_readTimingLock)
+ {
+ _readTimingPauseRequested = true;
+ }
+ }
+
+ public void ResumeTimingReads()
+ {
+ lock (_readTimingLock)
+ {
+ _readTimingEnabled = true;
+
+ // In case pause and resume were both called between ticks
+ _readTimingPauseRequested = false;
+ }
+ }
+
+ public void BytesRead(int count)
+ {
+ Interlocked.Add(ref _readTimingBytesRead, count);
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs
index 3358811e93..fdfb0d6c30 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs
@@ -10,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
@@ -21,7 +22,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
IHttpConnectionFeature,
IHttpRequestLifetimeFeature,
IHttpRequestIdentifierFeature,
- IHttpMaxRequestBodySizeFeature
+ IHttpMaxRequestBodySizeFeature,
+ IHttpRequestBodyMinimumDataRateFeature
{
// NOTE: When feature interfaces are added to or removed from this Frame class implementation,
// then the list of `implementedFeatures` in the generated code project MUST also be updated.
@@ -227,6 +229,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
+ MinimumDataRate IHttpRequestBodyMinimumDataRateFeature.MinimumDataRate
+ {
+ get => RequestBodyMinimumDataRate;
+ set => RequestBodyMinimumDataRate = value;
+ }
+
object IFeatureCollection.this[Type key]
{
get => FastFeatureGet(key);
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs
index 3a17316ade..ef0aa0faa6 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs
@@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly Type IHttpWebSocketFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature);
private static readonly Type ISessionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ISessionFeature);
private static readonly Type IHttpMaxRequestBodySizeFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
+ private static readonly Type IHttpRequestBodyMinimumDataRateFeatureType = typeof(global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpRequestBodyMinimumDataRateFeature);
private static readonly Type IHttpSendFileFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature);
private object _currentIHttpRequestFeature;
@@ -42,6 +43,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private object _currentIHttpWebSocketFeature;
private object _currentISessionFeature;
private object _currentIHttpMaxRequestBodySizeFeature;
+ private object _currentIHttpRequestBodyMinimumDataRateFeature;
private object _currentIHttpSendFileFeature;
private void FastReset()
@@ -53,6 +55,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpRequestLifetimeFeature = this;
_currentIHttpConnectionFeature = this;
_currentIHttpMaxRequestBodySizeFeature = this;
+ _currentIHttpRequestBodyMinimumDataRateFeature = this;
_currentIServiceProvidersFeature = null;
_currentIHttpAuthenticationFeature = null;
@@ -132,6 +135,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
return _currentIHttpMaxRequestBodySizeFeature;
}
+ if (key == IHttpRequestBodyMinimumDataRateFeatureType)
+ {
+ return _currentIHttpRequestBodyMinimumDataRateFeature;
+ }
if (key == IHttpSendFileFeatureType)
{
return _currentIHttpSendFileFeature;
@@ -223,6 +230,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpMaxRequestBodySizeFeature = feature;
return;
}
+ if (key == IHttpRequestBodyMinimumDataRateFeatureType)
+ {
+ _currentIHttpRequestBodyMinimumDataRateFeature = feature;
+ return;
+ }
if (key == IHttpSendFileFeatureType)
{
_currentIHttpSendFileFeature = feature;
@@ -297,6 +309,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
yield return new KeyValuePair(IHttpMaxRequestBodySizeFeatureType, _currentIHttpMaxRequestBodySizeFeature as global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
}
+ if (_currentIHttpRequestBodyMinimumDataRateFeature != null)
+ {
+ yield return new KeyValuePair(IHttpRequestBodyMinimumDataRateFeatureType, _currentIHttpRequestBodyMinimumDataRateFeature as global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpRequestBodyMinimumDataRateFeature);
+ }
if (_currentIHttpSendFileFeature != null)
{
yield return new KeyValuePair(IHttpSendFileFeatureType, _currentIHttpSendFileFeature as global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature);
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs
index 6919a10ccb..9ade0f5689 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs
@@ -21,6 +21,7 @@ using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
// ReSharper disable AccessToModifiedClosure
@@ -300,6 +301,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected FrameResponseHeaders FrameResponseHeaders { get; } = new FrameResponseHeaders();
+ public MinimumDataRate RequestBodyMinimumDataRate { get; set; }
+
public void InitializeStreams(MessageBody messageBody)
{
if (_frameStreams == null)
@@ -376,6 +379,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_responseBytesWritten = 0;
_requestCount++;
+
+ RequestBodyMinimumDataRate = ServerOptions.Limits.RequestBodyMinimumDataRate;
}
///
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameOfT.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameOfT.cs
index c660d0b60c..8eddab20db 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameOfT.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameOfT.cs
@@ -160,7 +160,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
if (_keepAlive)
{
// Finish reading the request body in case the app did not.
+ TimeoutControl.SetTimeout(Constants.RequestBodyDrainTimeout.Ticks, TimeoutAction.SendTimeoutResponse);
await messageBody.ConsumeAsync();
+ TimeoutControl.CancelTimeout();
+
// At this point both the request body pipe reader and writer should be completed.
RequestBodyPipe.Reset();
}
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/MessageBody.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/MessageBody.cs
index bc6e3c0f8a..f964f55001 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/MessageBody.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/MessageBody.cs
@@ -6,6 +6,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
@@ -16,6 +17,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly MessageBody _zeroContentLengthKeepAlive = new ForZeroContentLength(keepAlive: true);
private readonly Frame _context;
+
private bool _send100Continue = true;
private volatile bool _canceled;
@@ -32,22 +34,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public virtual bool IsEmpty => false;
+ private IKestrelTrace Log => _context.ServiceContext.Log;
+
private async Task PumpAsync()
{
Exception error = null;
try
{
+ var awaitable = _context.Input.ReadAsync();
+
+ if (!awaitable.IsCompleted)
+ {
+ TryProduceContinue();
+ }
+
+ TryStartTimingReads();
+
while (true)
{
- var awaitable = _context.Input.ReadAsync();
+ var result = await awaitable;
- if (!awaitable.IsCompleted)
+ if (_context.TimeoutControl.TimedOut)
{
- TryProduceContinue();
+ _context.ThrowRequestRejected(RequestRejectionReason.RequestTimeout);
}
- var result = await awaitable;
var readableBuffer = result.Buffer;
var consumed = readableBuffer.Start;
var examined = readableBuffer.End;
@@ -73,7 +85,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
writableBuffer.Commit();
}
- await writableBuffer.FlushAsync();
+ var writeAwaitable = writableBuffer.FlushAsync();
+ var backpressure = false;
+
+ if (!writeAwaitable.IsCompleted)
+ {
+ // Backpressure, stop controlling incoming data rate until data is read.
+ backpressure = true;
+ _context.TimeoutControl.PauseTimingReads();
+ }
+
+ await writeAwaitable;
+
+ if (backpressure)
+ {
+ _context.TimeoutControl.ResumeTimingReads();
+ }
if (done)
{
@@ -84,6 +111,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_context.ThrowRequestRejected(RequestRejectionReason.UnexpectedEndOfRequestContent);
}
+
+ awaitable = _context.Input.ReadAsync();
}
finally
{
@@ -98,6 +127,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
finally
{
_context.RequestBodyPipe.Writer.Complete(error);
+ TryStopTimingReads();
}
}
@@ -191,6 +221,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected void Copy(ReadableBuffer readableBuffer, WritableBuffer writableBuffer)
{
+ _context.TimeoutControl.BytesRead(readableBuffer.Length);
+
if (readableBuffer.IsSingleSpan)
{
writableBuffer.Write(readableBuffer.First.Span);
@@ -232,6 +264,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
}
+ private void TryStartTimingReads()
+ {
+ if (!RequestUpgrade)
+ {
+ Log.RequestBodyStart(_context.ConnectionIdFeature, _context.TraceIdentifier);
+ _context.TimeoutControl.StartTimingReads();
+ }
+ }
+
+ private void TryStopTimingReads()
+ {
+ if (!RequestUpgrade)
+ {
+ Log.RequestBodyDone(_context.ConnectionIdFeature, _context.TraceIdentifier);
+ _context.TimeoutControl.StopTimingReads();
+ }
+ }
+
public static MessageBody For(
HttpVersion httpVersion,
FrameRequestHeaders headers,
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/Constants.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/Constants.cs
index 8b22cd7cc4..7f93242028 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/Constants.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/Constants.cs
@@ -1,6 +1,8 @@
// 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.Internal.Infrastructure
{
internal static class Constants
@@ -28,5 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
public const string SocketDescriptorPrefix = "sockfd:";
public const string ServerName = "Kestrel";
+
+ public static readonly TimeSpan RequestBodyDrainTimeout = TimeSpan.FromSeconds(5);
}
}
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
index 91012fdbf5..b7d68eb03a 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
@@ -37,5 +37,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
void HeartbeatSlow(TimeSpan interval, DateTimeOffset now);
void ApplicationNeverCompleted(string connectionId);
+
+ void RequestBodyStart(string connectionId, string traceIdentifier);
+
+ void RequestBodyDone(string connectionId, string traceIdentifier);
+
+ void RequestBodyMininumDataRateNotSatisfied(string connectionId, string traceIdentifier, double rate);
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/ITimeoutControl.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/ITimeoutControl.cs
index e031495f1d..6b23f37c94 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/ITimeoutControl.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/ITimeoutControl.cs
@@ -5,8 +5,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
public interface ITimeoutControl
{
+ bool TimedOut { get; }
+
void SetTimeout(long ticks, TimeoutAction timeoutAction);
void ResetTimeout(long ticks, TimeoutAction timeoutAction);
void CancelTimeout();
+
+ void StartTimingReads();
+ void PauseTimingReads();
+ void ResumeTimingReads();
+ void StopTimingReads();
+ void BytesRead(int count);
}
}
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
index e04ca4e0b8..115d44184d 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
@@ -7,9 +7,6 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
- ///
- /// Summary description for KestrelTrace
- ///
public class KestrelTrace : IKestrelTrace
{
private static readonly Action _connectionStart =
@@ -57,6 +54,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private static readonly Action _connectionRejected =
LoggerMessage.Define(LogLevel.Warning, 24, @"Connection id ""{ConnectionId}"" rejected because the maximum number of concurrent connections has been reached.");
+ private static readonly Action _requestBodyStart =
+ LoggerMessage.Define(LogLevel.Debug, 25, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": started reading request body.");
+
+ private static readonly Action _requestBodyDone =
+ LoggerMessage.Define(LogLevel.Debug, 26, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": done reading request body.");
+
+ private static readonly Action _requestBodyMinimumDataRateNotSatisfied =
+ LoggerMessage.Define(LogLevel.Information, 27, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": request body incoming data rate dropped below {Rate} bytes/second.");
+
protected readonly ILogger _logger;
public KestrelTrace(ILogger logger)
@@ -139,6 +145,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_applicationNeverCompleted(_logger, connectionId, null);
}
+ public virtual void RequestBodyStart(string connectionId, string traceIdentifier)
+ {
+ _requestBodyStart(_logger, connectionId, traceIdentifier, null);
+ }
+
+ public virtual void RequestBodyDone(string connectionId, string traceIdentifier)
+ {
+ _requestBodyDone(_logger, connectionId, traceIdentifier, null);
+ }
+
+ public void RequestBodyMininumDataRateNotSatisfied(string connectionId, string traceIdentifier, double rate)
+ {
+ _requestBodyMinimumDataRateNotSatisfied(_logger, connectionId, traceIdentifier, rate, null);
+ }
+
public virtual void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
=> _logger.Log(logLevel, eventId, state, exception, formatter);
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs
index 3e02eaad9f..a1f79fc799 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs
@@ -2,7 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Threading;
using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
namespace Microsoft.AspNetCore.Server.Kestrel.Core
{
@@ -11,8 +13,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
// Matches the non-configurable default response buffer size for Kestrel in 1.0.0
private long? _maxResponseBufferSize = 64 * 1024;
- // Matches the default client_max_body_size in nginx. Also large enough that most requests
- // should be under the limit.
+ // Matches the default client_max_body_size in nginx.
+ // Also large enough that most requests should be under the limit.
private long? _maxRequestBufferSize = 1024 * 1024;
// Matches the default large_client_header_buffers in nginx.
@@ -33,7 +35,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
private TimeSpan _requestHeadersTimeout = TimeSpan.FromSeconds(30);
- // default to unlimited
+ // Unlimited connections are allowed by default.
private long? _maxConcurrentConnections = null;
private long? _maxConcurrentUpgradedConnections = null;
@@ -169,7 +171,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
public TimeSpan KeepAliveTimeout
{
get => _keepAliveTimeout;
- set => _keepAliveTimeout = value;
+ set
+ {
+ if (value <= TimeSpan.Zero && value != Timeout.InfiniteTimeSpan)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveTimeSpanRequired);
+ }
+ _keepAliveTimeout = value != Timeout.InfiniteTimeSpan ? value : TimeSpan.MaxValue;
+ }
}
///
@@ -181,7 +190,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
public TimeSpan RequestHeadersTimeout
{
get => _requestHeadersTimeout;
- set => _requestHeadersTimeout = value;
+ set
+ {
+ if (value <= TimeSpan.Zero && value != Timeout.InfiniteTimeSpan)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveTimeSpanRequired);
+ }
+ _requestHeadersTimeout = value != Timeout.InfiniteTimeSpan ? value : TimeSpan.MaxValue;
+ }
}
///
@@ -234,5 +250,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
_maxConcurrentUpgradedConnections = value;
}
}
+
+ ///
+ /// 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.
+ /// This limit has no effect on upgraded connections which are always unlimited.
+ /// This can be overridden per-request via .
+ ///
+ ///
+ /// Defaults to null.
+ ///
+ public MinimumDataRate RequestBodyMinimumDataRate { get; set; } = new MinimumDataRate(rate: 1, gracePeriod: TimeSpan.FromSeconds(5));
}
}
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs
index 877d6d308b..ce955e41d0 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs
@@ -977,7 +977,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
=> GetString("MaxRequestBodySizeCannotBeModifiedAfterRead");
///
- /// The maximum request body size cannot be modified after the request has be upgraded.
+ /// The maximum request body size cannot be modified after the request has been upgraded.
///
internal static string MaxRequestBodySizeCannotBeModifiedForUpgradedRequests
{
@@ -985,11 +985,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
}
///
- /// The maximum request body size cannot be modified after the request has be upgraded.
+ /// The maximum request body size cannot be modified after the request has been upgraded.
///
internal static string FormatMaxRequestBodySizeCannotBeModifiedForUpgradedRequests()
=> GetString("MaxRequestBodySizeCannotBeModifiedForUpgradedRequests");
+ ///
+ /// Value must be a positive TimeSpan.
+ ///
+ internal static string PositiveTimeSpanRequired
+ {
+ get => GetString("PositiveTimeSpanRequired");
+ }
+
+ ///
+ /// Value must be a positive TimeSpan.
+ ///
+ internal static string FormatPositiveTimeSpanRequired()
+ => GetString("PositiveTimeSpanRequired");
+
+ ///
+ /// Value must be a non-negative TimeSpan.
+ ///
+ internal static string NonNegativeTimeSpanRequired
+ {
+ get => GetString("NonNegativeTimeSpanRequired");
+ }
+
+ ///
+ /// Value must be a non-negative TimeSpan.
+ ///
+ internal static string FormatNonNegativeTimeSpanRequired()
+ => GetString("NonNegativeTimeSpanRequired");
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameConnectionTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameConnectionTests.cs
new file mode 100644
index 0000000000..9596ef625f
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameConnectionTests.cs
@@ -0,0 +1,324 @@
+// 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.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
+using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
+using Microsoft.AspNetCore.Testing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
+{
+ public class FrameConnectionTests : IDisposable
+ {
+ private readonly PipeFactory _pipeFactory;
+ private readonly FrameConnectionContext _frameConnectionContext;
+ private readonly FrameConnection _frameConnection;
+
+ public FrameConnectionTests()
+ {
+ _pipeFactory = new PipeFactory();
+
+ _frameConnectionContext = new FrameConnectionContext
+ {
+ ConnectionId = "0123456789",
+ ConnectionAdapters = new List(),
+ ConnectionInformation = new MockConnectionInformation
+ {
+ PipeFactory = _pipeFactory
+ },
+ FrameConnectionId = long.MinValue,
+ Input = _pipeFactory.Create(),
+ Output = _pipeFactory.Create(),
+ ServiceContext = new TestServiceContext
+ {
+ SystemClock = new SystemClock()
+ }
+ };
+
+ _frameConnection = new FrameConnection(_frameConnectionContext);
+ }
+
+ public void Dispose()
+ {
+ _pipeFactory.Dispose();
+ }
+
+ [Fact]
+ public void TimesOutWhenRequestBodyDoesNotSatisfyMinimumDataRate()
+ {
+ var requestBodyMinimumDataRate = 100;
+ var requestBodyGracePeriod = TimeSpan.FromSeconds(5);
+
+ _frameConnectionContext.ServiceContext.ServerOptions.Limits.RequestBodyMinimumDataRate =
+ new MinimumDataRate(rate: requestBodyMinimumDataRate, gracePeriod: requestBodyGracePeriod);
+
+ var mockLogger = new Mock();
+ _frameConnectionContext.ServiceContext.Log = mockLogger.Object;
+
+ _frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
+ _frameConnection.Frame.Reset();
+
+ // Initialize timestamp
+ var now = DateTimeOffset.UtcNow;
+ _frameConnection.Tick(now);
+
+ _frameConnection.StartTimingReads();
+
+ // Tick after grace period w/ low data rate
+ now += requestBodyGracePeriod + TimeSpan.FromSeconds(1);
+ _frameConnection.BytesRead(1);
+ _frameConnection.Tick(now);
+
+ // Timed out
+ Assert.True(_frameConnection.TimedOut);
+ mockLogger.Verify(logger =>
+ logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), requestBodyMinimumDataRate), Times.Once);
+ }
+
+ [Fact]
+ public void MinimumDataRateNotEnforcedDuringGracePeriod()
+ {
+ var requestBodyMinimumDataRate = 100;
+ var requestBodyGracePeriod = TimeSpan.FromSeconds(2);
+
+ _frameConnectionContext.ServiceContext.ServerOptions.Limits.RequestBodyMinimumDataRate =
+ new MinimumDataRate(rate: requestBodyMinimumDataRate, gracePeriod: requestBodyGracePeriod);
+
+ var mockLogger = new Mock();
+ _frameConnectionContext.ServiceContext.Log = mockLogger.Object;
+
+ _frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
+ _frameConnection.Frame.Reset();
+
+ // Initialize timestamp
+ var now = DateTimeOffset.UtcNow;
+ _frameConnection.Tick(now);
+
+ _frameConnection.StartTimingReads();
+
+ // Tick during grace period w/ low data rate
+ now += TimeSpan.FromSeconds(1);
+ _frameConnection.BytesRead(10);
+ _frameConnection.Tick(now);
+
+ // Not timed out
+ Assert.False(_frameConnection.TimedOut);
+ mockLogger.Verify(logger =>
+ logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), requestBodyMinimumDataRate), Times.Never);
+
+ // Tick after grace period w/ low data rate
+ now += TimeSpan.FromSeconds(2);
+ _frameConnection.BytesRead(10);
+ _frameConnection.Tick(now);
+
+ // Timed out
+ Assert.True(_frameConnection.TimedOut);
+ mockLogger.Verify(logger =>
+ logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), requestBodyMinimumDataRate), Times.Once);
+ }
+
+ [Fact]
+ public void DataRateIsAveragedOverTimeSpentReadingRequestBody()
+ {
+ var requestBodyMinimumDataRate = 100;
+ var requestBodyGracePeriod = TimeSpan.FromSeconds(1);
+
+ _frameConnectionContext.ServiceContext.ServerOptions.Limits.RequestBodyMinimumDataRate =
+ new MinimumDataRate(rate: requestBodyMinimumDataRate, gracePeriod: requestBodyGracePeriod);
+
+ var mockLogger = new Mock();
+ _frameConnectionContext.ServiceContext.Log = mockLogger.Object;
+
+ _frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
+ _frameConnection.Frame.Reset();
+
+ // Initialize timestamp
+ var now = DateTimeOffset.UtcNow;
+ _frameConnection.Tick(now);
+
+ _frameConnection.StartTimingReads();
+
+ // Tick after grace period to start enforcing minimum data rate
+ now += requestBodyGracePeriod;
+ _frameConnection.BytesRead(100);
+ _frameConnection.Tick(now);
+
+ // Data rate: 200 bytes/second
+ now += TimeSpan.FromSeconds(1);
+ _frameConnection.BytesRead(300);
+ _frameConnection.Tick(now);
+
+ // Not timed out
+ Assert.False(_frameConnection.TimedOut);
+ mockLogger.Verify(logger =>
+ logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), requestBodyMinimumDataRate), Times.Never);
+
+ // Data rate: 150 bytes/second
+ now += TimeSpan.FromSeconds(1);
+ _frameConnection.BytesRead(50);
+ _frameConnection.Tick(now);
+
+ // Not timed out
+ Assert.False(_frameConnection.TimedOut);
+ mockLogger.Verify(logger =>
+ logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), requestBodyMinimumDataRate), Times.Never);
+
+ // Data rate: 115 bytes/second
+ now += TimeSpan.FromSeconds(1);
+ _frameConnection.BytesRead(10);
+ _frameConnection.Tick(now);
+
+ // Not timed out
+ Assert.False(_frameConnection.TimedOut);
+ mockLogger.Verify(logger =>
+ logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), requestBodyMinimumDataRate), Times.Never);
+
+ // Data rate: 50 bytes/second
+ now += TimeSpan.FromSeconds(6);
+ _frameConnection.BytesRead(40);
+ _frameConnection.Tick(now);
+
+ // Timed out
+ Assert.True(_frameConnection.TimedOut);
+ mockLogger.Verify(logger =>
+ logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), requestBodyMinimumDataRate), Times.Once);
+ }
+
+ [Fact]
+ public void PausedTimeDoesNotCountAgainstRequestBodyTimeout()
+ {
+ var requestBodyTimeout = TimeSpan.FromSeconds(5);
+ var systemClock = new MockSystemClock();
+
+ _frameConnectionContext.ServiceContext.ServerOptions.Limits.RequestBodyMinimumDataRate =
+ new MinimumDataRate(rate: 100, gracePeriod: TimeSpan.Zero);
+ _frameConnectionContext.ServiceContext.SystemClock = systemClock;
+
+ var mockLogger = new Mock();
+ _frameConnectionContext.ServiceContext.Log = mockLogger.Object;
+
+ _frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
+ _frameConnection.Frame.Reset();
+
+ // Initialize timestamp
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ _frameConnection.StartTimingReads();
+
+ // Tick at 1s, expected counted time is 1s, expected data rate is 400 bytes/second
+ systemClock.UtcNow += TimeSpan.FromSeconds(1);
+ _frameConnection.BytesRead(400);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Pause at 1.5s
+ systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
+ _frameConnection.PauseTimingReads();
+
+ // Tick at 2s, expected counted time is 2s, expected data rate is 400 bytes/second
+ systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Tick at 6s, expected counted time is 2s, expected data rate is 400 bytes/second
+ systemClock.UtcNow += TimeSpan.FromSeconds(4);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Not timed out
+ Assert.False(_frameConnection.TimedOut);
+ mockLogger.Verify(
+ logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny()),
+ Times.Never);
+
+ // Resume at 6.5s
+ systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
+ _frameConnection.ResumeTimingReads();
+
+ // Tick at 8s, expected counted time is 4s, expected data rate is 100 bytes/second
+ systemClock.UtcNow += TimeSpan.FromSeconds(1.5);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Not timed out
+ Assert.False(_frameConnection.TimedOut);
+ mockLogger.Verify(
+ logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny()),
+ Times.Never);
+
+ // Tick at 9s, expected counted time is 9s, expected data rate drops below 100 bytes/second
+ systemClock.UtcNow += TimeSpan.FromSeconds(1);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Timed out
+ Assert.True(_frameConnection.TimedOut);
+ mockLogger.Verify(
+ logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny()),
+ Times.Once);
+ }
+
+ [Fact]
+ public void NotPausedWhenResumeCalledBeforeNextTick()
+ {
+ var systemClock = new MockSystemClock();
+
+ _frameConnectionContext.ServiceContext.ServerOptions.Limits.RequestBodyMinimumDataRate =
+ new MinimumDataRate(rate: 100, gracePeriod: TimeSpan.Zero);
+ _frameConnectionContext.ServiceContext.SystemClock = systemClock;
+
+ var mockLogger = new Mock();
+ _frameConnectionContext.ServiceContext.Log = mockLogger.Object;
+
+ _frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
+ _frameConnection.Frame.Reset();
+
+ // Initialize timestamp
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ _frameConnection.StartTimingReads();
+
+ // Tick at 1s, expected counted time is 1s, expected data rate is 100 bytes/second
+ systemClock.UtcNow += TimeSpan.FromSeconds(1);
+ _frameConnection.BytesRead(100);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Not timed out
+ Assert.False(_frameConnection.TimedOut);
+ mockLogger.Verify(
+ logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny()),
+ Times.Never);
+
+ // Pause at 1.25s
+ systemClock.UtcNow += TimeSpan.FromSeconds(0.25);
+ _frameConnection.PauseTimingReads();
+
+ // Resume at 1.5s
+ systemClock.UtcNow += TimeSpan.FromSeconds(0.25);
+ _frameConnection.ResumeTimingReads();
+
+ // Tick at 2s, expected counted time is 2s, expected data rate is 100 bytes/second
+ systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
+ _frameConnection.BytesRead(100);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Not timed out
+ Assert.False(_frameConnection.TimedOut);
+ mockLogger.Verify(
+ logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny()),
+ Times.Never);
+
+ // Tick at 3s, expected counted time is 3s, expected data rate drops below 100 bytes/second
+ systemClock.UtcNow += TimeSpan.FromSeconds(1);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Timed out
+ Assert.True(_frameConnection.TimedOut);
+ mockLogger.Verify(
+ logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny()),
+ Times.Once);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs
index e8d2481942..73fd432a15 100644
--- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs
+++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs
@@ -12,6 +12,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
@@ -140,6 +141,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.NotEqual(nextId, secondId);
}
+ [Fact]
+ public void ResetResetsRequestBodyMinimumDataRate()
+ {
+ _frame.RequestBodyMinimumDataRate = new MinimumDataRate(rate: 1, gracePeriod: TimeSpan.Zero);
+
+ _frame.Reset();
+
+ Assert.Equal(_serviceContext.ServerOptions.Limits.RequestBodyMinimumDataRate, _frame.RequestBodyMinimumDataRate);
+ }
+
[Fact]
public void TraceIdentifierCountsRequestsPerFrame()
{
@@ -243,6 +254,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Throws(() => ((IHttpResponseFeature)_frame).OnStarting(_ => TaskCache.CompletedTask, null));
}
+ [Theory]
+ [MemberData(nameof(RequestBodyMinimumDataRateData))]
+ public void ConfiguringRequestBodyMinimumDataRateFeatureSetsRequestBodyMinimumDateRate(MinimumDataRate minimumDataRate)
+ {
+ ((IFeatureCollection)_frame).Get().MinimumDataRate = minimumDataRate;
+
+ Assert.Same(minimumDataRate, _frame.RequestBodyMinimumDataRate);
+ }
+
[Fact]
public void ResetResetsRequestHeaders()
{
@@ -844,6 +864,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
}
+ public static TheoryData RequestBodyTimeoutDataValid => new TheoryData
+ {
+ TimeSpan.FromTicks(1),
+ TimeSpan.MaxValue,
+ Timeout.InfiniteTimeSpan,
+ TimeSpan.FromMilliseconds(-1) // Same as Timeout.InfiniteTimeSpan
+ };
+
+ public static TheoryData RequestBodyTimeoutDataInvalid => new TheoryData
+ {
+ TimeSpan.MinValue,
+ TimeSpan.FromTicks(-1),
+ TimeSpan.Zero
+ };
+
+ public static TheoryData RequestBodyMinimumDataRateData => new TheoryData
+ {
+ null,
+ new MinimumDataRate(rate: 1, gracePeriod: TimeSpan.Zero)
+ };
+
private class RequestHeadersWrapper : IHeaderDictionary
{
IHeaderDictionary _innerHeaders;
diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs
index 863dfd7ff4..914ceeff11 100644
--- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs
+++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs
@@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
-using Microsoft.AspNetCore.Server.Kestrel.Core;
+using System.Threading;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
@@ -154,16 +154,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
[Theory]
- [InlineData(0)]
- [InlineData(0.5)]
- [InlineData(2.1)]
- [InlineData(2.5)]
- [InlineData(2.9)]
- public void KeepAliveTimeoutValid(double seconds)
+ [MemberData(nameof(TimeoutValidData))]
+ public void KeepAliveTimeoutValid(TimeSpan value)
{
- var o = new KestrelServerLimits();
- o.KeepAliveTimeout = TimeSpan.FromSeconds(seconds);
- Assert.Equal(seconds, o.KeepAliveTimeout.TotalSeconds);
+ Assert.Equal(value, new KestrelServerLimits { KeepAliveTimeout = value }.KeepAliveTimeout);
+ }
+
+ [Fact]
+ public void KeepAliveTimeoutCanBeSetToInfinite()
+ {
+ Assert.Equal(TimeSpan.MaxValue, new KestrelServerLimits { KeepAliveTimeout = Timeout.InfiniteTimeSpan }.KeepAliveTimeout);
+ }
+
+ [Theory]
+ [MemberData(nameof(TimeoutInvalidData))]
+ public void KeepAliveTimeoutInvalid(TimeSpan value)
+ {
+ var exception = Assert.Throws(() => new KestrelServerLimits { KeepAliveTimeout = value });
+
+ Assert.Equal("value", exception.ParamName);
+ Assert.StartsWith(CoreStrings.PositiveTimeSpanRequired, exception.Message);
}
[Fact]
@@ -173,17 +183,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
[Theory]
- [InlineData(0)]
- [InlineData(0.5)]
- [InlineData(1.0)]
- [InlineData(2.5)]
- [InlineData(10)]
- [InlineData(60)]
- public void RequestHeadersTimeoutValid(double seconds)
+ [MemberData(nameof(TimeoutValidData))]
+ public void RequestHeadersTimeoutValid(TimeSpan value)
{
- var o = new KestrelServerLimits();
- o.RequestHeadersTimeout = TimeSpan.FromSeconds(seconds);
- Assert.Equal(seconds, o.RequestHeadersTimeout.TotalSeconds);
+ Assert.Equal(value, new KestrelServerLimits { RequestHeadersTimeout = value }.RequestHeadersTimeout);
+ }
+
+ [Fact]
+ public void RequestHeadersTimeoutCanBeSetToInfinite()
+ {
+ Assert.Equal(TimeSpan.MaxValue, new KestrelServerLimits { RequestHeadersTimeout = Timeout.InfiniteTimeSpan }.RequestHeadersTimeout);
+ }
+
+ [Theory]
+ [MemberData(nameof(TimeoutInvalidData))]
+ public void RequestHeadersTimeoutInvalid(TimeSpan value)
+ {
+ var exception = Assert.Throws(() => new KestrelServerLimits { RequestHeadersTimeout = value });
+
+ Assert.Equal("value", exception.ParamName);
+ Assert.StartsWith(CoreStrings.PositiveTimeSpanRequired, exception.Message);
}
[Fact]
@@ -272,5 +291,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var ex = Assert.Throws(() => new KestrelServerLimits().MaxRequestBodySize = value);
Assert.StartsWith(CoreStrings.NonNegativeNumberOrNullRequired, ex.Message);
}
+
+ [Fact]
+ public void RequestBodyMinimumDataRateDefault()
+ {
+ Assert.NotNull(new KestrelServerLimits().RequestBodyMinimumDataRate);
+ Assert.Equal(1, new KestrelServerLimits().RequestBodyMinimumDataRate.Rate);
+ Assert.Equal(TimeSpan.FromSeconds(5), new KestrelServerLimits().RequestBodyMinimumDataRate.GracePeriod);
+ }
+
+ public static TheoryData TimeoutValidData => new TheoryData
+ {
+ TimeSpan.FromTicks(1),
+ TimeSpan.MaxValue,
+ };
+
+ public static TheoryData TimeoutInfiniteData => new TheoryData
+ {
+ Timeout.InfiniteTimeSpan,
+ };
+
+ public static TheoryData TimeoutInvalidData => new TheoryData
+ {
+ TimeSpan.MinValue,
+ TimeSpan.FromTicks(-1),
+ TimeSpan.Zero
+ };
}
}
diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerTests.cs
index a61feba7bc..843888df40 100644
--- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerTests.cs
+++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerTests.cs
@@ -8,6 +8,7 @@ using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
@@ -100,9 +101,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public void StartWithMaxRequestBufferSizeLessThanMaxRequestLineSizeThrows(long maxRequestBufferSize, int maxRequestLineSize)
{
var testLogger = new TestApplicationErrorLogger { ThrowOnCriticalErrors = false };
- var options = new KestrelServerOptions();
- options.Limits.MaxRequestBufferSize = maxRequestBufferSize;
- options.Limits.MaxRequestLineSize = maxRequestLineSize;
+ var options = new KestrelServerOptions
+ {
+ Limits =
+ {
+ MaxRequestBufferSize = maxRequestBufferSize,
+ MaxRequestLineSize = maxRequestLineSize
+ }
+ };
using (var server = CreateServer(options, testLogger))
{
@@ -121,10 +127,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public void StartWithMaxRequestBufferSizeLessThanMaxRequestHeadersTotalSizeThrows(long maxRequestBufferSize, int maxRequestHeadersTotalSize)
{
var testLogger = new TestApplicationErrorLogger { ThrowOnCriticalErrors = false };
- var options = new KestrelServerOptions();
- options.Limits.MaxRequestBufferSize = maxRequestBufferSize;
- options.Limits.MaxRequestLineSize = (int)maxRequestBufferSize;
- options.Limits.MaxRequestHeadersTotalSize = maxRequestHeadersTotalSize;
+ var options = new KestrelServerOptions
+ {
+ Limits =
+ {
+ MaxRequestBufferSize = maxRequestBufferSize,
+ MaxRequestLineSize = (int)maxRequestBufferSize,
+ MaxRequestHeadersTotalSize = maxRequestHeadersTotalSize
+ }
+ };
using (var server = CreateServer(options, testLogger))
{
@@ -146,7 +157,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
[Fact]
- public void StartWithNoTrasnportFactoryThrows()
+ public void StartWithNoTransportFactoryThrows()
{
var exception = Assert.Throws(() =>
new KestrelServer(Options.Create(null), null, Mock.Of()));
diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MessageBodyTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MessageBodyTests.cs
index 1bb42355f6..b884baa874 100644
--- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MessageBodyTests.cs
+++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MessageBodyTests.cs
@@ -9,14 +9,12 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
-using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
-using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
@@ -30,7 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.FrameContext);
+ var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@@ -54,7 +52,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.FrameContext);
+ var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@@ -76,7 +74,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.FrameContext);
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@@ -100,7 +98,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.FrameContext);
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@@ -124,7 +122,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.FrameContext);
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@@ -147,7 +145,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.FrameContext);
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@@ -166,7 +164,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.FrameContext);
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@@ -187,7 +185,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.FrameContext);
+ var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@@ -210,7 +208,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.FrameContext);
+ var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@@ -233,7 +231,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.FrameContext);
+ var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@@ -251,7 +249,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.FrameContext);
+ var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@@ -267,7 +265,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "8197" }, input.FrameContext);
+ var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "8197" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@@ -294,7 +292,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput())
{
var ex = Assert.Throws(() =>
- MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked, not-chunked" }, input.FrameContext));
+ MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked, not-chunked" }, input.Frame));
Assert.Equal(StatusCodes.Status400BadRequest, ex.StatusCode);
Assert.Equal(CoreStrings.FormatBadRequest_FinalTransferCodingNotChunked("chunked, not-chunked"), ex.Message);
@@ -308,9 +306,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- input.FrameContext.Method = method;
+ input.Frame.Method = method;
var ex = Assert.Throws(() =>
- MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders(), input.FrameContext));
+ MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders(), input.Frame));
Assert.Equal(StatusCodes.Status411LengthRequired, ex.StatusCode);
Assert.Equal(CoreStrings.FormatBadRequest_LengthRequired(method), ex.Message);
@@ -324,9 +322,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- input.FrameContext.Method = method;
+ input.Frame.Method = method;
var ex = Assert.Throws(() =>
- MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders(), input.FrameContext));
+ MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders(), input.Frame));
Assert.Equal(StatusCodes.Status400BadRequest, ex.StatusCode);
Assert.Equal(CoreStrings.FormatBadRequest_LengthRequiredHttp10(method), ex.Message);
@@ -338,7 +336,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "5" }, input.FrameContext);
+ var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
+
input.Add("Hello");
using (var ms = new MemoryStream())
@@ -355,7 +354,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "5" }, input.FrameContext);
+ var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
+
input.Add("Hello");
await body.ConsumeAsync();
@@ -402,7 +402,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput())
{
- var body = MessageBody.For(HttpVersion.Http11, headers, input.FrameContext);
+ var body = MessageBody.For(HttpVersion.Http11, headers, input.Frame);
+
var copyToAsyncTask = body.CopyToAsync(mockDestination.Object);
// The block returned by IncomingStart always has at least 2048 available bytes,
@@ -448,7 +449,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
- var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderConnection = headerConnection }, input.FrameContext);
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderConnection = headerConnection }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@@ -463,15 +464,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
[Fact]
- public async Task StartAsyncDoesNotReturnAfterCancelingInput()
+ public async Task PumpAsyncDoesNotReturnAfterCancelingInput()
{
using (var input = new TestInput())
{
- var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.FrameContext);
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
- // Add some input and consume it to ensure StartAsync is in the loop
+ // Add some input and consume it to ensure PumpAsync is running
input.Add("a");
Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1));
@@ -485,15 +486,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
[Fact]
- public async Task StartAsyncReturnsAfterCanceling()
+ public async Task PumpAsyncReturnsAfterCanceling()
{
using (var input = new TestInput())
{
- var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.FrameContext);
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
- // Add some input and consume it to ensure StartAsync is in the loop
+ // Add some input and consume it to ensure PumpAsync is running
input.Add("a");
Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1));
@@ -511,6 +512,198 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
}
+ [Fact]
+ public async Task ReadAsyncThrowsOnTimeout()
+ {
+ using (var input = new TestInput())
+ {
+ var mockTimeoutControl = new Mock();
+
+ input.FrameContext.TimeoutControl = mockTimeoutControl.Object;
+
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
+
+ // Add some input and read it to start PumpAsync
+ input.Add("a");
+ Assert.Equal(1, await body.ReadAsync(new ArraySegment(new byte[1])));
+
+ // Time out on the next read
+ mockTimeoutControl
+ .Setup(timeoutControl => timeoutControl.TimedOut)
+ .Returns(true);
+
+ input.Cancel();
+
+ var exception = await Assert.ThrowsAsync(() => body.ReadAsync(new ArraySegment(new byte[1])));
+ Assert.Equal(StatusCodes.Status408RequestTimeout, exception.StatusCode);
+ }
+ }
+
+ [Fact]
+ public async Task ConsumeAsyncThrowsOnTimeout()
+ {
+ using (var input = new TestInput())
+ {
+ var mockTimeoutControl = new Mock();
+
+ input.FrameContext.TimeoutControl = mockTimeoutControl.Object;
+
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
+
+ // Add some input and read it to start PumpAsync
+ input.Add("a");
+ Assert.Equal(1, await body.ReadAsync(new ArraySegment(new byte[1])));
+
+ // Time out on the next read
+ mockTimeoutControl
+ .Setup(timeoutControl => timeoutControl.TimedOut)
+ .Returns(true);
+
+ input.Cancel();
+
+ var exception = await Assert.ThrowsAsync(() => body.ConsumeAsync());
+ Assert.Equal(StatusCodes.Status408RequestTimeout, exception.StatusCode);
+ }
+ }
+
+ [Fact]
+ public async Task CopyToAsyncThrowsOnTimeout()
+ {
+ using (var input = new TestInput())
+ {
+ var mockTimeoutControl = new Mock();
+
+ input.FrameContext.TimeoutControl = mockTimeoutControl.Object;
+
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
+
+ // Add some input and read it to start PumpAsync
+ input.Add("a");
+ Assert.Equal(1, await body.ReadAsync(new ArraySegment(new byte[1])));
+
+ // Time out on the next read
+ mockTimeoutControl
+ .Setup(timeoutControl => timeoutControl.TimedOut)
+ .Returns(true);
+
+ input.Cancel();
+
+ using (var ms = new MemoryStream())
+ {
+ var exception = await Assert.ThrowsAsync(() => body.CopyToAsync(ms));
+ Assert.Equal(StatusCodes.Status408RequestTimeout, exception.StatusCode);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task LogsWhenStartsReadingRequestBody()
+ {
+ using (var input = new TestInput())
+ {
+ var mockLogger = new Mock();
+ input.Frame.ServiceContext.Log = mockLogger.Object;
+ input.Frame.ConnectionIdFeature = "ConnectionId";
+ input.Frame.TraceIdentifier = "RequestId";
+
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame);
+ var stream = new FrameRequestStream();
+ stream.StartAcceptingReads(body);
+
+ // Add some input and consume it to ensure PumpAsync is running
+ input.Add("a");
+ Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1));
+
+ mockLogger.Verify(logger => logger.RequestBodyStart("ConnectionId", "RequestId"));
+
+ input.Fin();
+ }
+ }
+
+ [Fact]
+ public async Task LogsWhenStopsReadingRequestBody()
+ {
+ using (var input = new TestInput())
+ {
+ var logEvent = new ManualResetEventSlim();
+ var mockLogger = new Mock();
+ mockLogger
+ .Setup(logger => logger.RequestBodyDone("ConnectionId", "RequestId"))
+ .Callback(() => logEvent.Set());
+ input.Frame.ServiceContext.Log = mockLogger.Object;
+ input.Frame.ConnectionIdFeature = "ConnectionId";
+ input.Frame.TraceIdentifier = "RequestId";
+
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame);
+ var stream = new FrameRequestStream();
+ stream.StartAcceptingReads(body);
+
+ // Add some input and consume it to ensure PumpAsync is running
+ input.Add("a");
+ Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1));
+
+ input.Fin();
+
+ Assert.True(logEvent.Wait(TimeSpan.FromSeconds(10)));
+ }
+ }
+
+ [Fact]
+ public async Task OnlyEnforcesRequestBodyTimeoutAfterSending100Continue()
+ {
+ using (var input = new TestInput())
+ {
+ var produceContinueCalled = false;
+ var startTimingReadsCalledAfterProduceContinue = false;
+
+ var mockFrameControl = new Mock();
+ mockFrameControl
+ .Setup(frameControl => frameControl.ProduceContinue())
+ .Callback(() => produceContinueCalled = true);
+ input.Frame.FrameControl = mockFrameControl.Object;
+
+ var mockTimeoutControl = new Mock();
+ mockTimeoutControl
+ .Setup(timeoutControl => timeoutControl.StartTimingReads())
+ .Callback(() => startTimingReadsCalledAfterProduceContinue = produceContinueCalled);
+
+ input.FrameContext.TimeoutControl = mockTimeoutControl.Object;
+
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
+
+ // Add some input and read it to start PumpAsync
+ var readTask = body.ReadAsync(new ArraySegment(new byte[1]));
+
+ Assert.True(startTimingReadsCalledAfterProduceContinue);
+
+ input.Add("a");
+ await readTask;
+ }
+ }
+
+ [Fact]
+ public async Task DoesNotEnforceRequestBodyTimeoutOnUpgradeRequests()
+ {
+ using (var input = new TestInput())
+ {
+ var mockTimeoutControl = new Mock();
+ input.FrameContext.TimeoutControl = mockTimeoutControl.Object;
+
+ var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.Frame);
+
+ // Add some input and read it to start PumpAsync
+ input.Add("a");
+ Assert.Equal(1, await body.ReadAsync(new ArraySegment(new byte[1])));
+
+ input.Fin();
+
+ await Assert.ThrowsAsync(async () => await body.ReadAsync(new ArraySegment(new byte[1])));
+
+ mockTimeoutControl.Verify(timeoutControl => timeoutControl.StartTimingReads(), Times.Never);
+ mockTimeoutControl.Verify(timeoutControl => timeoutControl.StopTimingReads(), Times.Never);
+ }
+ }
+
private void AssertASCII(string expected, ArraySegment actual)
{
var encoding = Encoding.ASCII;
diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MinimumDataRateTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MinimumDataRateTests.cs
new file mode 100644
index 0000000000..79660bf799
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MinimumDataRateTests.cs
@@ -0,0 +1,61 @@
+// 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.Server.Kestrel.Core.Features;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
+{
+ public class MinimumDataRateTests
+ {
+ [Theory]
+ [InlineData(double.Epsilon)]
+ [InlineData(double.MaxValue)]
+ public void RateValid(double value)
+ {
+ Assert.Equal(value, new MinimumDataRate(rate: value, gracePeriod: TimeSpan.Zero).Rate);
+ }
+
+ [Theory]
+ [InlineData(double.MinValue)]
+ [InlineData(0)]
+ public void RateInvalid(double value)
+ {
+ var exception = Assert.Throws(() => new MinimumDataRate(rate: value, gracePeriod: TimeSpan.Zero));
+
+ Assert.Equal("rate", exception.ParamName);
+ Assert.StartsWith(CoreStrings.PositiveNumberRequired, exception.Message);
+ }
+
+ [Theory]
+ [MemberData(nameof(GracePeriodValidData))]
+ public void GracePeriodValid(TimeSpan value)
+ {
+ Assert.Equal(value, new MinimumDataRate(rate: 1, gracePeriod: value).GracePeriod);
+ }
+
+ [Theory]
+ [MemberData(nameof(GracePeriodInvalidData))]
+ public void GracePeriodInvalid(TimeSpan value)
+ {
+ var exception = Assert.Throws(() => new MinimumDataRate(rate: 1, gracePeriod: value));
+
+ Assert.Equal("gracePeriod", exception.ParamName);
+ Assert.StartsWith(CoreStrings.NonNegativeTimeSpanRequired, exception.Message);
+ }
+
+ public static TheoryData GracePeriodValidData => new TheoryData
+ {
+ TimeSpan.Zero,
+ TimeSpan.FromTicks(1),
+ TimeSpan.MaxValue
+ };
+
+ public static TheoryData GracePeriodInvalidData => new TheoryData
+ {
+ TimeSpan.MinValue,
+ TimeSpan.FromTicks(-1)
+ };
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/TestInput.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/TestInput.cs
index df90641045..1a9cf1620f 100644
--- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/TestInput.cs
+++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/TestInput.cs
@@ -2,21 +2,16 @@
// 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.Text;
-using System.Threading;
-using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
-using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions;
using Microsoft.AspNetCore.Testing;
-using Microsoft.Extensions.Internal;
using Moq;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
- class TestInput : IFrameControl, IDisposable
+ class TestInput : IDisposable
{
private MemoryPool _memoryPool;
private PipeFactory _pipelineFactory;
@@ -27,23 +22,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_pipelineFactory = new PipeFactory();
Pipe = _pipelineFactory.Create();
- FrameContext = new Frame