Add request body minimum data rate feature (#1874).

This commit is contained in:
Cesar Blum Silveira 2017-06-08 14:36:03 -07:00 committed by GitHub
parent f96c48c08d
commit fcc04f8c3d
33 changed files with 1381 additions and 184 deletions

View File

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

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -327,4 +327,10 @@
<data name="MaxRequestBodySizeCannotBeModifiedForUpgradedRequests" xml:space="preserve">
<value>The maximum request body size cannot be modified after the request has been upgraded.</value>
</data>
</root>
<data name="PositiveTimeSpanRequired" xml:space="preserve">
<value>Value must be a positive TimeSpan.</value>
</data>
<data name="NonNegativeTimeSpanRequired" xml:space="preserve">
<value>Value must be a non-negative TimeSpan.</value>
</data>
</root>

View File

@ -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
{
/// <summary>
/// Represents a minimum data rate for the request body of an HTTP request.
/// </summary>
public interface IHttpRequestBodyMinimumDataRateFeature
{
/// <summary>
/// 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.
/// </summary>
MinimumDataRate MinimumDataRate { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// Creates a new instance of <see cref="MinimumDataRate"/>.
/// </summary>
/// <param name="rate">The minimum rate in bytes/second at which data should be processed.</param>
/// <param name="gracePeriod">The amount of time to delay enforcement of <paramref name="rate"/>.</param>
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;
}
/// <summary>
/// The minimum rate in bytes/second at which data should be processed.
/// </summary>
public double Rate { get; }
/// <summary>
/// The amount of time to delay enforcement of <see cref="MinimumDataRate" />.
/// </summary>
public TimeSpan GracePeriod { get; }
}
}

View File

@ -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<TContext>(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<TContext>(IHttpApplication<TContext> application, IPipeReader input, IPipe output)
{
_frame = new Frame<TContext>(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<Stream> 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);
}
}
}

View File

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

View File

@ -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<Type, object>(IHttpMaxRequestBodySizeFeatureType, _currentIHttpMaxRequestBodySizeFeature as global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
}
if (_currentIHttpRequestBodyMinimumDataRateFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpRequestBodyMinimumDataRateFeatureType, _currentIHttpRequestBodyMinimumDataRateFeature as global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpRequestBodyMinimumDataRateFeature);
}
if (_currentIHttpSendFileFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature as global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature);

View File

@ -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;
}
/// <summary>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,9 +7,6 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
/// <summary>
/// Summary description for KestrelTrace
/// </summary>
public class KestrelTrace : IKestrelTrace
{
private static readonly Action<ILogger, string, Exception> _connectionStart =
@ -57,6 +54,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private static readonly Action<ILogger, string, Exception> _connectionRejected =
LoggerMessage.Define<string>(LogLevel.Warning, 24, @"Connection id ""{ConnectionId}"" rejected because the maximum number of concurrent connections has been reached.");
private static readonly Action<ILogger, string, string, Exception> _requestBodyStart =
LoggerMessage.Define<string, string>(LogLevel.Debug, 25, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": started reading request body.");
private static readonly Action<ILogger, string, string, Exception> _requestBodyDone =
LoggerMessage.Define<string, string>(LogLevel.Debug, 26, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": done reading request body.");
private static readonly Action<ILogger, string, string, double, Exception> _requestBodyMinimumDataRateNotSatisfied =
LoggerMessage.Define<string, string, double>(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<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
=> _logger.Log(logLevel, eventId, state, exception, formatter);

View File

@ -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;
}
}
/// <summary>
@ -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;
}
}
/// <summary>
@ -234,5 +250,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
_maxConcurrentUpgradedConnections = value;
}
}
/// <summary>
/// Gets or sets the request body minimum data rate in bytes/second.
/// Setting this property to null indicates no minimum data rate should be enforced.
/// This limit has no effect on upgraded connections which are always unlimited.
/// This can be overridden per-request via <see cref="IHttpRequestBodyMinimumDataRateFeature"/>.
/// </summary>
/// <remarks>
/// Defaults to null.
/// </remarks>
public MinimumDataRate RequestBodyMinimumDataRate { get; set; } = new MinimumDataRate(rate: 1, gracePeriod: TimeSpan.FromSeconds(5));
}
}

View File

@ -977,7 +977,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
=> GetString("MaxRequestBodySizeCannotBeModifiedAfterRead");
/// <summary>
/// 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.
/// </summary>
internal static string MaxRequestBodySizeCannotBeModifiedForUpgradedRequests
{
@ -985,11 +985,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
}
/// <summary>
/// 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.
/// </summary>
internal static string FormatMaxRequestBodySizeCannotBeModifiedForUpgradedRequests()
=> GetString("MaxRequestBodySizeCannotBeModifiedForUpgradedRequests");
/// <summary>
/// Value must be a positive TimeSpan.
/// </summary>
internal static string PositiveTimeSpanRequired
{
get => GetString("PositiveTimeSpanRequired");
}
/// <summary>
/// Value must be a positive TimeSpan.
/// </summary>
internal static string FormatPositiveTimeSpanRequired()
=> GetString("PositiveTimeSpanRequired");
/// <summary>
/// Value must be a non-negative TimeSpan.
/// </summary>
internal static string NonNegativeTimeSpanRequired
{
get => GetString("NonNegativeTimeSpanRequired");
}
/// <summary>
/// Value must be a non-negative TimeSpan.
/// </summary>
internal static string FormatNonNegativeTimeSpanRequired()
=> GetString("NonNegativeTimeSpanRequired");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -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<IConnectionAdapter>(),
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<IKestrelTrace>();
_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<string>(), It.IsAny<string>(), 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<IKestrelTrace>();
_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<string>(), It.IsAny<string>(), 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<string>(), It.IsAny<string>(), 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<IKestrelTrace>();
_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<string>(), It.IsAny<string>(), 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<string>(), It.IsAny<string>(), 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<string>(), It.IsAny<string>(), 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<string>(), It.IsAny<string>(), 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<IKestrelTrace>();
_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<string>(), It.IsAny<string>(), It.IsAny<double>()),
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<string>(), It.IsAny<string>(), It.IsAny<double>()),
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<string>(), It.IsAny<string>(), It.IsAny<double>()),
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<IKestrelTrace>();
_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<string>(), It.IsAny<string>(), It.IsAny<double>()),
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<string>(), It.IsAny<string>(), It.IsAny<double>()),
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<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Once);
}
}
}

View File

@ -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<InvalidOperationException>(() => ((IHttpResponseFeature)_frame).OnStarting(_ => TaskCache.CompletedTask, null));
}
[Theory]
[MemberData(nameof(RequestBodyMinimumDataRateData))]
public void ConfiguringRequestBodyMinimumDataRateFeatureSetsRequestBodyMinimumDateRate(MinimumDataRate minimumDataRate)
{
((IFeatureCollection)_frame).Get<IHttpRequestBodyMinimumDataRateFeature>().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<TimeSpan> RequestBodyTimeoutDataValid => new TheoryData<TimeSpan>
{
TimeSpan.FromTicks(1),
TimeSpan.MaxValue,
Timeout.InfiniteTimeSpan,
TimeSpan.FromMilliseconds(-1) // Same as Timeout.InfiniteTimeSpan
};
public static TheoryData<TimeSpan> RequestBodyTimeoutDataInvalid => new TheoryData<TimeSpan>
{
TimeSpan.MinValue,
TimeSpan.FromTicks(-1),
TimeSpan.Zero
};
public static TheoryData<MinimumDataRate> RequestBodyMinimumDataRateData => new TheoryData<MinimumDataRate>
{
null,
new MinimumDataRate(rate: 1, gracePeriod: TimeSpan.Zero)
};
private class RequestHeadersWrapper : IHeaderDictionary
{
IHeaderDictionary _innerHeaders;

View File

@ -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<ArgumentOutOfRangeException>(() => 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<ArgumentOutOfRangeException>(() => 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<ArgumentOutOfRangeException>(() => 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<TimeSpan> TimeoutValidData => new TheoryData<TimeSpan>
{
TimeSpan.FromTicks(1),
TimeSpan.MaxValue,
};
public static TheoryData<TimeSpan> TimeoutInfiniteData => new TheoryData<TimeSpan>
{
Timeout.InfiniteTimeSpan,
};
public static TheoryData<TimeSpan> TimeoutInvalidData => new TheoryData<TimeSpan>
{
TimeSpan.MinValue,
TimeSpan.FromTicks(-1),
TimeSpan.Zero
};
}
}

View File

@ -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<ArgumentNullException>(() =>
new KestrelServer(Options.Create<KestrelServerOptions>(null), null, Mock.Of<ILoggerFactory>()));

View File

@ -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<BadHttpRequestException>(() =>
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<BadHttpRequestException>(() =>
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<BadHttpRequestException>(() =>
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<ITimeoutControl>();
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<byte>(new byte[1])));
// Time out on the next read
mockTimeoutControl
.Setup(timeoutControl => timeoutControl.TimedOut)
.Returns(true);
input.Cancel();
var exception = await Assert.ThrowsAsync<BadHttpRequestException>(() => body.ReadAsync(new ArraySegment<byte>(new byte[1])));
Assert.Equal(StatusCodes.Status408RequestTimeout, exception.StatusCode);
}
}
[Fact]
public async Task ConsumeAsyncThrowsOnTimeout()
{
using (var input = new TestInput())
{
var mockTimeoutControl = new Mock<ITimeoutControl>();
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<byte>(new byte[1])));
// Time out on the next read
mockTimeoutControl
.Setup(timeoutControl => timeoutControl.TimedOut)
.Returns(true);
input.Cancel();
var exception = await Assert.ThrowsAsync<BadHttpRequestException>(() => body.ConsumeAsync());
Assert.Equal(StatusCodes.Status408RequestTimeout, exception.StatusCode);
}
}
[Fact]
public async Task CopyToAsyncThrowsOnTimeout()
{
using (var input = new TestInput())
{
var mockTimeoutControl = new Mock<ITimeoutControl>();
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<byte>(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<BadHttpRequestException>(() => body.CopyToAsync(ms));
Assert.Equal(StatusCodes.Status408RequestTimeout, exception.StatusCode);
}
}
}
[Fact]
public async Task LogsWhenStartsReadingRequestBody()
{
using (var input = new TestInput())
{
var mockLogger = new Mock<IKestrelTrace>();
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<IKestrelTrace>();
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<IFrameControl>();
mockFrameControl
.Setup(frameControl => frameControl.ProduceContinue())
.Callback(() => produceContinueCalled = true);
input.Frame.FrameControl = mockFrameControl.Object;
var mockTimeoutControl = new Mock<ITimeoutControl>();
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<byte>(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<ITimeoutControl>();
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<byte>(new byte[1])));
input.Fin();
await Assert.ThrowsAsync<BadHttpRequestException>(async () => await body.ReadAsync(new ArraySegment<byte>(new byte[1])));
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StartTimingReads(), Times.Never);
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StopTimingReads(), Times.Never);
}
}
private void AssertASCII(string expected, ArraySegment<byte> actual)
{
var encoding = Encoding.ASCII;

View File

@ -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<ArgumentOutOfRangeException>(() => 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<ArgumentOutOfRangeException>(() => new MinimumDataRate(rate: 1, gracePeriod: value));
Assert.Equal("gracePeriod", exception.ParamName);
Assert.StartsWith(CoreStrings.NonNegativeTimeSpanRequired, exception.Message);
}
public static TheoryData<TimeSpan> GracePeriodValidData => new TheoryData<TimeSpan>
{
TimeSpan.Zero,
TimeSpan.FromTicks(1),
TimeSpan.MaxValue
};
public static TheoryData<TimeSpan> GracePeriodInvalidData => new TheoryData<TimeSpan>
{
TimeSpan.MinValue,
TimeSpan.FromTicks(-1)
};
}
}

View File

@ -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<object>(null, new FrameContext
FrameContext = new FrameContext
{
ServiceContext = new TestServiceContext(),
Input = Pipe.Reader,
ConnectionInformation = new MockConnectionInformation
{
PipeFactory = _pipelineFactory
}
});
FrameContext.FrameControl = this;
},
TimeoutControl = Mock.Of<ITimeoutControl>()
};
Frame = new Frame<object>(null, FrameContext);
Frame.FrameControl = Mock.Of<IFrameControl>();
}
public IPipe Pipe { get; }
public PipeFactory PipeFactory => _pipelineFactory;
public Frame FrameContext { get; set; }
public FrameContext FrameContext { get; }
public Frame Frame { get; set; }
public void Add(string text)
{
@ -56,38 +56,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Pipe.Writer.Complete();
}
public void ProduceContinue()
public void Cancel()
{
}
public void Pause()
{
}
public void Resume()
{
}
public void End(ProduceEndType endType)
{
}
public void Abort()
{
}
public void Write(ArraySegment<byte> data, Action<Exception, object> callback, object state)
{
}
Task IFrameControl.WriteAsync(ArraySegment<byte> data, CancellationToken cancellationToken)
{
return TaskCache.CompletedTask;
}
Task IFrameControl.FlushAsync(CancellationToken cancellationToken)
{
return TaskCache.CompletedTask;
Pipe.Reader.CancelPendingRead();
}
public void Dispose()

View File

@ -2,6 +2,7 @@
// 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;
@ -96,7 +97,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
cts.CancelAfter(LongDelay);
await connection.Send(
"POST / HTTP/1.1",
"POST /consume HTTP/1.1",
"Host:",
"Transfer-Encoding: chunked",
"",
@ -193,7 +194,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
ServerOptions =
{
AddServerHeader = false,
Limits = { KeepAliveTimeout = KeepAliveTimeout }
Limits =
{
KeepAliveTimeout = KeepAliveTimeout,
RequestBodyMinimumDataRate = null
}
}
});
}
@ -201,6 +206,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
private async Task App(HttpContext httpContext, CancellationToken longRunningCt, CancellationToken upgradeCt)
{
var ct = httpContext.RequestAborted;
var responseStream = httpContext.Response.Body;
var responseBytes = Encoding.ASCII.GetBytes("hello, world");
if (httpContext.Request.Path == "/longrunning")
{
@ -208,8 +215,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
await Task.Delay(1000);
}
await httpContext.Response.WriteAsync("hello, world");
}
else if (httpContext.Request.Path == "/upgrade")
{
@ -220,14 +225,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
await Task.Delay(LongDelay);
}
var responseBytes = Encoding.ASCII.GetBytes("hello, world");
await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
responseStream = stream;
}
}
else
else if (httpContext.Request.Path == "/consume")
{
await httpContext.Response.WriteAsync("hello, world");
var buffer = new byte[1024];
while (await httpContext.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) ;
}
await responseStream.WriteAsync(responseBytes, 0, responseBytes.Length);
}
private async Task ReceiveResponse(TestConnection connection)

View File

@ -8,7 +8,6 @@ using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@ -274,6 +273,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
options.Limits.MaxRequestHeadersTotalSize = (int)maxRequestBufferSize;
}
options.Limits.RequestBodyMinimumDataRate = null;
})
.UseContentRoot(Directory.GetCurrentDirectory())
.Configure(app => app.Run(async context =>

View File

@ -0,0 +1,172 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
public class RequestBodyTimeoutTests
{
[Fact]
public async Task RequestTimesOutWhenRequestBodyNotReceivedAtDesiredMinimumRate()
{
var minimumDataRateGracePeriod = TimeSpan.FromSeconds(5);
var systemClock = new MockSystemClock();
var serviceContext = new TestServiceContext
{
SystemClock = systemClock,
DateHeaderValueManager = new DateHeaderValueManager(systemClock)
};
var appRunningEvent = new ManualResetEventSlim();
using (var server = new TestServer(context =>
{
context.Features.Get<IHttpRequestBodyMinimumDataRateFeature>().MinimumDataRate =
new MinimumDataRate(rate: 1, gracePeriod: minimumDataRateGracePeriod);
appRunningEvent.Set();
return context.Request.Body.ReadAsync(new byte[1], 0, 1);
}, serviceContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"POST / HTTP/1.1",
"Host:",
"Content-Length: 1",
"",
"");
Assert.True(appRunningEvent.Wait(TimeSpan.FromSeconds(10)));
systemClock.UtcNow += minimumDataRateGracePeriod + TimeSpan.FromSeconds(1);
await connection.Receive(
"HTTP/1.1 408 Request Timeout",
"");
await connection.ReceiveForcedEnd(
"Connection: close",
$"Date: {serviceContext.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
}
}
[Fact]
public async Task RequestTimesWhenNotDrainedWithinDrainTimeoutPeriod()
{
// This test requires a real clock since we can't control when the drain timeout is set
var systemClock = new SystemClock();
var serviceContext = new TestServiceContext
{
SystemClock = systemClock,
DateHeaderValueManager = new DateHeaderValueManager(systemClock)
};
var appRunningEvent = new ManualResetEventSlim();
using (var server = new TestServer(context =>
{
context.Features.Get<IHttpRequestBodyMinimumDataRateFeature>().MinimumDataRate = null;
appRunningEvent.Set();
return Task.CompletedTask;
}, serviceContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"POST / HTTP/1.1",
"Host:",
"Content-Length: 1",
"",
"");
Assert.True(appRunningEvent.Wait(TimeSpan.FromSeconds(10)));
await connection.Receive(
"HTTP/1.1 408 Request Timeout",
"Connection: close",
"");
await connection.ReceiveStartsWith(
"Date: ");
await connection.ReceiveForcedEnd(
"Content-Length: 0",
"",
"");
}
}
}
[Fact]
public async Task ConnectionClosedEvenIfAppSwallowsException()
{
var minimumDataRateGracePeriod = TimeSpan.FromSeconds(5);
var systemClock = new MockSystemClock();
var serviceContext = new TestServiceContext
{
SystemClock = systemClock,
DateHeaderValueManager = new DateHeaderValueManager(systemClock)
};
var appRunningEvent = new ManualResetEventSlim();
var exceptionSwallowedEvent = new ManualResetEventSlim();
using (var server = new TestServer(async context =>
{
context.Features.Get<IHttpRequestBodyMinimumDataRateFeature>().MinimumDataRate =
new MinimumDataRate(rate: 1, gracePeriod: minimumDataRateGracePeriod);
appRunningEvent.Set();
try
{
await context.Request.Body.ReadAsync(new byte[1], 0, 1);
}
catch (BadHttpRequestException ex) when (ex.StatusCode == 408)
{
exceptionSwallowedEvent.Set();
}
var response = "hello, world";
context.Response.ContentLength = response.Length;
await context.Response.WriteAsync("hello, world");
}, serviceContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"POST / HTTP/1.1",
"Host:",
"Content-Length: 1",
"",
"");
Assert.True(appRunningEvent.Wait(TimeSpan.FromSeconds(10)));
systemClock.UtcNow += minimumDataRateGracePeriod + TimeSpan.FromSeconds(1);
Assert.True(exceptionSwallowedEvent.Wait(TimeSpan.FromSeconds(10)));
await connection.Receive(
"HTTP/1.1 200 OK",
"");
await connection.ReceiveForcedEnd(
$"Date: {serviceContext.DateHeaderValue}",
"Content-Length: 12",
"",
"hello, world");
}
}
}
}
}

View File

@ -103,7 +103,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
ServerOptions =
{
AddServerHeader = false,
Limits = { RequestHeadersTimeout = RequestHeadersTimeout }
Limits =
{
RequestHeadersTimeout = RequestHeadersTimeout,
RequestBodyMinimumDataRate = null
}
}
});
}

View File

@ -59,9 +59,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
Assert.True(bufferLength % 256 == 0, $"{nameof(bufferLength)} must be evenly divisible by 256");
var builder = new WebHostBuilder()
.UseKestrel(o =>
.UseKestrel(options =>
{
o.Limits.MaxRequestBodySize = contentLength;
options.Limits.MaxRequestBodySize = contentLength;
options.Limits.RequestBodyMinimumDataRate = null;
})
.UseUrls("http://127.0.0.1:0/")
.Configure(app =>

View File

@ -7,6 +7,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks
{
public class MockTimeoutControl : ITimeoutControl
{
public bool TimedOut { get; }
public void CancelTimeout()
{
}
@ -18,5 +20,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks
public void SetTimeout(long ticks, TimeoutAction timeoutAction)
{
}
public void StartTimingReads()
{
}
public void StopTimingReads()
{
}
public void PauseTimingReads()
{
}
public void ResumeTimingReads()
{
}
public void BytesRead(int count)
{
}
}
}

View File

@ -36,5 +36,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
public void RequestProcessingError(string connectionId, Exception ex) { }
public void HeartbeatSlow(TimeSpan interval, DateTimeOffset now) { }
public void ApplicationNeverCompleted(string connectionId) { }
public void RequestBodyStart(string connectionId, string traceIdentifier) { }
public void RequestBodyDone(string connectionId, string traceIdentifier) { }
public void RequestBodyMininumDataRateNotSatisfied(string connectionId, string traceIdentifier, double rate) { }
}
}

View File

@ -2,24 +2,25 @@
// 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.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Testing
{
public class MockSystemClock : ISystemClock
{
private DateTimeOffset _utcNow = DateTimeOffset.Now;
private long _utcNowTicks = DateTimeOffset.UtcNow.Ticks;
public DateTimeOffset UtcNow
{
get
{
UtcNowCalled++;
return _utcNow;
return new DateTimeOffset(Interlocked.Read(ref _utcNowTicks), TimeSpan.Zero);
}
set
{
_utcNow = value;
Interlocked.Exchange(ref _utcNowTicks, value.Ticks);
}
}

View File

@ -29,7 +29,6 @@ namespace Microsoft.AspNetCore.Testing
SystemClock = new MockSystemClock();
DateHeaderValueManager = new DateHeaderValueManager(SystemClock);
ConnectionManager = new FrameConnectionManager(Log, ResourceCounter.Unlimited, ResourceCounter.Unlimited);
DateHeaderValue = DateHeaderValueManager.GetDateHeaderValues().String;
HttpParserFactory = frameAdapter => new HttpParser<FrameAdapter>(frameAdapter.Frame.ServiceContext.Log.IsEnabled(LogLevel.Information));
ServerOptions = new KestrelServerOptions
{
@ -39,6 +38,6 @@ namespace Microsoft.AspNetCore.Testing
public ILoggerFactory LoggerFactory { get; }
public string DateHeaderValue { get; }
public string DateHeaderValue => DateHeaderValueManager.GetDateHeaderValues().String;
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
namespace CodeGenerator
{
@ -45,12 +46,13 @@ namespace CodeGenerator
typeof(ITlsConnectionFeature),
typeof(IHttpWebSocketFeature),
typeof(ISessionFeature),
typeof(IHttpMaxRequestBodySizeFeature)
typeof(IHttpMaxRequestBodySizeFeature),
typeof(IHttpRequestBodyMinimumDataRateFeature),
};
var rareFeatures = new[]
{
typeof(IHttpSendFileFeature)
typeof(IHttpSendFileFeature),
};
var allFeatures = alwaysFeatures.Concat(commonFeatures).Concat(sometimesFeatures).Concat(rareFeatures);
@ -65,7 +67,8 @@ namespace CodeGenerator
typeof(IHttpRequestIdentifierFeature),
typeof(IHttpRequestLifetimeFeature),
typeof(IHttpConnectionFeature),
typeof(IHttpMaxRequestBodySizeFeature)
typeof(IHttpMaxRequestBodySizeFeature),
typeof(IHttpRequestBodyMinimumDataRateFeature),
};
return $@"// Copyright (c) .NET Foundation. All rights reserved.