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.