Merge branch 'rel/2.0.0-preview2' into dev

This commit is contained in:
Stephen Halter 2017-06-07 11:50:27 -07:00
commit c6df7eb644
21 changed files with 906 additions and 222 deletions

View File

@ -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;

View File

@ -318,4 +318,13 @@
<data name="UpgradeCannotBeCalledMultipleTimes" xml:space="preserve">
<value>IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection.</value>
</data>
</root>
<data name="BadRequest_RequestBodyTooLarge" xml:space="preserve">
<value>Request body too large.</value>
</data>
<data name="MaxRequestBodySizeCannotBeModifiedAfterRead" xml:space="preserve">
<value>The maximum request body size cannot be modified after the app has already started reading from the request body.</value>
</data>
<data name="MaxRequestBodySizeCannotBeModifiedForUpgradedRequests" xml:space="preserve">
<value>The maximum request body size cannot be modified after the request has been upgraded.</value>
</data>
</root>

View File

@ -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);

View File

@ -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<Type, object>(ISessionFeatureType, _currentISessionFeature as global::Microsoft.AspNetCore.Http.Features.ISessionFeature);
}
if (_currentIHttpMaxRequestBodySizeFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpMaxRequestBodySizeFeatureType, _currentIHttpMaxRequestBodySizeFeature as global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
}
if (_currentIHttpSendFileFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature as global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature);

View File

@ -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; }
/// <summary>
/// The request id. <seealso cref="HttpContext.TraceIdentifier"/>
@ -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<byte> target)
private void ThrowRequestTargetRejected(Span<byte> target)
=> throw GetInvalidRequestTargetException(target);
private BadHttpRequestException GetInvalidRequestTargetException(Span<byte> 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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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<int> ReadAsync(ArraySegment<byte> 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<int> ReadAsync(ArraySegment<byte> 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);
}
}
}
/// <summary>
@ -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
}

View File

@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
RequestLineTooLong,
HeadersExceedMaxTotalSize,
TooManyHeaders,
RequestBodyTooLarge,
RequestTimeout,
FinalTransferCodingNotChunked,
LengthRequired,

View File

@ -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();

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using 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
}
}
/// <summary>
/// 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 <see cref="IHttpMaxRequestBodySizeFeature"/>.
/// </summary>
/// <remarks>
/// Defaults to null (unlimited).
/// </remarks>
public long? MaxRequestBodySize
{
get => _maxRequestBodySize;
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.NonNegativeNumberOrNullRequired);
}
_maxRequestBodySize = value;
}
}
/// <summary>
/// Gets or sets the keep-alive timeout.
/// </summary>

View File

@ -948,6 +948,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatUpgradeCannotBeCalledMultipleTimes()
=> GetString("UpgradeCannotBeCalledMultipleTimes");
/// <summary>
/// Request body too large.
/// </summary>
internal static string BadRequest_RequestBodyTooLarge
{
get => GetString("BadRequest_RequestBodyTooLarge");
}
/// <summary>
/// Request body too large.
/// </summary>
internal static string FormatBadRequest_RequestBodyTooLarge()
=> GetString("BadRequest_RequestBodyTooLarge");
/// <summary>
/// The maximum request body size cannot be modified after the app has already started reading from the request body.
/// </summary>
internal static string MaxRequestBodySizeCannotBeModifiedAfterRead
{
get => GetString("MaxRequestBodySizeCannotBeModifiedAfterRead");
}
/// <summary>
/// The maximum request body size cannot be modified after the app has already started reading from the request body.
/// </summary>
internal static string FormatMaxRequestBodySizeCannotBeModifiedAfterRead()
=> GetString("MaxRequestBodySizeCannotBeModifiedAfterRead");
/// <summary>
/// The maximum request body size cannot be modified after the request has be upgraded.
/// </summary>
internal static string MaxRequestBodySizeCannotBeModifiedForUpgradedRequests
{
get => GetString("MaxRequestBodySizeCannotBeModifiedForUpgradedRequests");
}
/// <summary>
/// The maximum request body size cannot be modified after the request has be upgraded.
/// </summary>
internal static string FormatMaxRequestBodySizeCannotBeModifiedForUpgradedRequests()
=> GetString("MaxRequestBodySizeCannotBeModifiedForUpgradedRequests");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -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<InvalidOperationException>(() => ((IHttpMaxRequestBodySizeFeature)_frame).MaxRequestBodySize = 1);
Assert.Equal(CoreStrings.MaxRequestBodySizeCannotBeModifiedAfterRead, ex.Message);
}
[Fact]
public void ThrowsWhenMaxRequestBodySizeIsSetToANegativeValue()
{
// Assert
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => ((IHttpMaxRequestBodySizeFeature)_frame).MaxRequestBodySize = -1);
Assert.StartsWith(CoreStrings.NonNegativeNumberOrNullRequired, ex.Message);
}
private static async Task WaitForCondition(TimeSpan timeout, Func<bool> condition)
{
const int MaxWaitLoop = 150;

View File

@ -241,5 +241,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => 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<ArgumentOutOfRangeException>(() => new KestrelServerLimits().MaxRequestBodySize = value);
Assert.StartsWith(CoreStrings.NonNegativeNumberOrNullRequired, ex.Message);
}
}
}

View File

@ -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<IOException>(async () =>
await stream.ReadAsync(buffer, 0, buffer.Length));
Assert.IsType<OverflowException>(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<BadHttpRequestException>(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<byte>(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<byte>(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<byte>(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<byte>(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<InvalidOperationException>(async () => await body.ReadAsync(new ArraySegment<byte>(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<byte>(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));
}

View File

@ -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;
}
}
}
}

View File

@ -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<BadHttpRequestException>(
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<IHttpMaxRequestBodySizeFeature>();
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<BadHttpRequestException>(
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<IHttpMaxRequestBodySizeFeature>();
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<IHttpMaxRequestBodySizeFeature>();
Assert.Equal(new KestrelServerLimits().MaxRequestBodySize, feature.MaxRequestBodySize);
Assert.True(feature.IsReadOnly);
invalidOpEx = Assert.Throws<InvalidOperationException>(() =>
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<IHttpUpgradeFeature>();
var stream = await upgradeFeature.UpgradeAsync();
var feature = context.Features.Get<IHttpMaxRequestBodySizeFeature>();
Assert.Equal(new KestrelServerLimits().MaxRequestBodySize, feature.MaxRequestBodySize);
Assert.True(feature.IsReadOnly);
invalidOpEx = Assert.Throws<InvalidOperationException>(() =>
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<BadHttpRequestException>(
async () => await context.Request.Body.ReadAsync(buffer, 0, 1));
requestRejectedEx2 = await Assert.ThrowsAsync<BadHttpRequestException>(
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<BadHttpRequestException>(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<IHttpMaxRequestBodySizeFeature>();
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<BadHttpRequestException>(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<BadHttpRequestException>(
async () => await context.Request.Body.ReadAsync(buffer, 0, 1));
requestRejectedEx2 = await Assert.ThrowsAsync<BadHttpRequestException>(
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);
}
}
}

View File

@ -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 =>
{

View File

@ -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));

View File

@ -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.