Add request body minimum data rate feature (#1874).
This commit is contained in:
parent
f96c48c08d
commit
fcc04f8c3d
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>()));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -103,7 +103,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
ServerOptions =
|
||||
{
|
||||
AddServerHeader = false,
|
||||
Limits = { RequestHeadersTimeout = RequestHeadersTimeout }
|
||||
Limits =
|
||||
{
|
||||
RequestHeadersTimeout = RequestHeadersTimeout,
|
||||
RequestBodyMinimumDataRate = null
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue