diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/BadHttpRequestException.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/BadHttpRequestException.cs index 034465d212..e778f76641 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/BadHttpRequestException.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/BadHttpRequestException.cs @@ -71,6 +71,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core case RequestRejectionReason.TooManyHeaders: ex = new BadHttpRequestException(CoreStrings.BadRequest_TooManyHeaders, StatusCodes.Status431RequestHeaderFieldsTooLarge); break; + case RequestRejectionReason.RequestBodyTooLarge: + ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestBodyTooLarge, StatusCodes.Status413PayloadTooLarge); + break; case RequestRejectionReason.RequestTimeout: ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestTimeout, StatusCodes.Status408RequestTimeout); break; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx b/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx index f2f873fe3b..4ccc38f2c0 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx @@ -318,4 +318,13 @@ IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection. - + + Request body too large. + + + The maximum request body size cannot be modified after the app has already started reading from the request body. + + + The maximum request body size cannot be modified after the request has been upgraded. + + \ No newline at end of file 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 6ec4f45348..3358811e93 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 @@ -20,7 +20,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http IHttpUpgradeFeature, IHttpConnectionFeature, IHttpRequestLifetimeFeature, - IHttpRequestIdentifierFeature + IHttpRequestIdentifierFeature, + IHttpMaxRequestBodySizeFeature { // 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. @@ -202,6 +203,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http set => TraceIdentifier = value; } + bool IHttpMaxRequestBodySizeFeature.IsReadOnly => HasStartedConsumingRequestBody || _wasUpgraded; + + long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize + { + get => MaxRequestBodySize; + set + { + if (HasStartedConsumingRequestBody) + { + throw new InvalidOperationException(CoreStrings.MaxRequestBodySizeCannotBeModifiedAfterRead); + } + if (_wasUpgraded) + { + throw new InvalidOperationException(CoreStrings.MaxRequestBodySizeCannotBeModifiedForUpgradedRequests); + } + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.NonNegativeNumberOrNullRequired); + } + + MaxRequestBodySize = 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 28b0be9f78..3a17316ade 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 @@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private static readonly Type ITlsConnectionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature); 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 IHttpSendFileFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature); private object _currentIHttpRequestFeature; @@ -40,6 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private object _currentITlsConnectionFeature; private object _currentIHttpWebSocketFeature; private object _currentISessionFeature; + private object _currentIHttpMaxRequestBodySizeFeature; private object _currentIHttpSendFileFeature; private void FastReset() @@ -50,6 +52,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _currentIHttpRequestIdentifierFeature = this; _currentIHttpRequestLifetimeFeature = this; _currentIHttpConnectionFeature = this; + _currentIHttpMaxRequestBodySizeFeature = this; _currentIServiceProvidersFeature = null; _currentIHttpAuthenticationFeature = null; @@ -125,6 +128,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return _currentISessionFeature; } + if (key == IHttpMaxRequestBodySizeFeatureType) + { + return _currentIHttpMaxRequestBodySizeFeature; + } if (key == IHttpSendFileFeatureType) { return _currentIHttpSendFileFeature; @@ -211,6 +218,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _currentISessionFeature = feature; return; } + if (key == IHttpMaxRequestBodySizeFeatureType) + { + _currentIHttpMaxRequestBodySizeFeature = feature; + return; + } if (key == IHttpSendFileFeatureType) { _currentIHttpSendFileFeature = feature; @@ -281,6 +293,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { yield return new KeyValuePair(ISessionFeatureType, _currentISessionFeature as global::Microsoft.AspNetCore.Http.Features.ISessionFeature); } + if (_currentIHttpMaxRequestBodySizeFeature != null) + { + yield return new KeyValuePair(IHttpMaxRequestBodySizeFeatureType, _currentIHttpMaxRequestBodySizeFeature as global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature); + } 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 374b9e6518..6919a10ccb 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs @@ -121,6 +121,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http protected string ConnectionId => _frameContext.ConnectionId; public string ConnectionIdFeature { get; set; } + public bool HasStartedConsumingRequestBody { get; set; } + public long? MaxRequestBodySize { get; set; } /// /// The request id. @@ -310,8 +312,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public void PauseStreams() => _frameStreams.Pause(); - public void ResumeStreams() => _frameStreams.Resume(); - public void StopStreams() => _frameStreams.Stop(); public void Reset() @@ -326,6 +326,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http ResetFeatureCollection(); + HasStartedConsumingRequestBody = false; + MaxRequestBodySize = ServerOptions.Limits.MaxRequestBodySize; TraceIdentifier = null; Scheme = null; Method = null; @@ -368,7 +370,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _manuallySetRequestAbortToken = null; _abortedCts = null; - // Allow to bytes for \r\n after headers + // Allow two bytes for \r\n after headers _remainingRequestHeadersBytesAllowed = ServerOptions.Limits.MaxRequestHeadersTotalSize + 2; _requestHeadersParsed = 0; @@ -949,7 +951,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http var result = _parser.ParseRequestLine(new FrameAdapter(this), buffer, out consumed, out examined); if (!result && overLength) { - RejectRequest(RequestRejectionReason.RequestLineTooLong); + ThrowRequestRejected(RequestRejectionReason.RequestLineTooLong); } return result; @@ -973,7 +975,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (!result && overLength) { - RejectRequest(RequestRejectionReason.HeadersExceedMaxTotalSize); + ThrowRequestRejected(RequestRejectionReason.HeadersExceedMaxTotalSize); } if (result) { @@ -1059,13 +1061,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http throw new ObjectDisposedException(CoreStrings.UnhandledApplicationException, _applicationException); } - public void RejectRequest(RequestRejectionReason reason) + public void ThrowRequestRejected(RequestRejectionReason reason) => throw BadHttpRequestException.GetException(reason); - public void RejectRequest(RequestRejectionReason reason, string detail) + public void ThrowRequestRejected(RequestRejectionReason reason, string detail) => throw BadHttpRequestException.GetException(reason, detail); - private void RejectRequestTarget(Span target) + private void ThrowRequestTargetRejected(Span target) => throw GetInvalidRequestTargetException(target); private BadHttpRequestException GetInvalidRequestTargetException(Span target) @@ -1207,7 +1209,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } catch (InvalidOperationException) { - RejectRequestTarget(target); + ThrowRequestTargetRejected(target); } QueryString = query.GetAsciiStringNonNullCharacters(); @@ -1226,7 +1228,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http var ch = target[i]; if (!UriUtilities.IsValidAuthorityCharacter(ch)) { - RejectRequestTarget(target); + ThrowRequestTargetRejected(target); } } @@ -1234,7 +1236,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // requests (https://tools.ietf.org/html/rfc7231#section-4.3.6). if (method != HttpMethod.Connect) { - RejectRequest(RequestRejectionReason.ConnectMethodRequired); + ThrowRequestRejected(RequestRejectionReason.ConnectMethodRequired); } // When making a CONNECT request to establish a tunnel through one or @@ -1259,7 +1261,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // OPTIONS request (https://tools.ietf.org/html/rfc7231#section-4.3.7). if (method != HttpMethod.Options) { - RejectRequest(RequestRejectionReason.OptionsMethodRequired); + ThrowRequestRejected(RequestRejectionReason.OptionsMethodRequired); } RawTarget = Asterisk; @@ -1288,7 +1290,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (!Uri.TryCreate(RawTarget, UriKind.Absolute, out var uri)) { - RejectRequestTarget(target); + ThrowRequestTargetRejected(target); } _absoluteRequestTarget = uri; @@ -1312,7 +1314,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _requestHeadersParsed++; if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount) { - RejectRequest(RequestRejectionReason.TooManyHeaders); + ThrowRequestRejected(RequestRejectionReason.TooManyHeaders); } var valueString = value.GetAsciiStringNonNullCharacters(); @@ -1335,17 +1337,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http var host = FrameRequestHeaders.HeaderHost; if (host.Count <= 0) { - RejectRequest(RequestRejectionReason.MissingHostHeader); + ThrowRequestRejected(RequestRejectionReason.MissingHostHeader); } else if (host.Count > 1) { - RejectRequest(RequestRejectionReason.MultipleHostHeaders); + ThrowRequestRejected(RequestRejectionReason.MultipleHostHeaders); } else if (_requestTargetForm == HttpRequestTarget.AuthorityForm) { if (!host.Equals(RawTarget)) { - RejectRequest(RequestRejectionReason.InvalidHostHeader, host.ToString()); + ThrowRequestRejected(RequestRejectionReason.InvalidHostHeader, host.ToString()); } } else if (_requestTargetForm == HttpRequestTarget.AbsoluteForm) @@ -1361,7 +1363,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if ((host != _absoluteRequestTarget.Authority || !_absoluteRequestTarget.IsDefaultPort) && host != authorityAndPort) { - RejectRequest(RequestRejectionReason.InvalidHostHeader, host.ToString()); + ThrowRequestRejected(RequestRejectionReason.InvalidHostHeader, host.ToString()); } } } @@ -1370,9 +1372,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http => ConnectionInformation.PipeFactory.Create(new PipeOptions { ReaderScheduler = ServiceContext.ThreadPool, - WriterScheduler = ConnectionInformation.InputWriterScheduler, - MaximumSizeHigh = ServiceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0, - MaximumSizeLow = ServiceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0 + WriterScheduler = InlineScheduler.Default, + MaximumSizeHigh = 1, + MaximumSizeLow = 1 }); private enum HttpRequestTarget 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 769cd8e9db..c660d0b60c 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameOfT.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameOfT.cs @@ -93,8 +93,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http InitializeStreams(messageBody); - var messageBodyTask = messageBody.StartAsync(); - var context = _application.CreateContext(this); try { @@ -142,8 +140,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // If _requestAbort is set, the connection has already been closed. if (Volatile.Read(ref _requestAborted) == 0) { - ResumeStreams(); - if (HasResponseStarted) { // If the response has already started, call ProduceEnd() before @@ -158,21 +154,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http await ProduceEnd(); } - if (!_keepAlive) + // ForZeroContentLength does not complete the reader nor the writer + if (!messageBody.IsEmpty) { - messageBody.Cancel(); + if (_keepAlive) + { + // Finish reading the request body in case the app did not. + await messageBody.ConsumeAsync(); + // At this point both the request body pipe reader and writer should be completed. + RequestBodyPipe.Reset(); + } + else + { + RequestBodyPipe.Reader.Complete(); + messageBody.Cancel(); + Input.CancelPendingRead(); + } } - // An upgraded request has no defined request body length. - // Cancel any pending read so the read loop ends. - if (_upgradeAvailable) - { - Input.CancelPendingRead(); - } - - // Finish reading the request body in case the app did not. - await messageBody.ConsumeAsync(); - if (!HasResponseStarted) { await ProduceEnd(); @@ -200,15 +199,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // to ensure InitializeStreams has been called. StopStreams(); } - - // At this point both the request body pipe reader and writer should be completed. - await messageBodyTask; - - // ForZeroContentLength does not complete the reader nor the writer - if (_keepAlive && !messageBody.IsEmpty) - { - RequestBodyPipe.Reset(); - } } if (!_keepAlive) diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameRequestStream.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameRequestStream.cs index be5f9fd66f..ce518d17d8 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameRequestStream.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameRequestStream.cs @@ -169,14 +169,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _state = FrameStreamState.Closed; } - public void ResumeAcceptingReads() - { - if (_state == FrameStreamState.Closed) - { - _state = FrameStreamState.Open; - } - } - public void StopAcceptingReads() { // Can't use dispose (or close) as can be disposed too early by user code diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameResponseStream.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameResponseStream.cs index 062be560c2..3a76f4bf1b 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameResponseStream.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameResponseStream.cs @@ -123,14 +123,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _state = FrameStreamState.Closed; } - public void ResumeAcceptingWrites() - { - if (_state == FrameStreamState.Closed) - { - _state = FrameStreamState.Open; - } - } - public void StopAcceptingWrites() { // Can't use dispose (or close) as can be disposed too early by user code 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 6ab87c8096..bc6e3c0f8a 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/MessageBody.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/MessageBody.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines; -using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { @@ -33,7 +32,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public virtual bool IsEmpty => false; - public virtual async Task StartAsync() + private async Task PumpAsync() { Exception error = null; @@ -83,7 +82,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } else if (result.IsCompleted) { - _context.RejectRequest(RequestRejectionReason.UnexpectedEndOfRequestContent); + _context.ThrowRequestRejected(RequestRejectionReason.UnexpectedEndOfRequestContent); } } finally @@ -109,6 +108,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public virtual async Task ReadAsync(ArraySegment buffer, CancellationToken cancellationToken = default(CancellationToken)) { + TryInit(); + while (true) { var result = await _context.RequestBodyPipe.Reader.ReadAsync(); @@ -139,6 +140,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public virtual async Task CopyToAsync(Stream destination, CancellationToken cancellationToken = default(CancellationToken)) { + TryInit(); + while (true) { var result = await _context.RequestBodyPipe.Reader.ReadAsync(); @@ -169,7 +172,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public virtual async Task ConsumeAsync(CancellationToken cancellationToken = default(CancellationToken)) { - Exception error = null; + TryInit(); try { @@ -180,14 +183,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _context.RequestBodyPipe.Reader.Advance(result.Buffer.End); } while (!result.IsCompleted); } - catch (Exception ex) - { - error = ex; - throw; - } finally { - _context.RequestBodyPipe.Reader.Complete(error); + _context.RequestBodyPipe.Reader.Complete(); } } @@ -215,7 +213,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } - protected abstract bool Read(ReadableBuffer readableBuffer, WritableBuffer writableBuffer, out ReadCursor consumed, out ReadCursor examined); + private void TryInit() + { + if (!_context.HasStartedConsumingRequestBody) + { + OnReadStart(); + _context.HasStartedConsumingRequestBody = true; + _ = PumpAsync(); + } + } + + protected virtual bool Read(ReadableBuffer readableBuffer, WritableBuffer writableBuffer, out ReadCursor consumed, out ReadCursor examined) + { + throw new NotImplementedException(); + } + + protected virtual void OnReadStart() + { + } public static MessageBody For( HttpVersion httpVersion, @@ -248,15 +263,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // status code and then close the connection. if (transferCoding != TransferCoding.Chunked) { - context.RejectRequest(RequestRejectionReason.FinalTransferCodingNotChunked, transferEncoding.ToString()); + context.ThrowRequestRejected(RequestRejectionReason.FinalTransferCodingNotChunked, transferEncoding.ToString()); } if (upgrade) { - context.RejectRequest(RequestRejectionReason.UpgradeRequestCannotHavePayload); + context.ThrowRequestRejected(RequestRejectionReason.UpgradeRequestCannotHavePayload); } - return new ForChunkedEncoding(keepAlive, headers, context); + return new ForChunkedEncoding(keepAlive, context); } if (headers.ContentLength.HasValue) @@ -269,7 +284,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } else if (upgrade) { - context.RejectRequest(RequestRejectionReason.UpgradeRequestCannotHavePayload); + context.ThrowRequestRejected(RequestRejectionReason.UpgradeRequestCannotHavePayload); } return new ForContentLength(keepAlive, contentLength, context); @@ -283,7 +298,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HttpMethods.IsPost(context.Method) || HttpMethods.IsPut(context.Method)) { var requestRejectionReason = httpVersion == HttpVersion.Http11 ? RequestRejectionReason.LengthRequired : RequestRejectionReason.LengthRequiredHttp10; - context.RejectRequest(requestRejectionReason, context.Method); + context.ThrowRequestRejected(requestRejectionReason, context.Method); } } @@ -322,11 +337,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public override bool IsEmpty => true; - public override Task StartAsync() - { - return Task.CompletedTask; - } - public override Task ReadAsync(ArraySegment buffer, CancellationToken cancellationToken = default(CancellationToken)) { return Task.FromResult(0); @@ -341,11 +351,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return Task.CompletedTask; } - - protected override bool Read(ReadableBuffer readableBuffer, WritableBuffer writableBuffer, out ReadCursor consumed, out ReadCursor examined) - { - throw new NotImplementedException(); - } } private class ForContentLength : MessageBody @@ -378,6 +383,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return _inputLength == 0; } + + protected override void OnReadStart() + { + if (_contentLength > _context.MaxRequestBodySize) + { + _context.ThrowRequestRejected(RequestRejectionReason.RequestBodyTooLarge); + } + } } /// @@ -387,19 +400,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { // byte consts don't have a data type annotation so we pre-cast it private const byte ByteCR = (byte)'\r'; + // "7FFFFFFF\r\n" is the largest chunk size that could be returned as an int. + private const int MaxChunkPrefixBytes = 10; - private readonly IPipeReader _input; - private readonly FrameRequestHeaders _requestHeaders; private int _inputLength; + private long _consumedBytes; private Mode _mode = Mode.Prefix; - public ForChunkedEncoding(bool keepAlive, FrameRequestHeaders headers, Frame context) + public ForChunkedEncoding(bool keepAlive, Frame context) : base(context) { RequestKeepAlive = keepAlive; - _input = _context.Input; - _requestHeaders = headers; } protected override bool Read(ReadableBuffer readableBuffer, WritableBuffer writableBuffer, out ReadCursor consumed, out ReadCursor examined) @@ -471,6 +483,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http readableBuffer = readableBuffer.Slice(consumed); } + // _consumedBytes aren't tracked for trailer headers, since headers have seperate limits. if (_mode == Mode.TrailerHeaders) { if (_context.TakeMessageHeaders(readableBuffer, out consumed, out examined)) @@ -482,6 +495,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return _mode == Mode.Complete; } + private void AddAndCheckConsumedBytes(int consumedBytes) + { + _consumedBytes += consumedBytes; + + if (_consumedBytes > _context.MaxRequestBodySize) + { + _context.ThrowRequestRejected(RequestRejectionReason.RequestBodyTooLarge); + } + } + private void ParseChunkedPrefix(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined) { consumed = buffer.Start; @@ -499,13 +522,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http var chunkSize = CalculateChunkSize(ch1, 0); ch1 = ch2; - do + while (reader.ConsumedBytes < MaxChunkPrefixBytes) { if (ch1 == ';') { consumed = reader.Cursor; examined = reader.Cursor; + AddAndCheckConsumedBytes(reader.ConsumedBytes); _inputLength = chunkSize; _mode = Mode.Extension; return; @@ -523,23 +547,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http consumed = reader.Cursor; examined = reader.Cursor; + AddAndCheckConsumedBytes(reader.ConsumedBytes); _inputLength = chunkSize; - - if (chunkSize > 0) - { - _mode = Mode.Data; - } - else - { - _mode = Mode.Trailer; - } - + _mode = chunkSize > 0 ? Mode.Data : Mode.Trailer; return; } chunkSize = CalculateChunkSize(ch1, chunkSize); ch1 = ch2; - } while (ch1 != -1); + } + + // At this point, 10 bytes have been consumed which is enough to parse the max value "7FFFFFFF\r\n". + _context.ThrowRequestRejected(RequestRejectionReason.BadChunkSizeData); } private void ParseExtension(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined) @@ -548,39 +567,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // Just drain the data consumed = buffer.Start; examined = buffer.Start; + do { ReadCursor extensionCursor; if (ReadCursorOperations.Seek(buffer.Start, buffer.End, out extensionCursor, ByteCR) == -1) { // End marker not found yet + consumed = buffer.End; examined = buffer.End; + AddAndCheckConsumedBytes(buffer.Length); return; }; + var charsToByteCRExclusive = buffer.Slice(0, extensionCursor).Length; + var sufixBuffer = buffer.Slice(extensionCursor); if (sufixBuffer.Length < 2) { + consumed = extensionCursor; examined = buffer.End; + AddAndCheckConsumedBytes(charsToByteCRExclusive); return; } sufixBuffer = sufixBuffer.Slice(0, 2); var sufixSpan = sufixBuffer.ToSpan(); - if (sufixSpan[1] == '\n') { + // We consumed the \r\n at the end of the extension, so switch modes. + _mode = _inputLength > 0 ? Mode.Data : Mode.Trailer; + consumed = sufixBuffer.End; examined = sufixBuffer.End; - if (_inputLength > 0) - { - _mode = Mode.Data; - } - else - { - _mode = Mode.Trailer; - } + AddAndCheckConsumedBytes(charsToByteCRExclusive + 2); + } + else + { + // Don't consume suffixSpan[1] in case it is also a \r. + buffer = buffer.Slice(charsToByteCRExclusive + 1); + consumed = extensionCursor; + AddAndCheckConsumedBytes(charsToByteCRExclusive + 1); } } while (_mode == Mode.Extension); } @@ -594,6 +622,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http Copy(buffer.Slice(0, actual), writableBuffer); _inputLength -= actual; + AddAndCheckConsumedBytes(actual); if (_inputLength == 0) { @@ -618,11 +647,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { consumed = suffixBuffer.End; examined = suffixBuffer.End; + AddAndCheckConsumedBytes(2); _mode = Mode.Prefix; } else { - _context.RejectRequest(RequestRejectionReason.BadChunkSuffix); + _context.ThrowRequestRejected(RequestRejectionReason.BadChunkSuffix); } } @@ -644,6 +674,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { consumed = trailerBuffer.End; examined = trailerBuffer.End; + AddAndCheckConsumedBytes(2); _mode = Mode.Complete; } else @@ -654,23 +685,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private int CalculateChunkSize(int extraHexDigit, int currentParsedSize) { - checked + try { - if (extraHexDigit >= '0' && extraHexDigit <= '9') + checked { - return currentParsedSize * 0x10 + (extraHexDigit - '0'); - } - else if (extraHexDigit >= 'A' && extraHexDigit <= 'F') - { - return currentParsedSize * 0x10 + (extraHexDigit - ('A' - 10)); - } - else if (extraHexDigit >= 'a' && extraHexDigit <= 'f') - { - return currentParsedSize * 0x10 + (extraHexDigit - ('a' - 10)); + if (extraHexDigit >= '0' && extraHexDigit <= '9') + { + return currentParsedSize * 0x10 + (extraHexDigit - '0'); + } + else if (extraHexDigit >= 'A' && extraHexDigit <= 'F') + { + return currentParsedSize * 0x10 + (extraHexDigit - ('A' - 10)); + } + else if (extraHexDigit >= 'a' && extraHexDigit <= 'f') + { + return currentParsedSize * 0x10 + (extraHexDigit - ('a' - 10)); + } } } + catch (OverflowException ex) + { + throw new IOException(CoreStrings.BadRequest_BadChunkSizeData, ex); + } - _context.RejectRequest(RequestRejectionReason.BadChunkSizeData); + _context.ThrowRequestRejected(RequestRejectionReason.BadChunkSizeData); return -1; // can't happen, but compiler complains } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/RequestRejectionReason.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/RequestRejectionReason.cs index 730badcff8..b482c87a02 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/RequestRejectionReason.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/RequestRejectionReason.cs @@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http RequestLineTooLong, HeadersExceedMaxTotalSize, TooManyHeaders, + RequestBodyTooLarge, RequestTimeout, FinalTransferCodingNotChunked, LengthRequired, diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/Streams.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/Streams.cs index c81f9c3985..994ee2d31c 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/Streams.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/Streams.cs @@ -60,13 +60,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure _response.PauseAcceptingWrites(); } - public void Resume() - { - _request.ResumeAcceptingReads(); - _emptyRequest.ResumeAcceptingReads(); - _response.ResumeAcceptingWrites(); - } - public void Stop() { _request.StopAcceptingReads(); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs index dc86c064e6..3e02eaad9f 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.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 Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Server.Kestrel.Core { @@ -20,6 +21,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core // Matches the default large_client_header_buffers in nginx. private int _maxRequestHeadersTotalSize = 32 * 1024; + // Matches the default maxAllowedContentLength in IIS (~28.6 MB) + // https://www.iis.net/configreference/system.webserver/security/requestfiltering/requestlimits#005 + private long? _maxRequestBodySize = 30000000; + // Matches the default LimitRequestFields in Apache httpd. private int _maxRequestHeaderCount = 100; @@ -133,6 +138,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } } + /// + /// Gets or sets the maximum allowed size of any request body in bytes. + /// When set to null, the maximum request body size is unlimited. + /// This limit has no effect on upgraded connections which are always unlimited. + /// This can be overridden per-request via . + /// + /// + /// Defaults to null (unlimited). + /// + public long? MaxRequestBodySize + { + get => _maxRequestBodySize; + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.NonNegativeNumberOrNullRequired); + } + _maxRequestBodySize = value; + } + } + /// /// Gets or sets the keep-alive timeout. /// 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 5303d1e5b8..877d6d308b 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -948,6 +948,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatUpgradeCannotBeCalledMultipleTimes() => GetString("UpgradeCannotBeCalledMultipleTimes"); + /// + /// Request body too large. + /// + internal static string BadRequest_RequestBodyTooLarge + { + get => GetString("BadRequest_RequestBodyTooLarge"); + } + + /// + /// Request body too large. + /// + internal static string FormatBadRequest_RequestBodyTooLarge() + => GetString("BadRequest_RequestBodyTooLarge"); + + /// + /// The maximum request body size cannot be modified after the app has already started reading from the request body. + /// + internal static string MaxRequestBodySizeCannotBeModifiedAfterRead + { + get => GetString("MaxRequestBodySizeCannotBeModifiedAfterRead"); + } + + /// + /// The maximum request body size cannot be modified after the app has already started reading from the request body. + /// + internal static string FormatMaxRequestBodySizeCannotBeModifiedAfterRead() + => GetString("MaxRequestBodySizeCannotBeModifiedAfterRead"); + + /// + /// The maximum request body size cannot be modified after the request has be upgraded. + /// + internal static string MaxRequestBodySizeCannotBeModifiedForUpgradedRequests + { + get => GetString("MaxRequestBodySizeCannotBeModifiedForUpgradedRequests"); + } + + /// + /// The maximum request body size cannot be modified after the request has be upgraded. + /// + internal static string FormatMaxRequestBodySizeCannotBeModifiedForUpgradedRequests() + => GetString("MaxRequestBodySizeCannotBeModifiedForUpgradedRequests"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs index ca5eb53008..e8d2481942 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs @@ -6,7 +6,6 @@ using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -17,7 +16,6 @@ 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.Server.Kestrel.Internal.System.IO.Pipelines; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; @@ -733,6 +731,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await requestProcessingTask.TimeoutAfter(TimeSpan.FromSeconds(10)); } + [Fact] + public void ThrowsWhenMaxRequestBodySizeIsSetAfterReadingFromRequestBody() + { + // Act + // This would normally be set by the MessageBody during the first read. + _frame.HasStartedConsumingRequestBody = true; + + // Assert + Assert.True(((IHttpMaxRequestBodySizeFeature)_frame).IsReadOnly); + var ex = Assert.Throws(() => ((IHttpMaxRequestBodySizeFeature)_frame).MaxRequestBodySize = 1); + Assert.Equal(CoreStrings.MaxRequestBodySizeCannotBeModifiedAfterRead, ex.Message); + } + + [Fact] + public void ThrowsWhenMaxRequestBodySizeIsSetToANegativeValue() + { + // Assert + var ex = Assert.Throws(() => ((IHttpMaxRequestBodySizeFeature)_frame).MaxRequestBodySize = -1); + Assert.StartsWith(CoreStrings.NonNegativeNumberOrNullRequired, ex.Message); + } + private static async Task WaitForCondition(TimeSpan timeout, Func condition) { const int MaxWaitLoop = 150; diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs index 25db797284..863dfd7ff4 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs @@ -241,5 +241,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var ex = Assert.Throws(() => new KestrelServerLimits().MaxConcurrentUpgradedConnections = value); Assert.StartsWith(CoreStrings.NonNegativeNumberOrNullRequired, ex.Message); } + + [Fact] + public void MaxRequestBodySizeDefault() + { + // ~28.6 MB (https://www.iis.net/configreference/system.webserver/security/requestfiltering/requestlimits#005) + Assert.Equal(30000000, new KestrelServerLimits().MaxRequestBodySize); + } + + [Theory] + [InlineData(null)] + [InlineData(0)] + [InlineData(1)] + [InlineData(long.MaxValue)] + public void MaxRequestBodySizeValid(long? value) + { + var limits = new KestrelServerLimits + { + MaxRequestBodySize = value + }; + + Assert.Equal(value, limits.MaxRequestBodySize); + } + + [Theory] + [InlineData(long.MinValue)] + [InlineData(-1)] + public void MaxRequestBodySizeInvalid(long value) + { + var ex = Assert.Throws(() => new KestrelServerLimits().MaxRequestBodySize = value); + Assert.StartsWith(CoreStrings.NonNegativeNumberOrNullRequired, ex.Message); + } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MessageBodyTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MessageBodyTests.cs index bf61a5df51..1bb42355f6 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MessageBodyTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MessageBodyTests.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Theory] [InlineData(HttpVersion.Http10)] [InlineData(HttpVersion.Http11)] - public async Task CanReadFromContentLength(HttpVersion httpVersion) + public void CanReadFromContentLength(HttpVersion httpVersion) { using (var input = new TestInput()) { @@ -34,8 +34,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var stream = new FrameRequestStream(); stream.StartAcceptingReads(body); - var bodyTask = body.StartAsync(); - input.Add("Hello"); var buffer = new byte[1024]; @@ -46,8 +44,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests count = stream.Read(buffer, 0, buffer.Length); Assert.Equal(0, count); - - await bodyTask; } } @@ -62,8 +58,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var stream = new FrameRequestStream(); stream.StartAcceptingReads(body); - var bodyTask = body.StartAsync(); - input.Add("Hello"); var buffer = new byte[1024]; @@ -74,13 +68,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests count = await stream.ReadAsync(buffer, 0, buffer.Length); Assert.Equal(0, count); - - await bodyTask; } } [Fact] - public async Task CanReadFromChunkedEncoding() + public void CanReadFromChunkedEncoding() { using (var input = new TestInput()) { @@ -88,8 +80,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var stream = new FrameRequestStream(); stream.StartAcceptingReads(body); - var bodyTask = body.StartAsync(); - input.Add("5\r\nHello\r\n"); var buffer = new byte[1024]; @@ -102,8 +92,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests count = stream.Read(buffer, 0, buffer.Length); Assert.Equal(0, count); - - await bodyTask; } } @@ -116,8 +104,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var stream = new FrameRequestStream(); stream.StartAcceptingReads(body); - var bodyTask = body.StartAsync(); - input.Add("5\r\nHello\r\n"); var buffer = new byte[1024]; @@ -130,15 +116,74 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests count = await stream.ReadAsync(buffer, 0, buffer.Length); Assert.Equal(0, count); + } + } - await bodyTask; + [Fact] + public async Task ReadExitsGivenIncompleteChunkedExtension() + { + using (var input = new TestInput()) + { + var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.FrameContext); + var stream = new FrameRequestStream(); + stream.StartAcceptingReads(body); + + input.Add("5;\r\0"); + + var buffer = new byte[1024]; + var readTask = stream.ReadAsync(buffer, 0, buffer.Length); + + Assert.False(readTask.IsCompleted); + + input.Add("\r\r\r\nHello\r\n0\r\n\r\n"); + + Assert.Equal(5, await readTask.TimeoutAfter(TimeSpan.FromSeconds(10))); + Assert.Equal(0, await stream.ReadAsync(buffer, 0, buffer.Length)); + } + } + + [Fact] + public async Task ReadThrowsGivenChunkPrefixGreaterThanMaxInt() + { + using (var input = new TestInput()) + { + var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.FrameContext); + var stream = new FrameRequestStream(); + stream.StartAcceptingReads(body); + + input.Add("80000000\r\n"); + + var buffer = new byte[1024]; + var ex = await Assert.ThrowsAsync(async () => + await stream.ReadAsync(buffer, 0, buffer.Length)); + Assert.IsType(ex.InnerException); + Assert.Equal(CoreStrings.BadRequest_BadChunkSizeData, ex.Message); + } + } + + [Fact] + public async Task ReadThrowsGivenChunkPrefixGreaterThan8Bytes() + { + using (var input = new TestInput()) + { + var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.FrameContext); + var stream = new FrameRequestStream(); + stream.StartAcceptingReads(body); + + input.Add("012345678\r"); + + var buffer = new byte[1024]; + var ex = await Assert.ThrowsAsync(async () => + await stream.ReadAsync(buffer, 0, buffer.Length)); + + Assert.Equal(CoreStrings.BadRequest_BadChunkSizeData, ex.Message); } } [Theory] [InlineData(HttpVersion.Http10)] [InlineData(HttpVersion.Http11)] - public async Task CanReadFromRemainingData(HttpVersion httpVersion) + public void CanReadFromRemainingData(HttpVersion httpVersion) { using (var input = new TestInput()) { @@ -146,8 +191,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var stream = new FrameRequestStream(); stream.StartAcceptingReads(body); - var bodyTask = body.StartAsync(); - input.Add("Hello"); var buffer = new byte[1024]; @@ -157,8 +200,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests AssertASCII("Hello", new ArraySegment(buffer, 0, count)); input.Fin(); - - await bodyTask; } } @@ -173,8 +214,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var stream = new FrameRequestStream(); stream.StartAcceptingReads(body); - var bodyTask = body.StartAsync(); - input.Add("Hello"); var buffer = new byte[1024]; @@ -184,15 +223,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests AssertASCII("Hello", new ArraySegment(buffer, 0, count)); input.Fin(); - - await bodyTask; } } [Theory] [InlineData(HttpVersion.Http10)] [InlineData(HttpVersion.Http11)] - public async Task ReadFromNoContentLengthReturnsZero(HttpVersion httpVersion) + public void ReadFromNoContentLengthReturnsZero(HttpVersion httpVersion) { using (var input = new TestInput()) { @@ -200,14 +237,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var stream = new FrameRequestStream(); stream.StartAcceptingReads(body); - var bodyTask = body.StartAsync(); - input.Add("Hello"); var buffer = new byte[1024]; Assert.Equal(0, stream.Read(buffer, 0, buffer.Length)); - - await bodyTask; } } @@ -222,14 +255,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var stream = new FrameRequestStream(); stream.StartAcceptingReads(body); - var bodyTask = body.StartAsync(); - input.Add("Hello"); var buffer = new byte[1024]; Assert.Equal(0, await stream.ReadAsync(buffer, 0, buffer.Length)); - - await bodyTask; } } @@ -242,8 +271,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var stream = new FrameRequestStream(); stream.StartAcceptingReads(body); - var bodyTask = body.StartAsync(); - // Input needs to be greater than 4032 bytes to allocate a block not backed by a slab. var largeInput = new string('a', 8192); @@ -258,8 +285,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var requestArray = ms.ToArray(); Assert.Equal(8197, requestArray.Length); AssertASCII(largeInput + "Hello", new ArraySegment(requestArray, 0, requestArray.Length)); - - await bodyTask; } } @@ -314,8 +339,6 @@ 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 bodyTask = body.StartAsync(); - input.Add("Hello"); using (var ms = new MemoryStream()) @@ -324,8 +347,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } Assert.Equal(0, await body.ReadAsync(new ArraySegment(new byte[1]))); - - await bodyTask; } } @@ -335,15 +356,11 @@ 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 bodyTask = body.StartAsync(); - input.Add("Hello"); await body.ConsumeAsync(); await Assert.ThrowsAsync(async () => await body.ReadAsync(new ArraySegment(new byte[1]))); - - await bodyTask; } } @@ -386,8 +403,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(HttpVersion.Http11, headers, input.FrameContext); - var bodyTask = body.StartAsync(); - var copyToAsyncTask = body.CopyToAsync(mockDestination.Object); // The block returned by IncomingStart always has at least 2048 available bytes, @@ -421,8 +436,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await copyToAsyncTask; Assert.Equal(2, writeCount); - - await bodyTask; } } @@ -431,7 +444,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [InlineData("Keep-Alive, Upgrade")] [InlineData("upgrade, keep-alive")] [InlineData("Upgrade, Keep-Alive")] - public async Task ConnectionUpgradeKeepAlive(string headerConnection) + public void ConnectionUpgradeKeepAlive(string headerConnection) { using (var input = new TestInput()) { @@ -439,8 +452,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var stream = new FrameRequestStream(); stream.StartAcceptingReads(body); - var bodyTask = body.StartAsync(); - input.Add("Hello"); var buffer = new byte[1024]; @@ -448,8 +459,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests AssertASCII("Hello", new ArraySegment(buffer, 0, 5)); input.Fin(); - - await bodyTask; } } @@ -462,8 +471,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var stream = new FrameRequestStream(); stream.StartAcceptingReads(body); - var bodyTask = body.StartAsync(); - // Add some input and consume it to ensure StartAsync is in the loop input.Add("a"); Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1)); @@ -474,8 +481,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests input.Add("b"); Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1)); - // All input was read, body task should complete - await bodyTask; } } @@ -488,8 +493,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var stream = new FrameRequestStream(); stream.StartAcceptingReads(body); - var bodyTask = body.StartAsync(); - // Add some input and consume it to ensure StartAsync is in the loop input.Add("a"); Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1)); @@ -503,8 +506,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // Unblock the loop input.Pipe.Reader.CancelPendingRead(); - await bodyTask.TimeoutAfter(TimeSpan.FromSeconds(10)); - // There shouldn't be any additional data available Assert.Equal(0, await stream.ReadAsync(new byte[1], 0, 1)); } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/StreamsTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/StreamsTests.cs index 95eae94658..6a7b108228 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/StreamsTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/StreamsTests.cs @@ -80,13 +80,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { RequestUpgrade = upgradeable; } - - protected override bool Read(ReadableBuffer readableBuffer, WritableBuffer writableBuffer, out ReadCursor consumed, out ReadCursor examined) - { - consumed = default(ReadCursor); - examined = default(ReadCursor); - return true; - } } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBodySizeTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBodySizeTests.cs new file mode 100644 index 0000000000..9d61ffb19f --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBodySizeTests.cs @@ -0,0 +1,494 @@ +// 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.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class MaxRequestBodySizeTests + { + [Fact] + public async Task RejectsRequestWithContentLengthHeaderExceedingGlobalLimit() + { + // 4 GiB + var globalMaxRequestBodySize = 0x100000000; + BadHttpRequestException requestRejectedEx = null; + + using (var server = new TestServer(async context => + { + var buffer = new byte[1]; + requestRejectedEx = await Assert.ThrowsAsync( + async () => await context.Request.Body.ReadAsync(buffer, 0, 1)); + throw requestRejectedEx; + }, + new TestServiceContext { ServerOptions = { Limits = { MaxRequestBodySize = globalMaxRequestBodySize } } })) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: " + (globalMaxRequestBodySize + 1), + "", + ""); + await connection.ReceiveForcedEnd( + "HTTP/1.1 413 Payload Too Large", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + + Assert.NotNull(requestRejectedEx); + Assert.Equal(CoreStrings.BadRequest_RequestBodyTooLarge, requestRejectedEx.Message); + } + + [Fact] + public async Task RejectsRequestWithContentLengthHeaderExceedingPerRequestLimit() + { + // 8 GiB + var globalMaxRequestBodySize = 0x200000000; + // 4 GiB + var perRequestMaxRequestBodySize = 0x100000000; + BadHttpRequestException requestRejectedEx = null; + + using (var server = new TestServer(async context => + { + var feature = context.Features.Get(); + Assert.Equal(globalMaxRequestBodySize, feature.MaxRequestBodySize); + + // Disable the MaxRequestBodySize prior to calling Request.Body.ReadAsync(); + feature.MaxRequestBodySize = perRequestMaxRequestBodySize; + + var buffer = new byte[1]; + requestRejectedEx = await Assert.ThrowsAsync( + async () => await context.Request.Body.ReadAsync(buffer, 0, 1)); + throw requestRejectedEx; + }, + new TestServiceContext { ServerOptions = { Limits = { MaxRequestBodySize = globalMaxRequestBodySize } } })) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: " + (perRequestMaxRequestBodySize + 1), + "", + ""); + await connection.ReceiveForcedEnd( + "HTTP/1.1 413 Payload Too Large", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + + Assert.NotNull(requestRejectedEx); + Assert.Equal(CoreStrings.BadRequest_RequestBodyTooLarge, requestRejectedEx.Message); + } + + [Fact] + public async Task DoesNotRejectRequestWithContentLengthHeaderExceedingGlobalLimitIfLimitDisabledPerRequest() + { + using (var server = new TestServer(async context => + { + var feature = context.Features.Get(); + Assert.Equal(0, feature.MaxRequestBodySize); + + // Disable the MaxRequestBodySize prior to calling Request.Body.ReadAsync(); + feature.MaxRequestBodySize = null; + + var buffer = new byte[1]; + + Assert.Equal(1, await context.Request.Body.ReadAsync(buffer, 0, 1)); + Assert.Equal(buffer[0], (byte)'A'); + Assert.Equal(0, await context.Request.Body.ReadAsync(buffer, 0, 1)); + + context.Response.ContentLength = 1; + await context.Response.Body.WriteAsync(buffer, 0, 1); + }, + new TestServiceContext { ServerOptions = { Limits = { MaxRequestBodySize = 0 } } })) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: 1", + "", + "A"); + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 1", + "", + "A"); + } + } + } + + [Fact] + public async Task DoesNotRejectBodylessGetRequestWithZeroMaxRequestBodySize() + { + using (var server = new TestServer(context => Task.CompletedTask, + new TestServiceContext { ServerOptions = { Limits = { MaxRequestBodySize = 0 } } })) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + "POST / HTTP/1.1", + "Host:", + "Content-Length: 1", + "", + "A"); + await connection.ReceiveForcedEnd( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + "HTTP/1.1 413 Payload Too Large", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + + [Fact] + public async Task SettingMaxRequestBodySizeAfterReadingFromRequestBodyThrows() + { + var perRequestMaxRequestBodySize = 0x10; + var payloadSize = perRequestMaxRequestBodySize + 1; + var payload = new string('A', payloadSize); + InvalidOperationException invalidOpEx = null; + + using (var server = new TestServer(async context => + { + var buffer = new byte[1]; + Assert.Equal(1, await context.Request.Body.ReadAsync(buffer, 0, 1)); + + var feature = context.Features.Get(); + Assert.Equal(new KestrelServerLimits().MaxRequestBodySize, feature.MaxRequestBodySize); + Assert.True(feature.IsReadOnly); + + invalidOpEx = Assert.Throws(() => + feature.MaxRequestBodySize = perRequestMaxRequestBodySize); + throw invalidOpEx; + })) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: " + payloadSize, + "", + payload); + await connection.Receive( + "HTTP/1.1 500 Internal Server Error", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + + Assert.NotNull(invalidOpEx); + Assert.Equal(CoreStrings.MaxRequestBodySizeCannotBeModifiedAfterRead, invalidOpEx.Message); + } + + [Fact] + public async Task SettingMaxRequestBodySizeAfterUpgradingRequestThrows() + { + InvalidOperationException invalidOpEx = null; + + using (var server = new TestServer(async context => + { + var upgradeFeature = context.Features.Get(); + var stream = await upgradeFeature.UpgradeAsync(); + + var feature = context.Features.Get(); + Assert.Equal(new KestrelServerLimits().MaxRequestBodySize, feature.MaxRequestBodySize); + Assert.True(feature.IsReadOnly); + + invalidOpEx = Assert.Throws(() => + feature.MaxRequestBodySize = 0x10); + throw invalidOpEx; + })) + { + using (var connection = server.CreateConnection()) + { + await connection.Send("GET / HTTP/1.1", + "Host:", + "Connection: Upgrade", + "", + ""); + await connection.Receive("HTTP/1.1 101 Switching Protocols", + "Connection: Upgrade", + $"Date: {server.Context.DateHeaderValue}", + "", + ""); + await connection.ReceiveForcedEnd(); + } + } + + Assert.NotNull(invalidOpEx); + Assert.Equal(CoreStrings.MaxRequestBodySizeCannotBeModifiedForUpgradedRequests, invalidOpEx.Message); + } + + [Fact] + public async Task EveryReadFailsWhenContentLengthHeaderExceedsGlobalLimit() + { + BadHttpRequestException requestRejectedEx1 = null; + BadHttpRequestException requestRejectedEx2 = null; + + using (var server = new TestServer(async context => + { + var buffer = new byte[1]; + requestRejectedEx1 = await Assert.ThrowsAsync( + async () => await context.Request.Body.ReadAsync(buffer, 0, 1)); + requestRejectedEx2 = await Assert.ThrowsAsync( + async () => await context.Request.Body.ReadAsync(buffer, 0, 1)); + throw requestRejectedEx2; + }, + new TestServiceContext { ServerOptions = { Limits = { MaxRequestBodySize = 0 } } })) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: " + (new KestrelServerLimits().MaxRequestBodySize + 1), + "", + ""); + await connection.ReceiveForcedEnd( + "HTTP/1.1 413 Payload Too Large", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + + Assert.NotNull(requestRejectedEx1); + Assert.NotNull(requestRejectedEx2); + Assert.Equal(CoreStrings.BadRequest_RequestBodyTooLarge, requestRejectedEx1.Message); + Assert.Equal(CoreStrings.BadRequest_RequestBodyTooLarge, requestRejectedEx2.Message); + } + + [Fact] + public async Task ChunkFramingAndExtensionsCountTowardsRequestBodySize() + { + var chunkedPayload = "5;random chunk extension\r\nHello\r\n6\r\n World\r\n0\r\n\r\n"; + var globalMaxRequestBodySize = chunkedPayload.Length - 1; + BadHttpRequestException requestRejectedEx = null; + + using (var server = new TestServer(async context => + { + var buffer = new byte[11]; + requestRejectedEx = await Assert.ThrowsAsync(async () => + { + var count = 0; + do + { + count = await context.Request.Body.ReadAsync(buffer, 0, 11); + } while (count != 0); + }); + + throw requestRejectedEx; + }, + new TestServiceContext { ServerOptions = { Limits = { MaxRequestBodySize = globalMaxRequestBodySize } } })) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Transfer-Encoding: chunked", + "", + chunkedPayload); + await connection.ReceiveForcedEnd( + "HTTP/1.1 413 Payload Too Large", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + + Assert.NotNull(requestRejectedEx); + Assert.Equal(CoreStrings.BadRequest_RequestBodyTooLarge, requestRejectedEx.Message); + } + + [Fact] + public async Task TrailingHeadersDoNotCountTowardsRequestBodySize() + { + var chunkedPayload = $"5;random chunk extension\r\nHello\r\n6\r\n World\r\n0\r\n"; + var trailingHeaders = "Trailing-Header: trailing-value\r\n\r\n"; + var globalMaxRequestBodySize = chunkedPayload.Length; + + using (var server = new TestServer(async context => + { + var offset = 0; + var count = 0; + var buffer = new byte[11]; + + do + { + count = await context.Request.Body.ReadAsync(buffer, offset, 11 - offset); + offset += count; + } while (count != 0); + + Assert.Equal("Hello World", Encoding.ASCII.GetString(buffer)); + Assert.Equal("trailing-value", context.Request.Headers["Trailing-Header"].ToString()); + }, + new TestServiceContext { ServerOptions = { Limits = { MaxRequestBodySize = globalMaxRequestBodySize } } })) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Transfer-Encoding: chunked", + "", + chunkedPayload + trailingHeaders); + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + + [Fact] + public async Task PerRequestMaxRequestBodySizeGetsReset() + { + var chunkedPayload = "5;random chunk extension\r\nHello\r\n6\r\n World\r\n0\r\n\r\n"; + var globalMaxRequestBodySize = chunkedPayload.Length - 1; + var firstRequest = true; + BadHttpRequestException requestRejectedEx = null; + + using (var server = new TestServer(async context => + { + var feature = context.Features.Get(); + Assert.Equal(globalMaxRequestBodySize, feature.MaxRequestBodySize); + + var buffer = new byte[11]; + var count = 0; + + if (firstRequest) + { + firstRequest = false; + feature.MaxRequestBodySize = chunkedPayload.Length; + + do + { + count = await context.Request.Body.ReadAsync(buffer, 0, 11); + } while (count != 0); + } + else + { + requestRejectedEx = await Assert.ThrowsAsync(async () => + { + do + { + count = await context.Request.Body.ReadAsync(buffer, 0, 11); + } while (count != 0); + }); + + throw requestRejectedEx; + } + }, + new TestServiceContext { ServerOptions = { Limits = { MaxRequestBodySize = globalMaxRequestBodySize } } })) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Transfer-Encoding: chunked", + "", + chunkedPayload + "POST / HTTP/1.1", + "Host:", + "Transfer-Encoding: chunked", + "", + chunkedPayload); + await connection.ReceiveForcedEnd( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + "HTTP/1.1 413 Payload Too Large", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + + Assert.NotNull(requestRejectedEx); + Assert.Equal(CoreStrings.BadRequest_RequestBodyTooLarge, requestRejectedEx.Message); + } + + [Fact] + public async Task EveryReadFailsWhenChunkedPayloadExceedsGlobalLimit() + { + BadHttpRequestException requestRejectedEx1 = null; + BadHttpRequestException requestRejectedEx2 = null; + + using (var server = new TestServer(async context => + { + var buffer = new byte[1]; + requestRejectedEx1 = await Assert.ThrowsAsync( + async () => await context.Request.Body.ReadAsync(buffer, 0, 1)); + requestRejectedEx2 = await Assert.ThrowsAsync( + async () => await context.Request.Body.ReadAsync(buffer, 0, 1)); + throw requestRejectedEx2; + }, + new TestServiceContext { ServerOptions = { Limits = { MaxRequestBodySize = 0 } } })) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Transfer-Encoding: chunked", + "", + "1\r\n"); + await connection.ReceiveForcedEnd( + "HTTP/1.1 413 Payload Too Large", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + + Assert.NotNull(requestRejectedEx1); + Assert.NotNull(requestRejectedEx2); + Assert.Equal(CoreStrings.BadRequest_RequestBodyTooLarge, requestRejectedEx1.Message); + Assert.Equal(CoreStrings.BadRequest_RequestBodyTooLarge, requestRejectedEx2.Message); + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs index a5fbc348aa..50004ca37a 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs @@ -59,7 +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() + .UseKestrel(o => + { + o.Limits.MaxRequestBodySize = contentLength; + }) .UseUrls("http://127.0.0.1:0/") .Configure(app => { diff --git a/test/shared/TestConnection.cs b/test/shared/TestConnection.cs index f4ff0a2b62..fc6458c287 100644 --- a/test/shared/TestConnection.cs +++ b/test/shared/TestConnection.cs @@ -117,20 +117,31 @@ namespace Microsoft.AspNetCore.Testing var expected = string.Join("\r\n", lines); var actual = new char[expected.Length]; var offset = 0; - while (offset < expected.Length) + + try { - var data = new byte[expected.Length]; - var task = _reader.ReadAsync(actual, offset, actual.Length - offset); - if (!Debugger.IsAttached) + while (offset < expected.Length) { - task = task.TimeoutAfter(Timeout); + var data = new byte[expected.Length]; + var task = _reader.ReadAsync(actual, offset, actual.Length - offset); + if (!Debugger.IsAttached) + { + task = task.TimeoutAfter(Timeout); + } + var count = await task.ConfigureAwait(false); + if (count == 0) + { + break; + } + offset += count; } - var count = await task.ConfigureAwait(false); - if (count == 0) - { - break; - } - offset += count; + } + catch (TimeoutException ex) when (offset != 0) + { + throw new TimeoutException($"Did not receive a complete response within {Timeout}.{Environment.NewLine}{Environment.NewLine}" + + $"Expected:{Environment.NewLine}{expected}{Environment.NewLine}{Environment.NewLine}" + + $"Actual:{Environment.NewLine}{new string(actual, 0, offset)}{Environment.NewLine}", + ex); } Assert.Equal(expected, new string(actual, 0, offset)); diff --git a/tools/CodeGenerator/FrameFeatureCollection.cs b/tools/CodeGenerator/FrameFeatureCollection.cs index 01b9c1cfa9..d97136380d 100644 --- a/tools/CodeGenerator/FrameFeatureCollection.cs +++ b/tools/CodeGenerator/FrameFeatureCollection.cs @@ -44,7 +44,8 @@ namespace CodeGenerator typeof(IItemsFeature), typeof(ITlsConnectionFeature), typeof(IHttpWebSocketFeature), - typeof(ISessionFeature) + typeof(ISessionFeature), + typeof(IHttpMaxRequestBodySizeFeature) }; var rareFeatures = new[] @@ -64,6 +65,7 @@ namespace CodeGenerator typeof(IHttpRequestIdentifierFeature), typeof(IHttpRequestLifetimeFeature), typeof(IHttpConnectionFeature), + typeof(IHttpMaxRequestBodySizeFeature) }; return $@"// Copyright (c) .NET Foundation. All rights reserved.