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