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

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

View File

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

View File

@ -327,4 +327,10 @@
<data name="MaxRequestBodySizeCannotBeModifiedForUpgradedRequests" xml:space="preserve">
<value>The maximum request body size cannot be modified after the request has been upgraded.</value>
</data>
<data name="PositiveTimeSpanRequired" xml:space="preserve">
<value>Value must be a positive TimeSpan.</value>
</data>
<data name="NonNegativeTimeSpanRequired" xml:space="preserve">
<value>Value must be a non-negative TimeSpan.</value>
</data>
</root>

View File

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

View File

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

View File

@ -29,6 +29,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private long _timeoutTimestamp = long.MaxValue;
private TimeoutAction _timeoutAction;
private object _readTimingLock = new object();
private bool _readTimingEnabled;
private bool _readTimingPauseRequested;
private long _readTimingElapsedTicks;
private long _readTimingBytesRead;
private Task _lifetimeTask;
public FrameConnection(FrameConnectionContext context)
@ -36,6 +42,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_context = context;
}
// For testing
internal Frame Frame => _frame;
public bool TimedOut { get; private set; }
public string ConnectionId => _context.ConnectionId;
public IPipeWriter Input => _context.Input.Writer;
public IPipeReader Output => _context.Output.Reader;
@ -91,15 +102,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
}
// _frame must be initialized before adding the connection to the connection manager
_frame = new Frame<TContext>(application, new FrameContext
{
ConnectionId = _context.ConnectionId,
ConnectionInformation = _context.ConnectionInformation,
ServiceContext = _context.ServiceContext,
TimeoutControl = this,
Input = input,
Output = output
});
CreateFrame(application, input, output);
// Do this before the first await so we don't yield control to the transport until we've
// added the connection to the connection manager
@ -140,9 +143,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
}
}
internal void CreateFrame<TContext>(IHttpApplication<TContext> application, IPipeReader input, IPipe output)
{
_frame = new Frame<TContext>(application, new FrameContext
{
ConnectionId = _context.ConnectionId,
ConnectionInformation = _context.ConnectionInformation,
ServiceContext = _context.ServiceContext,
TimeoutControl = this,
Input = input,
Output = output
});
}
public void OnConnectionClosed(Exception ex)
{
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
// Abort the connection (if not already aborted)
_frame.Abort(ex);
@ -152,7 +168,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public Task StopAsync()
{
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
_frame.Stop();
@ -161,7 +177,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public void Abort(Exception ex)
{
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
// Abort the connection (if not already aborted)
_frame.Abort(ex);
@ -169,7 +185,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public Task AbortAsync(Exception ex)
{
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
// Abort the connection (if not already aborted)
_frame.Abort(ex);
@ -177,16 +193,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
return _lifetimeTask;
}
public void Timeout()
public void SetTimeoutResponse()
{
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
_frame.SetBadRequestState(RequestRejectionReason.RequestTimeout);
}
public void Timeout()
{
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
TimedOut = true;
_readTimingEnabled = false;
_frame.Stop();
}
private async Task<Stream> ApplyConnectionAdaptersAsync()
{
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
var features = new FeatureCollection();
var connectionAdapters = _context.ConnectionAdapters;
@ -231,7 +256,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public void Tick(DateTimeOffset now)
{
Debug.Assert(_frame != null, $"nameof({_frame}) is null");
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
var timestamp = now.Ticks;
@ -242,10 +267,41 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
if (_timeoutAction == TimeoutAction.SendTimeoutResponse)
{
Timeout();
SetTimeoutResponse();
}
_frame.Stop();
Timeout();
}
else
{
lock (_readTimingLock)
{
if (_readTimingEnabled)
{
_readTimingElapsedTicks += timestamp - _lastTimestamp;
if (_frame.RequestBodyMinimumDataRate?.Rate > 0 && _readTimingElapsedTicks > _frame.RequestBodyMinimumDataRate.GracePeriod.Ticks)
{
var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond;
var rate = Interlocked.Read(ref _readTimingBytesRead) / elapsedSeconds;
if (rate < _frame.RequestBodyMinimumDataRate.Rate)
{
Log.RequestBodyMininumDataRateNotSatisfied(_context.ConnectionId, _frame.TraceIdentifier, _frame.RequestBodyMinimumDataRate.Rate);
Timeout();
}
}
// PauseTimingReads() cannot just set _timingReads to false. It needs to go through at least one tick
// before pausing, otherwise _readTimingElapsed might never be updated if PauseTimingReads() is always
// called before the next tick.
if (_readTimingPauseRequested)
{
_readTimingEnabled = false;
_readTimingPauseRequested = false;
}
}
}
}
Interlocked.Exchange(ref _lastTimestamp, timestamp);
@ -275,5 +331,47 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
Interlocked.Exchange(ref _timeoutTimestamp, _lastTimestamp + ticks + Heartbeat.Interval.Ticks);
}
public void StartTimingReads()
{
lock (_readTimingLock)
{
_readTimingElapsedTicks = 0;
_readTimingBytesRead = 0;
_readTimingEnabled = true;
}
}
public void StopTimingReads()
{
lock (_readTimingLock)
{
_readTimingEnabled = false;
}
}
public void PauseTimingReads()
{
lock (_readTimingLock)
{
_readTimingPauseRequested = true;
}
}
public void ResumeTimingReads()
{
lock (_readTimingLock)
{
_readTimingEnabled = true;
// In case pause and resume were both called between ticks
_readTimingPauseRequested = false;
}
}
public void BytesRead(int count)
{
Interlocked.Add(ref _readTimingBytesRead, count);
}
}
}

View File

@ -10,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
@ -21,7 +22,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
IHttpConnectionFeature,
IHttpRequestLifetimeFeature,
IHttpRequestIdentifierFeature,
IHttpMaxRequestBodySizeFeature
IHttpMaxRequestBodySizeFeature,
IHttpRequestBodyMinimumDataRateFeature
{
// NOTE: When feature interfaces are added to or removed from this Frame class implementation,
// then the list of `implementedFeatures` in the generated code project MUST also be updated.
@ -227,6 +229,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
MinimumDataRate IHttpRequestBodyMinimumDataRateFeature.MinimumDataRate
{
get => RequestBodyMinimumDataRate;
set => RequestBodyMinimumDataRate = value;
}
object IFeatureCollection.this[Type key]
{
get => FastFeatureGet(key);

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
@ -16,6 +17,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly MessageBody _zeroContentLengthKeepAlive = new ForZeroContentLength(keepAlive: true);
private readonly Frame _context;
private bool _send100Continue = true;
private volatile bool _canceled;
@ -32,22 +34,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public virtual bool IsEmpty => false;
private IKestrelTrace Log => _context.ServiceContext.Log;
private async Task PumpAsync()
{
Exception error = null;
try
{
var awaitable = _context.Input.ReadAsync();
if (!awaitable.IsCompleted)
{
TryProduceContinue();
}
TryStartTimingReads();
while (true)
{
var awaitable = _context.Input.ReadAsync();
var result = await awaitable;
if (!awaitable.IsCompleted)
if (_context.TimeoutControl.TimedOut)
{
TryProduceContinue();
_context.ThrowRequestRejected(RequestRejectionReason.RequestTimeout);
}
var result = await awaitable;
var readableBuffer = result.Buffer;
var consumed = readableBuffer.Start;
var examined = readableBuffer.End;
@ -73,7 +85,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
writableBuffer.Commit();
}
await writableBuffer.FlushAsync();
var writeAwaitable = writableBuffer.FlushAsync();
var backpressure = false;
if (!writeAwaitable.IsCompleted)
{
// Backpressure, stop controlling incoming data rate until data is read.
backpressure = true;
_context.TimeoutControl.PauseTimingReads();
}
await writeAwaitable;
if (backpressure)
{
_context.TimeoutControl.ResumeTimingReads();
}
if (done)
{
@ -84,6 +111,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_context.ThrowRequestRejected(RequestRejectionReason.UnexpectedEndOfRequestContent);
}
awaitable = _context.Input.ReadAsync();
}
finally
{
@ -98,6 +127,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
finally
{
_context.RequestBodyPipe.Writer.Complete(error);
TryStopTimingReads();
}
}
@ -191,6 +221,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected void Copy(ReadableBuffer readableBuffer, WritableBuffer writableBuffer)
{
_context.TimeoutControl.BytesRead(readableBuffer.Length);
if (readableBuffer.IsSingleSpan)
{
writableBuffer.Write(readableBuffer.First.Span);
@ -232,6 +264,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
}
private void TryStartTimingReads()
{
if (!RequestUpgrade)
{
Log.RequestBodyStart(_context.ConnectionIdFeature, _context.TraceIdentifier);
_context.TimeoutControl.StartTimingReads();
}
}
private void TryStopTimingReads()
{
if (!RequestUpgrade)
{
Log.RequestBodyDone(_context.ConnectionIdFeature, _context.TraceIdentifier);
_context.TimeoutControl.StopTimingReads();
}
}
public static MessageBody For(
HttpVersion httpVersion,
FrameRequestHeaders headers,

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
internal static class Constants
@ -28,5 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
public const string SocketDescriptorPrefix = "sockfd:";
public const string ServerName = "Kestrel";
public static readonly TimeSpan RequestBodyDrainTimeout = TimeSpan.FromSeconds(5);
}
}

View File

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

View File

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

View File

@ -7,9 +7,6 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
/// <summary>
/// Summary description for KestrelTrace
/// </summary>
public class KestrelTrace : IKestrelTrace
{
private static readonly Action<ILogger, string, Exception> _connectionStart =
@ -57,6 +54,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private static readonly Action<ILogger, string, Exception> _connectionRejected =
LoggerMessage.Define<string>(LogLevel.Warning, 24, @"Connection id ""{ConnectionId}"" rejected because the maximum number of concurrent connections has been reached.");
private static readonly Action<ILogger, string, string, Exception> _requestBodyStart =
LoggerMessage.Define<string, string>(LogLevel.Debug, 25, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": started reading request body.");
private static readonly Action<ILogger, string, string, Exception> _requestBodyDone =
LoggerMessage.Define<string, string>(LogLevel.Debug, 26, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": done reading request body.");
private static readonly Action<ILogger, string, string, double, Exception> _requestBodyMinimumDataRateNotSatisfied =
LoggerMessage.Define<string, string, double>(LogLevel.Information, 27, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": request body incoming data rate dropped below {Rate} bytes/second.");
protected readonly ILogger _logger;
public KestrelTrace(ILogger logger)
@ -139,6 +145,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_applicationNeverCompleted(_logger, connectionId, null);
}
public virtual void RequestBodyStart(string connectionId, string traceIdentifier)
{
_requestBodyStart(_logger, connectionId, traceIdentifier, null);
}
public virtual void RequestBodyDone(string connectionId, string traceIdentifier)
{
_requestBodyDone(_logger, connectionId, traceIdentifier, null);
}
public void RequestBodyMininumDataRateNotSatisfied(string connectionId, string traceIdentifier, double rate)
{
_requestBodyMinimumDataRateNotSatisfied(_logger, connectionId, traceIdentifier, rate, null);
}
public virtual void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
=> _logger.Log(logLevel, eventId, state, exception, formatter);

View File

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

View File

@ -977,7 +977,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
=> GetString("MaxRequestBodySizeCannotBeModifiedAfterRead");
/// <summary>
/// The maximum request body size cannot be modified after the request has be upgraded.
/// The maximum request body size cannot be modified after the request has been upgraded.
/// </summary>
internal static string MaxRequestBodySizeCannotBeModifiedForUpgradedRequests
{
@ -985,11 +985,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
}
/// <summary>
/// The maximum request body size cannot be modified after the request has be upgraded.
/// The maximum request body size cannot be modified after the request has been upgraded.
/// </summary>
internal static string FormatMaxRequestBodySizeCannotBeModifiedForUpgradedRequests()
=> GetString("MaxRequestBodySizeCannotBeModifiedForUpgradedRequests");
/// <summary>
/// Value must be a positive TimeSpan.
/// </summary>
internal static string PositiveTimeSpanRequired
{
get => GetString("PositiveTimeSpanRequired");
}
/// <summary>
/// Value must be a positive TimeSpan.
/// </summary>
internal static string FormatPositiveTimeSpanRequired()
=> GetString("PositiveTimeSpanRequired");
/// <summary>
/// Value must be a non-negative TimeSpan.
/// </summary>
internal static string NonNegativeTimeSpanRequired
{
get => GetString("NonNegativeTimeSpanRequired");
}
/// <summary>
/// Value must be a non-negative TimeSpan.
/// </summary>
internal static string FormatNonNegativeTimeSpanRequired()
=> GetString("NonNegativeTimeSpanRequired");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

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

View File

@ -12,6 +12,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
@ -140,6 +141,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.NotEqual(nextId, secondId);
}
[Fact]
public void ResetResetsRequestBodyMinimumDataRate()
{
_frame.RequestBodyMinimumDataRate = new MinimumDataRate(rate: 1, gracePeriod: TimeSpan.Zero);
_frame.Reset();
Assert.Equal(_serviceContext.ServerOptions.Limits.RequestBodyMinimumDataRate, _frame.RequestBodyMinimumDataRate);
}
[Fact]
public void TraceIdentifierCountsRequestsPerFrame()
{
@ -243,6 +254,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Throws<InvalidOperationException>(() => ((IHttpResponseFeature)_frame).OnStarting(_ => TaskCache.CompletedTask, null));
}
[Theory]
[MemberData(nameof(RequestBodyMinimumDataRateData))]
public void ConfiguringRequestBodyMinimumDataRateFeatureSetsRequestBodyMinimumDateRate(MinimumDataRate minimumDataRate)
{
((IFeatureCollection)_frame).Get<IHttpRequestBodyMinimumDataRateFeature>().MinimumDataRate = minimumDataRate;
Assert.Same(minimumDataRate, _frame.RequestBodyMinimumDataRate);
}
[Fact]
public void ResetResetsRequestHeaders()
{
@ -844,6 +864,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
}
public static TheoryData<TimeSpan> RequestBodyTimeoutDataValid => new TheoryData<TimeSpan>
{
TimeSpan.FromTicks(1),
TimeSpan.MaxValue,
Timeout.InfiniteTimeSpan,
TimeSpan.FromMilliseconds(-1) // Same as Timeout.InfiniteTimeSpan
};
public static TheoryData<TimeSpan> RequestBodyTimeoutDataInvalid => new TheoryData<TimeSpan>
{
TimeSpan.MinValue,
TimeSpan.FromTicks(-1),
TimeSpan.Zero
};
public static TheoryData<MinimumDataRate> RequestBodyMinimumDataRateData => new TheoryData<MinimumDataRate>
{
null,
new MinimumDataRate(rate: 1, gracePeriod: TimeSpan.Zero)
};
private class RequestHeadersWrapper : IHeaderDictionary
{
IHeaderDictionary _innerHeaders;

View File

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

View File

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

View File

@ -9,14 +9,12 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
@ -30,7 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.FrameContext);
var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@ -54,7 +52,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.FrameContext);
var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@ -76,7 +74,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.FrameContext);
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@ -100,7 +98,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.FrameContext);
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@ -124,7 +122,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.FrameContext);
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@ -147,7 +145,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.FrameContext);
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@ -166,7 +164,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.FrameContext);
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@ -187,7 +185,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.FrameContext);
var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@ -210,7 +208,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.FrameContext);
var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@ -233,7 +231,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.FrameContext);
var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@ -251,7 +249,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.FrameContext);
var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@ -267,7 +265,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "8197" }, input.FrameContext);
var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "8197" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@ -294,7 +292,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput())
{
var ex = Assert.Throws<BadHttpRequestException>(() =>
MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked, not-chunked" }, input.FrameContext));
MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked, not-chunked" }, input.Frame));
Assert.Equal(StatusCodes.Status400BadRequest, ex.StatusCode);
Assert.Equal(CoreStrings.FormatBadRequest_FinalTransferCodingNotChunked("chunked, not-chunked"), ex.Message);
@ -308,9 +306,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
input.FrameContext.Method = method;
input.Frame.Method = method;
var ex = Assert.Throws<BadHttpRequestException>(() =>
MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders(), input.FrameContext));
MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders(), input.Frame));
Assert.Equal(StatusCodes.Status411LengthRequired, ex.StatusCode);
Assert.Equal(CoreStrings.FormatBadRequest_LengthRequired(method), ex.Message);
@ -324,9 +322,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
input.FrameContext.Method = method;
input.Frame.Method = method;
var ex = Assert.Throws<BadHttpRequestException>(() =>
MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders(), input.FrameContext));
MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders(), input.Frame));
Assert.Equal(StatusCodes.Status400BadRequest, ex.StatusCode);
Assert.Equal(CoreStrings.FormatBadRequest_LengthRequiredHttp10(method), ex.Message);
@ -338,7 +336,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "5" }, input.FrameContext);
var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
input.Add("Hello");
using (var ms = new MemoryStream())
@ -355,7 +354,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "5" }, input.FrameContext);
var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
input.Add("Hello");
await body.ConsumeAsync();
@ -402,7 +402,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput())
{
var body = MessageBody.For(HttpVersion.Http11, headers, input.FrameContext);
var body = MessageBody.For(HttpVersion.Http11, headers, input.Frame);
var copyToAsyncTask = body.CopyToAsync(mockDestination.Object);
// The block returned by IncomingStart always has at least 2048 available bytes,
@ -448,7 +449,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
using (var input = new TestInput())
{
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderConnection = headerConnection }, input.FrameContext);
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderConnection = headerConnection }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
@ -463,15 +464,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
[Fact]
public async Task StartAsyncDoesNotReturnAfterCancelingInput()
public async Task PumpAsyncDoesNotReturnAfterCancelingInput()
{
using (var input = new TestInput())
{
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.FrameContext);
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
// Add some input and consume it to ensure StartAsync is in the loop
// Add some input and consume it to ensure PumpAsync is running
input.Add("a");
Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1));
@ -485,15 +486,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
[Fact]
public async Task StartAsyncReturnsAfterCanceling()
public async Task PumpAsyncReturnsAfterCanceling()
{
using (var input = new TestInput())
{
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.FrameContext);
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
// Add some input and consume it to ensure StartAsync is in the loop
// Add some input and consume it to ensure PumpAsync is running
input.Add("a");
Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1));
@ -511,6 +512,198 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
}
[Fact]
public async Task ReadAsyncThrowsOnTimeout()
{
using (var input = new TestInput())
{
var mockTimeoutControl = new Mock<ITimeoutControl>();
input.FrameContext.TimeoutControl = mockTimeoutControl.Object;
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
// Add some input and read it to start PumpAsync
input.Add("a");
Assert.Equal(1, await body.ReadAsync(new ArraySegment<byte>(new byte[1])));
// Time out on the next read
mockTimeoutControl
.Setup(timeoutControl => timeoutControl.TimedOut)
.Returns(true);
input.Cancel();
var exception = await Assert.ThrowsAsync<BadHttpRequestException>(() => body.ReadAsync(new ArraySegment<byte>(new byte[1])));
Assert.Equal(StatusCodes.Status408RequestTimeout, exception.StatusCode);
}
}
[Fact]
public async Task ConsumeAsyncThrowsOnTimeout()
{
using (var input = new TestInput())
{
var mockTimeoutControl = new Mock<ITimeoutControl>();
input.FrameContext.TimeoutControl = mockTimeoutControl.Object;
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
// Add some input and read it to start PumpAsync
input.Add("a");
Assert.Equal(1, await body.ReadAsync(new ArraySegment<byte>(new byte[1])));
// Time out on the next read
mockTimeoutControl
.Setup(timeoutControl => timeoutControl.TimedOut)
.Returns(true);
input.Cancel();
var exception = await Assert.ThrowsAsync<BadHttpRequestException>(() => body.ConsumeAsync());
Assert.Equal(StatusCodes.Status408RequestTimeout, exception.StatusCode);
}
}
[Fact]
public async Task CopyToAsyncThrowsOnTimeout()
{
using (var input = new TestInput())
{
var mockTimeoutControl = new Mock<ITimeoutControl>();
input.FrameContext.TimeoutControl = mockTimeoutControl.Object;
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
// Add some input and read it to start PumpAsync
input.Add("a");
Assert.Equal(1, await body.ReadAsync(new ArraySegment<byte>(new byte[1])));
// Time out on the next read
mockTimeoutControl
.Setup(timeoutControl => timeoutControl.TimedOut)
.Returns(true);
input.Cancel();
using (var ms = new MemoryStream())
{
var exception = await Assert.ThrowsAsync<BadHttpRequestException>(() => body.CopyToAsync(ms));
Assert.Equal(StatusCodes.Status408RequestTimeout, exception.StatusCode);
}
}
}
[Fact]
public async Task LogsWhenStartsReadingRequestBody()
{
using (var input = new TestInput())
{
var mockLogger = new Mock<IKestrelTrace>();
input.Frame.ServiceContext.Log = mockLogger.Object;
input.Frame.ConnectionIdFeature = "ConnectionId";
input.Frame.TraceIdentifier = "RequestId";
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
// Add some input and consume it to ensure PumpAsync is running
input.Add("a");
Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1));
mockLogger.Verify(logger => logger.RequestBodyStart("ConnectionId", "RequestId"));
input.Fin();
}
}
[Fact]
public async Task LogsWhenStopsReadingRequestBody()
{
using (var input = new TestInput())
{
var logEvent = new ManualResetEventSlim();
var mockLogger = new Mock<IKestrelTrace>();
mockLogger
.Setup(logger => logger.RequestBodyDone("ConnectionId", "RequestId"))
.Callback(() => logEvent.Set());
input.Frame.ServiceContext.Log = mockLogger.Object;
input.Frame.ConnectionIdFeature = "ConnectionId";
input.Frame.TraceIdentifier = "RequestId";
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
// Add some input and consume it to ensure PumpAsync is running
input.Add("a");
Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1));
input.Fin();
Assert.True(logEvent.Wait(TimeSpan.FromSeconds(10)));
}
}
[Fact]
public async Task OnlyEnforcesRequestBodyTimeoutAfterSending100Continue()
{
using (var input = new TestInput())
{
var produceContinueCalled = false;
var startTimingReadsCalledAfterProduceContinue = false;
var mockFrameControl = new Mock<IFrameControl>();
mockFrameControl
.Setup(frameControl => frameControl.ProduceContinue())
.Callback(() => produceContinueCalled = true);
input.Frame.FrameControl = mockFrameControl.Object;
var mockTimeoutControl = new Mock<ITimeoutControl>();
mockTimeoutControl
.Setup(timeoutControl => timeoutControl.StartTimingReads())
.Callback(() => startTimingReadsCalledAfterProduceContinue = produceContinueCalled);
input.FrameContext.TimeoutControl = mockTimeoutControl.Object;
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
// Add some input and read it to start PumpAsync
var readTask = body.ReadAsync(new ArraySegment<byte>(new byte[1]));
Assert.True(startTimingReadsCalledAfterProduceContinue);
input.Add("a");
await readTask;
}
}
[Fact]
public async Task DoesNotEnforceRequestBodyTimeoutOnUpgradeRequests()
{
using (var input = new TestInput())
{
var mockTimeoutControl = new Mock<ITimeoutControl>();
input.FrameContext.TimeoutControl = mockTimeoutControl.Object;
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.Frame);
// Add some input and read it to start PumpAsync
input.Add("a");
Assert.Equal(1, await body.ReadAsync(new ArraySegment<byte>(new byte[1])));
input.Fin();
await Assert.ThrowsAsync<BadHttpRequestException>(async () => await body.ReadAsync(new ArraySegment<byte>(new byte[1])));
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StartTimingReads(), Times.Never);
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StopTimingReads(), Times.Never);
}
}
private void AssertASCII(string expected, ArraySegment<byte> actual)
{
var encoding = Encoding.ASCII;

View File

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

View File

@ -2,21 +2,16 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
using Moq;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
class TestInput : IFrameControl, IDisposable
class TestInput : IDisposable
{
private MemoryPool _memoryPool;
private PipeFactory _pipelineFactory;
@ -27,23 +22,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_pipelineFactory = new PipeFactory();
Pipe = _pipelineFactory.Create();
FrameContext = new Frame<object>(null, new FrameContext
FrameContext = new FrameContext
{
ServiceContext = new TestServiceContext(),
Input = Pipe.Reader,
ConnectionInformation = new MockConnectionInformation
{
PipeFactory = _pipelineFactory
}
});
FrameContext.FrameControl = this;
},
TimeoutControl = Mock.Of<ITimeoutControl>()
};
Frame = new Frame<object>(null, FrameContext);
Frame.FrameControl = Mock.Of<IFrameControl>();
}
public IPipe Pipe { get; }
public PipeFactory PipeFactory => _pipelineFactory;
public Frame FrameContext { get; set; }
public FrameContext FrameContext { get; }
public Frame Frame { get; set; }
public void Add(string text)
{
@ -56,38 +56,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Pipe.Writer.Complete();
}
public void ProduceContinue()
public void Cancel()
{
}
public void Pause()
{
}
public void Resume()
{
}
public void End(ProduceEndType endType)
{
}
public void Abort()
{
}
public void Write(ArraySegment<byte> data, Action<Exception, object> callback, object state)
{
}
Task IFrameControl.WriteAsync(ArraySegment<byte> data, CancellationToken cancellationToken)
{
return TaskCache.CompletedTask;
}
Task IFrameControl.FlushAsync(CancellationToken cancellationToken)
{
return TaskCache.CompletedTask;
Pipe.Reader.CancelPendingRead();
}
public void Dispose()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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