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

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

View File

@ -25,7 +25,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{0EF2AC
test\shared\KestrelTestLoggerProvider.cs = test\shared\KestrelTestLoggerProvider.cs test\shared\KestrelTestLoggerProvider.cs = test\shared\KestrelTestLoggerProvider.cs
test\shared\LifetimeNotImplemented.cs = test\shared\LifetimeNotImplemented.cs test\shared\LifetimeNotImplemented.cs = test\shared\LifetimeNotImplemented.cs
test\shared\MockConnectionInformation.cs = test\shared\MockConnectionInformation.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\MockLogger.cs = test\shared\MockLogger.cs
test\shared\MockSystemClock.cs = test\shared\MockSystemClock.cs test\shared\MockSystemClock.cs = test\shared\MockSystemClock.cs
test\shared\StringExtensions.cs = test\shared\StringExtensions.cs test\shared\StringExtensions.cs = test\shared\StringExtensions.cs

View File

@ -327,4 +327,10 @@
<data name="MaxRequestBodySizeCannotBeModifiedForUpgradedRequests" xml:space="preserve"> <data name="MaxRequestBodySizeCannotBeModifiedForUpgradedRequests" xml:space="preserve">
<value>The maximum request body size cannot be modified after the request has been upgraded.</value> <value>The maximum request body size cannot be modified after the request has been upgraded.</value>
</data> </data>
<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> </root>

View File

@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
{
/// <summary>
/// Represents a minimum data rate for the request body of an HTTP request.
/// </summary>
public interface IHttpRequestBodyMinimumDataRateFeature
{
/// <summary>
/// The minimum data rate in bytes/second at which the request body should be received.
/// Setting this property to null indicates no minimum data rate should be enforced.
/// This limit has no effect on upgraded connections which are always unlimited.
/// </summary>
MinimumDataRate MinimumDataRate { get; set; }
}
}

View File

@ -0,0 +1,41 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
{
public class MinimumDataRate
{
/// <summary>
/// Creates a new instance of <see cref="MinimumDataRate"/>.
/// </summary>
/// <param name="rate">The minimum rate in bytes/second at which data should be processed.</param>
/// <param name="gracePeriod">The amount of time to delay enforcement of <paramref name="rate"/>.</param>
public MinimumDataRate(double rate, TimeSpan gracePeriod)
{
if (rate <= 0)
{
throw new ArgumentOutOfRangeException(nameof(rate), CoreStrings.PositiveNumberRequired);
}
if (gracePeriod < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(gracePeriod), CoreStrings.NonNegativeTimeSpanRequired);
}
Rate = rate;
GracePeriod = gracePeriod;
}
/// <summary>
/// The minimum rate in bytes/second at which data should be processed.
/// </summary>
public double Rate { get; }
/// <summary>
/// The amount of time to delay enforcement of <see cref="MinimumDataRate" />.
/// </summary>
public TimeSpan GracePeriod { get; }
}
}

View File

@ -29,6 +29,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private long _timeoutTimestamp = long.MaxValue; private long _timeoutTimestamp = long.MaxValue;
private TimeoutAction _timeoutAction; private TimeoutAction _timeoutAction;
private object _readTimingLock = new object();
private bool _readTimingEnabled;
private bool _readTimingPauseRequested;
private long _readTimingElapsedTicks;
private long _readTimingBytesRead;
private Task _lifetimeTask; private Task _lifetimeTask;
public FrameConnection(FrameConnectionContext context) public FrameConnection(FrameConnectionContext context)
@ -36,6 +42,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_context = context; _context = context;
} }
// For testing
internal Frame Frame => _frame;
public bool TimedOut { get; private set; }
public string ConnectionId => _context.ConnectionId; public string ConnectionId => _context.ConnectionId;
public IPipeWriter Input => _context.Input.Writer; public IPipeWriter Input => _context.Input.Writer;
public IPipeReader Output => _context.Output.Reader; 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 must be initialized before adding the connection to the connection manager
_frame = new Frame<TContext>(application, new FrameContext CreateFrame(application, input, output);
{
ConnectionId = _context.ConnectionId,
ConnectionInformation = _context.ConnectionInformation,
ServiceContext = _context.ServiceContext,
TimeoutControl = this,
Input = input,
Output = output
});
// Do this before the first await so we don't yield control to the transport until we've // 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 // 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) 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) // Abort the connection (if not already aborted)
_frame.Abort(ex); _frame.Abort(ex);
@ -152,7 +168,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public Task StopAsync() public Task StopAsync()
{ {
Debug.Assert(_frame != null, $"nameof({_frame}) is null"); Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
_frame.Stop(); _frame.Stop();
@ -161,7 +177,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public void Abort(Exception ex) 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) // Abort the connection (if not already aborted)
_frame.Abort(ex); _frame.Abort(ex);
@ -169,7 +185,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public Task AbortAsync(Exception ex) 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) // Abort the connection (if not already aborted)
_frame.Abort(ex); _frame.Abort(ex);
@ -177,16 +193,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
return _lifetimeTask; 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); _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() 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 features = new FeatureCollection();
var connectionAdapters = _context.ConnectionAdapters; var connectionAdapters = _context.ConnectionAdapters;
@ -231,7 +256,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public void Tick(DateTimeOffset now) 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; var timestamp = now.Ticks;
@ -242,10 +267,41 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
if (_timeoutAction == TimeoutAction.SendTimeoutResponse) 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); 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. // Add Heartbeat.Interval since this can be called right before the next heartbeat.
Interlocked.Exchange(ref _timeoutTimestamp, _lastTimestamp + ticks + Heartbeat.Interval.Ticks); Interlocked.Exchange(ref _timeoutTimestamp, _lastTimestamp + ticks + Heartbeat.Interval.Ticks);
} }
public void StartTimingReads()
{
lock (_readTimingLock)
{
_readTimingElapsedTicks = 0;
_readTimingBytesRead = 0;
_readTimingEnabled = true;
}
}
public void StopTimingReads()
{
lock (_readTimingLock)
{
_readTimingEnabled = false;
}
}
public void PauseTimingReads()
{
lock (_readTimingLock)
{
_readTimingPauseRequested = true;
}
}
public void ResumeTimingReads()
{
lock (_readTimingLock)
{
_readTimingEnabled = true;
// In case pause and resume were both called between ticks
_readTimingPauseRequested = false;
}
}
public void BytesRead(int count)
{
Interlocked.Add(ref _readTimingBytesRead, count);
}
} }
} }

View File

@ -10,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
@ -21,7 +22,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
IHttpConnectionFeature, IHttpConnectionFeature,
IHttpRequestLifetimeFeature, IHttpRequestLifetimeFeature,
IHttpRequestIdentifierFeature, IHttpRequestIdentifierFeature,
IHttpMaxRequestBodySizeFeature IHttpMaxRequestBodySizeFeature,
IHttpRequestBodyMinimumDataRateFeature
{ {
// NOTE: When feature interfaces are added to or removed from this Frame class implementation, // 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. // 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] object IFeatureCollection.this[Type key]
{ {
get => FastFeatureGet(key); get => FastFeatureGet(key);

View File

@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly Type IHttpWebSocketFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature); private static readonly Type IHttpWebSocketFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature);
private static readonly Type ISessionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ISessionFeature); 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 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 static readonly Type IHttpSendFileFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature);
private object _currentIHttpRequestFeature; private object _currentIHttpRequestFeature;
@ -42,6 +43,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private object _currentIHttpWebSocketFeature; private object _currentIHttpWebSocketFeature;
private object _currentISessionFeature; private object _currentISessionFeature;
private object _currentIHttpMaxRequestBodySizeFeature; private object _currentIHttpMaxRequestBodySizeFeature;
private object _currentIHttpRequestBodyMinimumDataRateFeature;
private object _currentIHttpSendFileFeature; private object _currentIHttpSendFileFeature;
private void FastReset() private void FastReset()
@ -53,6 +55,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpRequestLifetimeFeature = this; _currentIHttpRequestLifetimeFeature = this;
_currentIHttpConnectionFeature = this; _currentIHttpConnectionFeature = this;
_currentIHttpMaxRequestBodySizeFeature = this; _currentIHttpMaxRequestBodySizeFeature = this;
_currentIHttpRequestBodyMinimumDataRateFeature = this;
_currentIServiceProvidersFeature = null; _currentIServiceProvidersFeature = null;
_currentIHttpAuthenticationFeature = null; _currentIHttpAuthenticationFeature = null;
@ -132,6 +135,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
return _currentIHttpMaxRequestBodySizeFeature; return _currentIHttpMaxRequestBodySizeFeature;
} }
if (key == IHttpRequestBodyMinimumDataRateFeatureType)
{
return _currentIHttpRequestBodyMinimumDataRateFeature;
}
if (key == IHttpSendFileFeatureType) if (key == IHttpSendFileFeatureType)
{ {
return _currentIHttpSendFileFeature; return _currentIHttpSendFileFeature;
@ -223,6 +230,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpMaxRequestBodySizeFeature = feature; _currentIHttpMaxRequestBodySizeFeature = feature;
return; return;
} }
if (key == IHttpRequestBodyMinimumDataRateFeatureType)
{
_currentIHttpRequestBodyMinimumDataRateFeature = feature;
return;
}
if (key == IHttpSendFileFeatureType) if (key == IHttpSendFileFeatureType)
{ {
_currentIHttpSendFileFeature = feature; _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); 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) if (_currentIHttpSendFileFeature != null)
{ {
yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature as global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature); yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature as global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature);

View File

@ -21,6 +21,7 @@ using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
// ReSharper disable AccessToModifiedClosure // ReSharper disable AccessToModifiedClosure
@ -300,6 +301,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected FrameResponseHeaders FrameResponseHeaders { get; } = new FrameResponseHeaders(); protected FrameResponseHeaders FrameResponseHeaders { get; } = new FrameResponseHeaders();
public MinimumDataRate RequestBodyMinimumDataRate { get; set; }
public void InitializeStreams(MessageBody messageBody) public void InitializeStreams(MessageBody messageBody)
{ {
if (_frameStreams == null) if (_frameStreams == null)
@ -376,6 +379,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_responseBytesWritten = 0; _responseBytesWritten = 0;
_requestCount++; _requestCount++;
RequestBodyMinimumDataRate = ServerOptions.Limits.RequestBodyMinimumDataRate;
} }
/// <summary> /// <summary>

View File

@ -160,7 +160,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
if (_keepAlive) if (_keepAlive)
{ {
// Finish reading the request body in case the app did not. // Finish reading the request body in case the app did not.
TimeoutControl.SetTimeout(Constants.RequestBodyDrainTimeout.Ticks, TimeoutAction.SendTimeoutResponse);
await messageBody.ConsumeAsync(); await messageBody.ConsumeAsync();
TimeoutControl.CancelTimeout();
// At this point both the request body pipe reader and writer should be completed. // At this point both the request body pipe reader and writer should be completed.
RequestBodyPipe.Reset(); RequestBodyPipe.Reset();
} }

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines; using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http 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 static readonly MessageBody _zeroContentLengthKeepAlive = new ForZeroContentLength(keepAlive: true);
private readonly Frame _context; private readonly Frame _context;
private bool _send100Continue = true; private bool _send100Continue = true;
private volatile bool _canceled; private volatile bool _canceled;
@ -32,22 +34,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public virtual bool IsEmpty => false; public virtual bool IsEmpty => false;
private IKestrelTrace Log => _context.ServiceContext.Log;
private async Task PumpAsync() private async Task PumpAsync()
{ {
Exception error = null; Exception error = null;
try try
{ {
var awaitable = _context.Input.ReadAsync();
if (!awaitable.IsCompleted)
{
TryProduceContinue();
}
TryStartTimingReads();
while (true) 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 readableBuffer = result.Buffer;
var consumed = readableBuffer.Start; var consumed = readableBuffer.Start;
var examined = readableBuffer.End; var examined = readableBuffer.End;
@ -73,7 +85,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
writableBuffer.Commit(); 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) if (done)
{ {
@ -84,6 +111,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
_context.ThrowRequestRejected(RequestRejectionReason.UnexpectedEndOfRequestContent); _context.ThrowRequestRejected(RequestRejectionReason.UnexpectedEndOfRequestContent);
} }
awaitable = _context.Input.ReadAsync();
} }
finally finally
{ {
@ -98,6 +127,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
finally finally
{ {
_context.RequestBodyPipe.Writer.Complete(error); _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) protected void Copy(ReadableBuffer readableBuffer, WritableBuffer writableBuffer)
{ {
_context.TimeoutControl.BytesRead(readableBuffer.Length);
if (readableBuffer.IsSingleSpan) if (readableBuffer.IsSingleSpan)
{ {
writableBuffer.Write(readableBuffer.First.Span); 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( public static MessageBody For(
HttpVersion httpVersion, HttpVersion httpVersion,
FrameRequestHeaders headers, FrameRequestHeaders headers,

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{ {
internal static class Constants internal static class Constants
@ -28,5 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
public const string SocketDescriptorPrefix = "sockfd:"; public const string SocketDescriptorPrefix = "sockfd:";
public const string ServerName = "Kestrel"; public const string ServerName = "Kestrel";
public static readonly TimeSpan RequestBodyDrainTimeout = TimeSpan.FromSeconds(5);
} }
} }

View File

@ -37,5 +37,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
void HeartbeatSlow(TimeSpan interval, DateTimeOffset now); void HeartbeatSlow(TimeSpan interval, DateTimeOffset now);
void ApplicationNeverCompleted(string connectionId); void ApplicationNeverCompleted(string connectionId);
void RequestBodyStart(string connectionId, string traceIdentifier);
void RequestBodyDone(string connectionId, string traceIdentifier);
void RequestBodyMininumDataRateNotSatisfied(string connectionId, string traceIdentifier, double rate);
} }
} }

View File

@ -5,8 +5,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{ {
public interface ITimeoutControl public interface ITimeoutControl
{ {
bool TimedOut { get; }
void SetTimeout(long ticks, TimeoutAction timeoutAction); void SetTimeout(long ticks, TimeoutAction timeoutAction);
void ResetTimeout(long ticks, TimeoutAction timeoutAction); void ResetTimeout(long ticks, TimeoutAction timeoutAction);
void CancelTimeout(); void CancelTimeout();
void StartTimingReads();
void PauseTimingReads();
void ResumeTimingReads();
void StopTimingReads();
void BytesRead(int count);
} }
} }

View File

@ -7,9 +7,6 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{ {
/// <summary>
/// Summary description for KestrelTrace
/// </summary>
public class KestrelTrace : IKestrelTrace public class KestrelTrace : IKestrelTrace
{ {
private static readonly Action<ILogger, string, Exception> _connectionStart = 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 = 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."); 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; protected readonly ILogger _logger;
public KestrelTrace(ILogger logger) public KestrelTrace(ILogger logger)
@ -139,6 +145,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_applicationNeverCompleted(_logger, connectionId, null); _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) 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); => _logger.Log(logLevel, eventId, state, exception, formatter);

View File

@ -2,7 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Threading;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
namespace Microsoft.AspNetCore.Server.Kestrel.Core 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 // Matches the non-configurable default response buffer size for Kestrel in 1.0.0
private long? _maxResponseBufferSize = 64 * 1024; private long? _maxResponseBufferSize = 64 * 1024;
// Matches the default client_max_body_size in nginx. Also large enough that most requests // Matches the default client_max_body_size in nginx.
// should be under the limit. // Also large enough that most requests should be under the limit.
private long? _maxRequestBufferSize = 1024 * 1024; private long? _maxRequestBufferSize = 1024 * 1024;
// Matches the default large_client_header_buffers in nginx. // 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); private TimeSpan _requestHeadersTimeout = TimeSpan.FromSeconds(30);
// default to unlimited // Unlimited connections are allowed by default.
private long? _maxConcurrentConnections = null; private long? _maxConcurrentConnections = null;
private long? _maxConcurrentUpgradedConnections = null; private long? _maxConcurrentUpgradedConnections = null;
@ -169,7 +171,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
public TimeSpan KeepAliveTimeout public TimeSpan KeepAliveTimeout
{ {
get => _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> /// <summary>
@ -181,7 +190,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
public TimeSpan RequestHeadersTimeout public TimeSpan RequestHeadersTimeout
{ {
get => _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> /// <summary>
@ -234,5 +250,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
_maxConcurrentUpgradedConnections = value; _maxConcurrentUpgradedConnections = value;
} }
} }
/// <summary>
/// Gets or sets the request body minimum data rate in bytes/second.
/// Setting this property to null indicates no minimum data rate should be enforced.
/// This limit has no effect on upgraded connections which are always unlimited.
/// This can be overridden per-request via <see cref="IHttpRequestBodyMinimumDataRateFeature"/>.
/// </summary>
/// <remarks>
/// Defaults to null.
/// </remarks>
public MinimumDataRate RequestBodyMinimumDataRate { get; set; } = new MinimumDataRate(rate: 1, gracePeriod: TimeSpan.FromSeconds(5));
} }
} }

View File

@ -977,7 +977,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
=> GetString("MaxRequestBodySizeCannotBeModifiedAfterRead"); => GetString("MaxRequestBodySizeCannotBeModifiedAfterRead");
/// <summary> /// <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> /// </summary>
internal static string MaxRequestBodySizeCannotBeModifiedForUpgradedRequests internal static string MaxRequestBodySizeCannotBeModifiedForUpgradedRequests
{ {
@ -985,11 +985,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
} }
/// <summary> /// <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> /// </summary>
internal static string FormatMaxRequestBodySizeCannotBeModifiedForUpgradedRequests() internal static string FormatMaxRequestBodySizeCannotBeModifiedForUpgradedRequests()
=> GetString("MaxRequestBodySizeCannotBeModifiedForUpgradedRequests"); => 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -0,0 +1,324 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
using Microsoft.AspNetCore.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class FrameConnectionTests : IDisposable
{
private readonly PipeFactory _pipeFactory;
private readonly FrameConnectionContext _frameConnectionContext;
private readonly FrameConnection _frameConnection;
public FrameConnectionTests()
{
_pipeFactory = new PipeFactory();
_frameConnectionContext = new FrameConnectionContext
{
ConnectionId = "0123456789",
ConnectionAdapters = new List<IConnectionAdapter>(),
ConnectionInformation = new MockConnectionInformation
{
PipeFactory = _pipeFactory
},
FrameConnectionId = long.MinValue,
Input = _pipeFactory.Create(),
Output = _pipeFactory.Create(),
ServiceContext = new TestServiceContext
{
SystemClock = new SystemClock()
}
};
_frameConnection = new FrameConnection(_frameConnectionContext);
}
public void Dispose()
{
_pipeFactory.Dispose();
}
[Fact]
public void TimesOutWhenRequestBodyDoesNotSatisfyMinimumDataRate()
{
var requestBodyMinimumDataRate = 100;
var requestBodyGracePeriod = TimeSpan.FromSeconds(5);
_frameConnectionContext.ServiceContext.ServerOptions.Limits.RequestBodyMinimumDataRate =
new MinimumDataRate(rate: requestBodyMinimumDataRate, gracePeriod: requestBodyGracePeriod);
var mockLogger = new Mock<IKestrelTrace>();
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
_frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
_frameConnection.Frame.Reset();
// Initialize timestamp
var now = DateTimeOffset.UtcNow;
_frameConnection.Tick(now);
_frameConnection.StartTimingReads();
// Tick after grace period w/ low data rate
now += requestBodyGracePeriod + TimeSpan.FromSeconds(1);
_frameConnection.BytesRead(1);
_frameConnection.Tick(now);
// Timed out
Assert.True(_frameConnection.TimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), requestBodyMinimumDataRate), Times.Once);
}
[Fact]
public void MinimumDataRateNotEnforcedDuringGracePeriod()
{
var requestBodyMinimumDataRate = 100;
var requestBodyGracePeriod = TimeSpan.FromSeconds(2);
_frameConnectionContext.ServiceContext.ServerOptions.Limits.RequestBodyMinimumDataRate =
new MinimumDataRate(rate: requestBodyMinimumDataRate, gracePeriod: requestBodyGracePeriod);
var mockLogger = new Mock<IKestrelTrace>();
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
_frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
_frameConnection.Frame.Reset();
// Initialize timestamp
var now = DateTimeOffset.UtcNow;
_frameConnection.Tick(now);
_frameConnection.StartTimingReads();
// Tick during grace period w/ low data rate
now += TimeSpan.FromSeconds(1);
_frameConnection.BytesRead(10);
_frameConnection.Tick(now);
// Not timed out
Assert.False(_frameConnection.TimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), requestBodyMinimumDataRate), Times.Never);
// Tick after grace period w/ low data rate
now += TimeSpan.FromSeconds(2);
_frameConnection.BytesRead(10);
_frameConnection.Tick(now);
// Timed out
Assert.True(_frameConnection.TimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), requestBodyMinimumDataRate), Times.Once);
}
[Fact]
public void DataRateIsAveragedOverTimeSpentReadingRequestBody()
{
var requestBodyMinimumDataRate = 100;
var requestBodyGracePeriod = TimeSpan.FromSeconds(1);
_frameConnectionContext.ServiceContext.ServerOptions.Limits.RequestBodyMinimumDataRate =
new MinimumDataRate(rate: requestBodyMinimumDataRate, gracePeriod: requestBodyGracePeriod);
var mockLogger = new Mock<IKestrelTrace>();
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
_frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
_frameConnection.Frame.Reset();
// Initialize timestamp
var now = DateTimeOffset.UtcNow;
_frameConnection.Tick(now);
_frameConnection.StartTimingReads();
// Tick after grace period to start enforcing minimum data rate
now += requestBodyGracePeriod;
_frameConnection.BytesRead(100);
_frameConnection.Tick(now);
// Data rate: 200 bytes/second
now += TimeSpan.FromSeconds(1);
_frameConnection.BytesRead(300);
_frameConnection.Tick(now);
// Not timed out
Assert.False(_frameConnection.TimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), requestBodyMinimumDataRate), Times.Never);
// Data rate: 150 bytes/second
now += TimeSpan.FromSeconds(1);
_frameConnection.BytesRead(50);
_frameConnection.Tick(now);
// Not timed out
Assert.False(_frameConnection.TimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), requestBodyMinimumDataRate), Times.Never);
// Data rate: 115 bytes/second
now += TimeSpan.FromSeconds(1);
_frameConnection.BytesRead(10);
_frameConnection.Tick(now);
// Not timed out
Assert.False(_frameConnection.TimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), requestBodyMinimumDataRate), Times.Never);
// Data rate: 50 bytes/second
now += TimeSpan.FromSeconds(6);
_frameConnection.BytesRead(40);
_frameConnection.Tick(now);
// Timed out
Assert.True(_frameConnection.TimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), requestBodyMinimumDataRate), Times.Once);
}
[Fact]
public void PausedTimeDoesNotCountAgainstRequestBodyTimeout()
{
var requestBodyTimeout = TimeSpan.FromSeconds(5);
var systemClock = new MockSystemClock();
_frameConnectionContext.ServiceContext.ServerOptions.Limits.RequestBodyMinimumDataRate =
new MinimumDataRate(rate: 100, gracePeriod: TimeSpan.Zero);
_frameConnectionContext.ServiceContext.SystemClock = systemClock;
var mockLogger = new Mock<IKestrelTrace>();
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
_frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
_frameConnection.Frame.Reset();
// Initialize timestamp
_frameConnection.Tick(systemClock.UtcNow);
_frameConnection.StartTimingReads();
// Tick at 1s, expected counted time is 1s, expected data rate is 400 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(1);
_frameConnection.BytesRead(400);
_frameConnection.Tick(systemClock.UtcNow);
// Pause at 1.5s
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_frameConnection.PauseTimingReads();
// Tick at 2s, expected counted time is 2s, expected data rate is 400 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_frameConnection.Tick(systemClock.UtcNow);
// Tick at 6s, expected counted time is 2s, expected data rate is 400 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(4);
_frameConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_frameConnection.TimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Never);
// Resume at 6.5s
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_frameConnection.ResumeTimingReads();
// Tick at 8s, expected counted time is 4s, expected data rate is 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(1.5);
_frameConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_frameConnection.TimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Never);
// Tick at 9s, expected counted time is 9s, expected data rate drops below 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(1);
_frameConnection.Tick(systemClock.UtcNow);
// Timed out
Assert.True(_frameConnection.TimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Once);
}
[Fact]
public void NotPausedWhenResumeCalledBeforeNextTick()
{
var systemClock = new MockSystemClock();
_frameConnectionContext.ServiceContext.ServerOptions.Limits.RequestBodyMinimumDataRate =
new MinimumDataRate(rate: 100, gracePeriod: TimeSpan.Zero);
_frameConnectionContext.ServiceContext.SystemClock = systemClock;
var mockLogger = new Mock<IKestrelTrace>();
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
_frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
_frameConnection.Frame.Reset();
// Initialize timestamp
_frameConnection.Tick(systemClock.UtcNow);
_frameConnection.StartTimingReads();
// Tick at 1s, expected counted time is 1s, expected data rate is 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(1);
_frameConnection.BytesRead(100);
_frameConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_frameConnection.TimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Never);
// Pause at 1.25s
systemClock.UtcNow += TimeSpan.FromSeconds(0.25);
_frameConnection.PauseTimingReads();
// Resume at 1.5s
systemClock.UtcNow += TimeSpan.FromSeconds(0.25);
_frameConnection.ResumeTimingReads();
// Tick at 2s, expected counted time is 2s, expected data rate is 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_frameConnection.BytesRead(100);
_frameConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_frameConnection.TimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Never);
// Tick at 3s, expected counted time is 3s, expected data rate drops below 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(1);
_frameConnection.Tick(systemClock.UtcNow);
// Timed out
Assert.True(_frameConnection.TimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Once);
}
}
}

View File

@ -12,6 +12,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; 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;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
@ -140,6 +141,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.NotEqual(nextId, secondId); 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] [Fact]
public void TraceIdentifierCountsRequestsPerFrame() public void TraceIdentifierCountsRequestsPerFrame()
{ {
@ -243,6 +254,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Throws<InvalidOperationException>(() => ((IHttpResponseFeature)_frame).OnStarting(_ => TaskCache.CompletedTask, null)); 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] [Fact]
public void ResetResetsRequestHeaders() 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 private class RequestHeadersWrapper : IHeaderDictionary
{ {
IHeaderDictionary _innerHeaders; IHeaderDictionary _innerHeaders;

View File

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNetCore.Server.Kestrel.Core; using System.Threading;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
@ -154,16 +154,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
} }
[Theory] [Theory]
[InlineData(0)] [MemberData(nameof(TimeoutValidData))]
[InlineData(0.5)] public void KeepAliveTimeoutValid(TimeSpan value)
[InlineData(2.1)]
[InlineData(2.5)]
[InlineData(2.9)]
public void KeepAliveTimeoutValid(double seconds)
{ {
var o = new KestrelServerLimits(); Assert.Equal(value, new KestrelServerLimits { KeepAliveTimeout = value }.KeepAliveTimeout);
o.KeepAliveTimeout = TimeSpan.FromSeconds(seconds); }
Assert.Equal(seconds, o.KeepAliveTimeout.TotalSeconds);
[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] [Fact]
@ -173,17 +183,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
} }
[Theory] [Theory]
[InlineData(0)] [MemberData(nameof(TimeoutValidData))]
[InlineData(0.5)] public void RequestHeadersTimeoutValid(TimeSpan value)
[InlineData(1.0)]
[InlineData(2.5)]
[InlineData(10)]
[InlineData(60)]
public void RequestHeadersTimeoutValid(double seconds)
{ {
var o = new KestrelServerLimits(); Assert.Equal(value, new KestrelServerLimits { RequestHeadersTimeout = value }.RequestHeadersTimeout);
o.RequestHeadersTimeout = TimeSpan.FromSeconds(seconds); }
Assert.Equal(seconds, o.RequestHeadersTimeout.TotalSeconds);
[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] [Fact]
@ -272,5 +291,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new KestrelServerLimits().MaxRequestBodySize = value); var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new KestrelServerLimits().MaxRequestBodySize = value);
Assert.StartsWith(CoreStrings.NonNegativeNumberOrNullRequired, ex.Message); Assert.StartsWith(CoreStrings.NonNegativeNumberOrNullRequired, ex.Message);
} }
[Fact]
public void RequestBodyMinimumDataRateDefault()
{
Assert.NotNull(new KestrelServerLimits().RequestBodyMinimumDataRate);
Assert.Equal(1, new KestrelServerLimits().RequestBodyMinimumDataRate.Rate);
Assert.Equal(TimeSpan.FromSeconds(5), new KestrelServerLimits().RequestBodyMinimumDataRate.GracePeriod);
}
public static TheoryData<TimeSpan> TimeoutValidData => new TheoryData<TimeSpan>
{
TimeSpan.FromTicks(1),
TimeSpan.MaxValue,
};
public static TheoryData<TimeSpan> TimeoutInfiniteData => new TheoryData<TimeSpan>
{
Timeout.InfiniteTimeSpan,
};
public static TheoryData<TimeSpan> TimeoutInvalidData => new TheoryData<TimeSpan>
{
TimeSpan.MinValue,
TimeSpan.FromTicks(-1),
TimeSpan.Zero
};
} }
} }

View File

@ -8,6 +8,7 @@ using System.Threading;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
@ -100,9 +101,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public void StartWithMaxRequestBufferSizeLessThanMaxRequestLineSizeThrows(long maxRequestBufferSize, int maxRequestLineSize) public void StartWithMaxRequestBufferSizeLessThanMaxRequestLineSizeThrows(long maxRequestBufferSize, int maxRequestLineSize)
{ {
var testLogger = new TestApplicationErrorLogger { ThrowOnCriticalErrors = false }; var testLogger = new TestApplicationErrorLogger { ThrowOnCriticalErrors = false };
var options = new KestrelServerOptions(); var options = new KestrelServerOptions
options.Limits.MaxRequestBufferSize = maxRequestBufferSize; {
options.Limits.MaxRequestLineSize = maxRequestLineSize; Limits =
{
MaxRequestBufferSize = maxRequestBufferSize,
MaxRequestLineSize = maxRequestLineSize
}
};
using (var server = CreateServer(options, testLogger)) using (var server = CreateServer(options, testLogger))
{ {
@ -121,10 +127,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public void StartWithMaxRequestBufferSizeLessThanMaxRequestHeadersTotalSizeThrows(long maxRequestBufferSize, int maxRequestHeadersTotalSize) public void StartWithMaxRequestBufferSizeLessThanMaxRequestHeadersTotalSizeThrows(long maxRequestBufferSize, int maxRequestHeadersTotalSize)
{ {
var testLogger = new TestApplicationErrorLogger { ThrowOnCriticalErrors = false }; var testLogger = new TestApplicationErrorLogger { ThrowOnCriticalErrors = false };
var options = new KestrelServerOptions(); var options = new KestrelServerOptions
options.Limits.MaxRequestBufferSize = maxRequestBufferSize; {
options.Limits.MaxRequestLineSize = (int)maxRequestBufferSize; Limits =
options.Limits.MaxRequestHeadersTotalSize = maxRequestHeadersTotalSize; {
MaxRequestBufferSize = maxRequestBufferSize,
MaxRequestLineSize = (int)maxRequestBufferSize,
MaxRequestHeadersTotalSize = maxRequestHeadersTotalSize
}
};
using (var server = CreateServer(options, testLogger)) using (var server = CreateServer(options, testLogger))
{ {
@ -146,7 +157,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
} }
[Fact] [Fact]
public void StartWithNoTrasnportFactoryThrows() public void StartWithNoTransportFactoryThrows()
{ {
var exception = Assert.Throws<ArgumentNullException>(() => var exception = Assert.Throws<ArgumentNullException>(() =>
new KestrelServer(Options.Create<KestrelServerOptions>(null), null, Mock.Of<ILoggerFactory>())); new KestrelServer(Options.Create<KestrelServerOptions>(null), null, Mock.Of<ILoggerFactory>()));

View File

@ -9,14 +9,12 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; 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.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Moq; using Moq;
using Xunit; using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk; using Xunit.Sdk;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
@ -30,7 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
@ -54,7 +52,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
@ -76,7 +74,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
@ -100,7 +98,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
@ -124,7 +122,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
@ -147,7 +145,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
@ -166,7 +164,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
@ -187,7 +185,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
@ -210,7 +208,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
@ -233,7 +231,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
@ -251,7 +249,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
@ -267,7 +265,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
@ -294,7 +292,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var ex = Assert.Throws<BadHttpRequestException>(() => 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(StatusCodes.Status400BadRequest, ex.StatusCode);
Assert.Equal(CoreStrings.FormatBadRequest_FinalTransferCodingNotChunked("chunked, not-chunked"), ex.Message); 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()) using (var input = new TestInput())
{ {
input.FrameContext.Method = method; input.Frame.Method = method;
var ex = Assert.Throws<BadHttpRequestException>(() => 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(StatusCodes.Status411LengthRequired, ex.StatusCode);
Assert.Equal(CoreStrings.FormatBadRequest_LengthRequired(method), ex.Message); Assert.Equal(CoreStrings.FormatBadRequest_LengthRequired(method), ex.Message);
@ -324,9 +322,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) using (var input = new TestInput())
{ {
input.FrameContext.Method = method; input.Frame.Method = method;
var ex = Assert.Throws<BadHttpRequestException>(() => 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(StatusCodes.Status400BadRequest, ex.StatusCode);
Assert.Equal(CoreStrings.FormatBadRequest_LengthRequiredHttp10(method), ex.Message); Assert.Equal(CoreStrings.FormatBadRequest_LengthRequiredHttp10(method), ex.Message);
@ -338,7 +336,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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"); input.Add("Hello");
using (var ms = new MemoryStream()) using (var ms = new MemoryStream())
@ -355,7 +354,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) 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"); input.Add("Hello");
await body.ConsumeAsync(); await body.ConsumeAsync();
@ -402,7 +402,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) 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); var copyToAsyncTask = body.CopyToAsync(mockDestination.Object);
// The block returned by IncomingStart always has at least 2048 available bytes, // 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()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
@ -463,15 +464,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
} }
[Fact] [Fact]
public async Task StartAsyncDoesNotReturnAfterCancelingInput() public async Task PumpAsyncDoesNotReturnAfterCancelingInput()
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); 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"); input.Add("a");
Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1)); Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1));
@ -485,15 +486,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
} }
[Fact] [Fact]
public async Task StartAsyncReturnsAfterCanceling() public async Task PumpAsyncReturnsAfterCanceling()
{ {
using (var input = new TestInput()) 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(); var stream = new FrameRequestStream();
stream.StartAcceptingReads(body); 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"); input.Add("a");
Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1)); 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) private void AssertASCII(string expected, ArraySegment<byte> actual)
{ {
var encoding = Encoding.ASCII; var encoding = Encoding.ASCII;

View File

@ -0,0 +1,61 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class MinimumDataRateTests
{
[Theory]
[InlineData(double.Epsilon)]
[InlineData(double.MaxValue)]
public void RateValid(double value)
{
Assert.Equal(value, new MinimumDataRate(rate: value, gracePeriod: TimeSpan.Zero).Rate);
}
[Theory]
[InlineData(double.MinValue)]
[InlineData(0)]
public void RateInvalid(double value)
{
var exception = Assert.Throws<ArgumentOutOfRangeException>(() => new MinimumDataRate(rate: value, gracePeriod: TimeSpan.Zero));
Assert.Equal("rate", exception.ParamName);
Assert.StartsWith(CoreStrings.PositiveNumberRequired, exception.Message);
}
[Theory]
[MemberData(nameof(GracePeriodValidData))]
public void GracePeriodValid(TimeSpan value)
{
Assert.Equal(value, new MinimumDataRate(rate: 1, gracePeriod: value).GracePeriod);
}
[Theory]
[MemberData(nameof(GracePeriodInvalidData))]
public void GracePeriodInvalid(TimeSpan value)
{
var exception = Assert.Throws<ArgumentOutOfRangeException>(() => new MinimumDataRate(rate: 1, gracePeriod: value));
Assert.Equal("gracePeriod", exception.ParamName);
Assert.StartsWith(CoreStrings.NonNegativeTimeSpanRequired, exception.Message);
}
public static TheoryData<TimeSpan> GracePeriodValidData => new TheoryData<TimeSpan>
{
TimeSpan.Zero,
TimeSpan.FromTicks(1),
TimeSpan.MaxValue
};
public static TheoryData<TimeSpan> GracePeriodInvalidData => new TheoryData<TimeSpan>
{
TimeSpan.MinValue,
TimeSpan.FromTicks(-1)
};
}
}

View File

@ -2,21 +2,16 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.IO;
using System.Text; 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.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines; using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
using Moq; using Moq;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
class TestInput : IFrameControl, IDisposable class TestInput : IDisposable
{ {
private MemoryPool _memoryPool; private MemoryPool _memoryPool;
private PipeFactory _pipelineFactory; private PipeFactory _pipelineFactory;
@ -27,23 +22,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_pipelineFactory = new PipeFactory(); _pipelineFactory = new PipeFactory();
Pipe = _pipelineFactory.Create(); Pipe = _pipelineFactory.Create();
FrameContext = new Frame<object>(null, new FrameContext FrameContext = new FrameContext
{ {
ServiceContext = new TestServiceContext(), ServiceContext = new TestServiceContext(),
Input = Pipe.Reader, Input = Pipe.Reader,
ConnectionInformation = new MockConnectionInformation ConnectionInformation = new MockConnectionInformation
{ {
PipeFactory = _pipelineFactory PipeFactory = _pipelineFactory
} },
}); TimeoutControl = Mock.Of<ITimeoutControl>()
FrameContext.FrameControl = this; };
Frame = new Frame<object>(null, FrameContext);
Frame.FrameControl = Mock.Of<IFrameControl>();
} }
public IPipe Pipe { get; } public IPipe Pipe { get; }
public PipeFactory PipeFactory => _pipelineFactory; public PipeFactory PipeFactory => _pipelineFactory;
public Frame FrameContext { get; set; } public FrameContext FrameContext { get; }
public Frame Frame { get; set; }
public void Add(string text) public void Add(string text)
{ {
@ -56,38 +56,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Pipe.Writer.Complete(); Pipe.Writer.Complete();
} }
public void ProduceContinue() public void Cancel()
{ {
} Pipe.Reader.CancelPendingRead();
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;
} }
public void Dispose() public void Dispose()

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.IO;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -96,7 +97,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
cts.CancelAfter(LongDelay); cts.CancelAfter(LongDelay);
await connection.Send( await connection.Send(
"POST / HTTP/1.1", "POST /consume HTTP/1.1",
"Host:", "Host:",
"Transfer-Encoding: chunked", "Transfer-Encoding: chunked",
"", "",
@ -193,7 +194,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
ServerOptions = ServerOptions =
{ {
AddServerHeader = false, 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) private async Task App(HttpContext httpContext, CancellationToken longRunningCt, CancellationToken upgradeCt)
{ {
var ct = httpContext.RequestAborted; var ct = httpContext.RequestAborted;
var responseStream = httpContext.Response.Body;
var responseBytes = Encoding.ASCII.GetBytes("hello, world");
if (httpContext.Request.Path == "/longrunning") if (httpContext.Request.Path == "/longrunning")
{ {
@ -208,8 +215,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{ {
await Task.Delay(1000); await Task.Delay(1000);
} }
await httpContext.Response.WriteAsync("hello, world");
} }
else if (httpContext.Request.Path == "/upgrade") else if (httpContext.Request.Path == "/upgrade")
{ {
@ -220,14 +225,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
await Task.Delay(LongDelay); await Task.Delay(LongDelay);
} }
var responseBytes = Encoding.ASCII.GetBytes("hello, world"); responseStream = stream;
await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
} }
} }
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) private async Task ReceiveResponse(TestConnection connection)

View File

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

View File

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

View File

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

View File

@ -59,9 +59,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
Assert.True(bufferLength % 256 == 0, $"{nameof(bufferLength)} must be evenly divisible by 256"); Assert.True(bufferLength % 256 == 0, $"{nameof(bufferLength)} must be evenly divisible by 256");
var builder = new WebHostBuilder() 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/") .UseUrls("http://127.0.0.1:0/")
.Configure(app => .Configure(app =>

View File

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

View File

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

View File

@ -2,24 +2,25 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Threading;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Testing namespace Microsoft.AspNetCore.Testing
{ {
public class MockSystemClock : ISystemClock public class MockSystemClock : ISystemClock
{ {
private DateTimeOffset _utcNow = DateTimeOffset.Now; private long _utcNowTicks = DateTimeOffset.UtcNow.Ticks;
public DateTimeOffset UtcNow public DateTimeOffset UtcNow
{ {
get get
{ {
UtcNowCalled++; UtcNowCalled++;
return _utcNow; return new DateTimeOffset(Interlocked.Read(ref _utcNowTicks), TimeSpan.Zero);
} }
set set
{ {
_utcNow = value; Interlocked.Exchange(ref _utcNowTicks, value.Ticks);
} }
} }

View File

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

View File

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