From 7b15720a05195c33cc17c2e8d0f31931acf43944 Mon Sep 17 00:00:00 2001 From: Chris R Date: Fri, 23 Jun 2017 14:11:51 -0700 Subject: [PATCH] # This is a combination of 2 commits. # This is the 1st commit message: #539 Implement request body size limit # The commit message #2 will be skipped: # Check exception messages --- .../FeatureContext.cs | 23 +- .../HttpSysOptions.cs | 27 ++ .../MessagePump.cs | 7 +- .../RequestProcessing/Request.cs | 32 +- .../RequestProcessing/RequestStream.cs | 131 ++++-- .../RequestStreamAsyncResult.cs | 8 +- .../StandardFeatureCollection.cs | 1 + .../RequestBodyLimitTests.cs | 422 ++++++++++++++++++ .../Utilities.cs | 34 +- 9 files changed, 609 insertions(+), 76 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestBodyLimitTests.cs diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/FeatureContext.cs b/src/Microsoft.AspNetCore.Server.HttpSys/FeatureContext.cs index f8eec7eeea..b2e41d46aa 100644 --- a/src/Microsoft.AspNetCore.Server.HttpSys/FeatureContext.cs +++ b/src/Microsoft.AspNetCore.Server.HttpSys/FeatureContext.cs @@ -28,7 +28,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys IHttpRequestLifetimeFeature, IHttpAuthenticationFeature, IHttpUpgradeFeature, - IHttpRequestIdentifierFeature + IHttpRequestIdentifierFeature, + IHttpMaxRequestBodySizeFeature { private RequestContext _requestContext; private IFeatureCollection _features; @@ -62,11 +63,11 @@ namespace Microsoft.AspNetCore.Server.HttpSys private bool _responseStarted; private bool _completed; - internal FeatureContext(RequestContext requestContext, bool enableResponseCaching) + internal FeatureContext(RequestContext requestContext) { _requestContext = requestContext; _features = new FeatureCollection(new StandardFeatureCollection(this)); - _enableResponseCaching = enableResponseCaching; + _enableResponseCaching = _requestContext.Server.Options.EnableResponseCaching; // Pre-initialize any fields that are not lazy at the lower level. _requestHeaders = Request.Headers; @@ -78,7 +79,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys _scheme = Request.Scheme; _user = _requestContext.User; - _responseStream = new ResponseStream(requestContext.Response.Body, OnStart); + _responseStream = new ResponseStream(requestContext.Response.Body, OnResponseStart); _responseHeaders = Response.Headers; } @@ -405,7 +406,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys async Task IHttpSendFileFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) { - await OnStart(); + await OnResponseStart(); await Response.SendFileAsync(path, offset, length, cancellation); } @@ -433,7 +434,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys async Task IHttpUpgradeFeature.UpgradeAsync() { - await OnStart(); + await OnResponseStart(); return await _requestContext.UpgradeAsync(); } @@ -463,7 +464,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys } } - internal async Task OnStart() + bool IHttpMaxRequestBodySizeFeature.IsReadOnly => Request.HasRequestBodyStarted; + + long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize + { + get => Request.MaxRequestBodySize; + set => Request.MaxRequestBodySize = value; + } + + internal async Task OnResponseStart() { if (_responseStarted) { diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs b/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs index 7fc7089a15..1d1e0cc00f 100644 --- a/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs +++ b/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.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.HttpSys { @@ -9,12 +10,16 @@ namespace Microsoft.AspNetCore.Server.HttpSys { private const long DefaultRequestQueueLength = 1000; // Http.sys default. internal static readonly int DefaultMaxAccepts = 5 * Environment.ProcessorCount; + // Matches the default maxAllowedContentLength in IIS (~28.6 MB) + // https://www.iis.net/configreference/system.webserver/security/requestfiltering/requestlimits#005 + private const long DefaultMaxRequestBodySize = 30000000; // The native request queue private long _requestQueueLength = DefaultRequestQueueLength; private long? _maxConnections; private RequestQueue _requestQueue; private UrlGroup _urlGroup; + private long? _maxRequestBodySize = DefaultMaxRequestBodySize; public HttpSysOptions() { @@ -104,6 +109,28 @@ namespace Microsoft.AspNetCore.Server.HttpSys } } + /// + /// 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 30,000,000 bytes, which is approximately 28.6MB. + /// + public long? MaxRequestBodySize + { + get => _maxRequestBodySize; + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, string.Empty); + } + _maxRequestBodySize = value; + } + } + internal void Apply(UrlGroup urlGroup, RequestQueue requestQueue) { _urlGroup = urlGroup; diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/MessagePump.cs b/src/Microsoft.AspNetCore.Server.HttpSys/MessagePump.cs index 54e2db7a5d..82e6a03039 100644 --- a/src/Microsoft.AspNetCore.Server.HttpSys/MessagePump.cs +++ b/src/Microsoft.AspNetCore.Server.HttpSys/MessagePump.cs @@ -56,14 +56,11 @@ namespace Microsoft.AspNetCore.Server.HttpSys _processRequest = new Action(ProcessRequestAsync); _maxAccepts = _options.MaxAccepts; - EnableResponseCaching = _options.EnableResponseCaching; _shutdownSignal = new TaskCompletionSource(); } internal HttpSysListener Listener { get; } - internal bool EnableResponseCaching { get; set; } - public IFeatureCollection Features { get; } public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) @@ -199,12 +196,12 @@ namespace Microsoft.AspNetCore.Server.HttpSys Interlocked.Increment(ref _outstandingRequests); try { - var featureContext = new FeatureContext(requestContext, EnableResponseCaching); + var featureContext = new FeatureContext(requestContext); context = _application.CreateContext(featureContext.Features); try { await _application.ProcessRequestAsync(context).SupressContext(); - await featureContext.OnStart(); + await featureContext.OnResponseStart(); requestContext.Dispose(); _application.DisposeContext(context, null); } diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/Request.cs b/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/Request.cs index 043790f8e0..0147b84f03 100644 --- a/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/Request.cs +++ b/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/Request.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys private BoundaryType _contentBoundaryType; private long? _contentLength; - private Stream _nativeStream; + private RequestStream _nativeStream; private SocketAddress _localEndPoint; private SocketAddress _remoteEndPoint; @@ -143,15 +143,32 @@ namespace Microsoft.AspNetCore.Server.HttpSys public string Method { get; } - public Stream Body + public Stream Body => EnsureRequestStream() ?? Stream.Null; + + private RequestStream EnsureRequestStream() { - get + if (_nativeStream == null && HasEntityBody) { - if (_nativeStream == null) + _nativeStream = new RequestStream(RequestContext) { - _nativeStream = HasEntityBody ? new RequestStream(RequestContext) : Stream.Null; + MaxSize = RequestContext.Server.Options.MaxRequestBodySize + }; + } + return _nativeStream; + } + + public bool HasRequestBodyStarted => _nativeStream?.HasStarted ?? false; + + public long? MaxRequestBodySize + { + get => EnsureRequestStream()?.MaxSize; + set + { + EnsureRequestStream(); + if (_nativeStream != null) + { + _nativeStream.MaxSize = value; } - return _nativeStream; } } @@ -319,10 +336,11 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal void SwitchToOpaqueMode() { - if (_nativeStream == null || _nativeStream == Stream.Null) + if (_nativeStream == null) { _nativeStream = new RequestStream(RequestContext); } + _nativeStream.SwitchToOpaqueMode(); } } } diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStream.cs b/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStream.cs index 0129c290e8..f8da3974c3 100644 --- a/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStream.cs +++ b/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStream.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Threading; @@ -17,6 +18,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys private RequestContext _requestContext; private uint _dataChunkOffset; private int _dataChunkIndex; + private long? _maxSize; + private long _totalRead; private bool _closed; internal RequestStream(RequestContext httpContext) @@ -35,68 +38,53 @@ namespace Microsoft.AspNetCore.Server.HttpSys private ILogger Logger => RequestContext.Server.Logger; - public override bool CanSeek + public bool HasStarted { get; private set; } + + public long? MaxSize { - get + get => _maxSize; + set { - return false; + if (HasStarted) + { + throw new InvalidOperationException("The maximum request size cannot be changed after the request body has started reading."); + } + if (value.HasValue && value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, string.Empty); + } + _maxSize = value; } } - public override bool CanWrite - { - get - { - return false; - } - } + public override bool CanSeek => false; - public override bool CanRead - { - get - { - return true; - } - } + public override bool CanWrite => false; - public override long Length - { - get - { - throw new NotSupportedException(Resources.Exception_NoSeek); - } - } + public override bool CanRead => true; + + public override long Length => throw new NotSupportedException(Resources.Exception_NoSeek); public override long Position { - get - { - throw new NotSupportedException(Resources.Exception_NoSeek); - } - set - { - throw new NotSupportedException(Resources.Exception_NoSeek); - } + get => throw new NotSupportedException(Resources.Exception_NoSeek); + set => throw new NotSupportedException(Resources.Exception_NoSeek); } public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(Resources.Exception_NoSeek); - } + => throw new NotSupportedException(Resources.Exception_NoSeek); - public override void SetLength(long value) - { - throw new NotSupportedException(Resources.Exception_NoSeek); - } + public override void SetLength(long value) => throw new NotSupportedException(Resources.Exception_NoSeek); - public override void Flush() - { - throw new InvalidOperationException(Resources.Exception_ReadOnlyStream); - } + public override void Flush() => throw new InvalidOperationException(Resources.Exception_ReadOnlyStream); public override Task FlushAsync(CancellationToken cancellationToken) + => throw new InvalidOperationException(Resources.Exception_ReadOnlyStream); + + internal void SwitchToOpaqueMode() { - throw new InvalidOperationException(Resources.Exception_ReadOnlyStream); + HasStarted = true; + _maxSize = null; } internal void Abort() @@ -124,6 +112,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys public override unsafe int Read([In, Out] byte[] buffer, int offset, int size) { ValidateReadBuffer(buffer, offset, size); + CheckSizeLimit(); if (_closed) { return 0; @@ -177,6 +166,10 @@ namespace Microsoft.AspNetCore.Server.HttpSys } UpdateAfterRead(statusCode, dataRead); } + if (TryCheckSizeLimit((int)dataRead, out var ex)) + { + throw ex; + } // TODO: Verbose log dump data read return (int)dataRead; @@ -193,6 +186,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys public override unsafe IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback callback, object state) { ValidateReadBuffer(buffer, offset, size); + CheckSizeLimit(); if (_closed) { RequestStreamAsyncResult result = new RequestStreamAsyncResult(this, state, callback); @@ -295,7 +289,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys castedAsyncResult.EndCalled = true; // wait & then check for errors // Throws on failure - int dataRead = castedAsyncResult.Task.Result; + int dataRead = castedAsyncResult.Task.GetAwaiter().GetResult(); // TODO: Verbose log #dataRead. return dataRead; } @@ -303,6 +297,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys public override unsafe Task ReadAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken) { ValidateReadBuffer(buffer, offset, size); + CheckSizeLimit(); if (_closed) { return Task.FromResult(0); @@ -323,6 +318,10 @@ namespace Microsoft.AspNetCore.Server.HttpSys if (_dataChunkIndex != -1 && dataRead == size) { UpdateAfterRead(UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS, dataRead); + if (TryCheckSizeLimit((int)dataRead, out var exception)) + { + return Task.FromException(exception); + } // TODO: Verbose log #dataRead return Task.FromResult((int)dataRead); } @@ -378,6 +377,10 @@ namespace Microsoft.AspNetCore.Server.HttpSys { uint totalRead = dataRead + bytesReturned; UpdateAfterRead(statusCode, totalRead); + if (TryCheckSizeLimit((int)totalRead, out var exception)) + { + return Task.FromException(exception); + } // TODO: Verbose log totalRead return Task.FromResult((int)totalRead); } @@ -396,6 +399,10 @@ namespace Microsoft.AspNetCore.Server.HttpSys asyncResult.Dispose(); uint totalRead = dataRead + bytesReturned; UpdateAfterRead(statusCode, totalRead); + if (TryCheckSizeLimit((int)totalRead, out var exception)) + { + return Task.FromException(exception); + } // TODO: Verbose log return Task.FromResult((int)totalRead); } @@ -418,6 +425,40 @@ namespace Microsoft.AspNetCore.Server.HttpSys throw new InvalidOperationException(Resources.Exception_ReadOnlyStream); } + // Called before each read + private void CheckSizeLimit() + { + // Note SwitchToOpaqueMode sets HasStarted and clears _maxSize, so these limits don't apply. + if (!HasStarted) + { + var contentLength = RequestContext.Request.ContentLength; + if (contentLength.HasValue && _maxSize.HasValue && contentLength.Value > _maxSize.Value) + { + throw new IOException( + $"The request's Content-Length {contentLength.Value} is larger than the request body size limit {_maxSize.Value}."); + } + + HasStarted = true; + } + else if (TryCheckSizeLimit(0, out var exception)) + { + throw exception; + } + } + + // Called after each read. + internal bool TryCheckSizeLimit(int bytesRead, out Exception exception) + { + _totalRead += bytesRead; + if (_maxSize.HasValue && _totalRead > _maxSize.Value) + { + exception = new IOException($"The total number of bytes read {_totalRead} has exceeded the request body size limit {_maxSize.Value}."); + return true; + } + exception = null; + return false; + } + protected override void Dispose(bool disposing) { try diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStreamAsyncResult.cs b/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStreamAsyncResult.cs index 99f04d3fd9..5304f616f1 100644 --- a/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStreamAsyncResult.cs +++ b/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStreamAsyncResult.cs @@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } catch (Exception e) { - asyncResult.Fail(e); + asyncResult.Fail(new IOException(string.Empty, e)); } } @@ -112,7 +112,11 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal void Complete(int read, uint errorCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) { - if (_tcs.TrySetResult(read + (int)DataAlreadyRead)) + if (_requestStream.TryCheckSizeLimit(read + (int)DataAlreadyRead, out var exception)) + { + _tcs.TrySetException(exception); + } + else if (_tcs.TrySetResult(read + (int)DataAlreadyRead)) { RequestStream.UpdateAfterRead((uint)errorCode, (uint)(read + DataAlreadyRead)); if (_callback != null) diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/StandardFeatureCollection.cs b/src/Microsoft.AspNetCore.Server.HttpSys/StandardFeatureCollection.cs index 710286fc9d..d0fa5789dc 100644 --- a/src/Microsoft.AspNetCore.Server.HttpSys/StandardFeatureCollection.cs +++ b/src/Microsoft.AspNetCore.Server.HttpSys/StandardFeatureCollection.cs @@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys { typeof(IHttpAuthenticationFeature), _identityFunc }, { typeof(IHttpRequestIdentifierFeature), _identityFunc }, { typeof(RequestContext), ctx => ctx.RequestContext }, + { typeof(IHttpMaxRequestBodySizeFeature), _identityFunc }, }; private readonly FeatureContext _featureContext; diff --git a/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestBodyLimitTests.cs b/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestBodyLimitTests.cs new file mode 100644 index 0000000000..039e81a1a4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestBodyLimitTests.cs @@ -0,0 +1,422 @@ +// 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.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.HttpSys +{ + public class RequestBodyLimitTests + { + [ConditionalFact] + public async Task ContentLengthEqualsLimit_ReadSync_Success() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Equal(11, httpContext.Request.ContentLength); + byte[] input = new byte[100]; + int read = httpContext.Request.Body.Read(input, 0, input.Length); + httpContext.Response.ContentLength = read; + httpContext.Response.Body.Write(input, 0, read); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address, "Hello World"); + Assert.Equal("Hello World", response); + } + } + + [ConditionalFact] + public async Task ContentLengthEqualsLimit_ReadAync_Success() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, async httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Equal(11, httpContext.Request.ContentLength); + byte[] input = new byte[100]; + int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length); + httpContext.Response.ContentLength = read; + await httpContext.Response.Body.WriteAsync(input, 0, read); + })) + { + var response = await SendRequestAsync(address, "Hello World"); + Assert.Equal("Hello World", response); + } + } + + [ConditionalFact] + public async Task ContentLengthEqualsLimit_ReadBeginEnd_Success() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Equal(11, httpContext.Request.ContentLength); + byte[] input = new byte[100]; + int read = httpContext.Request.Body.EndRead(httpContext.Request.Body.BeginRead(input, 0, input.Length, null, null)); + httpContext.Response.ContentLength = read; + httpContext.Response.Body.EndWrite(httpContext.Response.Body.BeginWrite(input, 0, read, null, null)); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address, "Hello World"); + Assert.Equal("Hello World", response); + } + } + + [ConditionalFact] + public async Task ChunkedEqualsLimit_ReadSync_Success() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Null(httpContext.Request.ContentLength); + byte[] input = new byte[100]; + int read = httpContext.Request.Body.Read(input, 0, input.Length); + httpContext.Response.ContentLength = read; + httpContext.Response.Body.Write(input, 0, read); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address, "Hello World", chunked: true); + Assert.Equal("Hello World", response); + } + } + + [ConditionalFact] + public async Task ChunkedEqualsLimit_ReadAync_Success() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, async httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Null(httpContext.Request.ContentLength); + byte[] input = new byte[100]; + int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length); + httpContext.Response.ContentLength = read; + await httpContext.Response.Body.WriteAsync(input, 0, read); + })) + { + var response = await SendRequestAsync(address, "Hello World", chunked: true); + Assert.Equal("Hello World", response); + } + } + + [ConditionalFact] + public async Task ChunkedEqualsLimit_ReadBeginEnd_Success() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Null(httpContext.Request.ContentLength); + byte[] input = new byte[100]; + int read = httpContext.Request.Body.EndRead(httpContext.Request.Body.BeginRead(input, 0, input.Length, null, null)); + httpContext.Response.ContentLength = read; + httpContext.Response.Body.EndWrite(httpContext.Response.Body.BeginWrite(input, 0, read, null, null)); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address, "Hello World", chunked: true); + Assert.Equal("Hello World", response); + } + } + + [ConditionalFact] + public async Task ContentLengthExceedsLimit_ReadSync_ThrowsImmidately() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Equal(11, httpContext.Request.ContentLength); + byte[] input = new byte[100]; + var ex = Assert.Throws(() => httpContext.Request.Body.Read(input, 0, input.Length)); + Assert.Equal("The request's Content-Length 11 is larger than the request body size limit 10.", ex.Message); + ex = Assert.Throws(() => httpContext.Request.Body.Read(input, 0, input.Length)); + Assert.Equal("The request's Content-Length 11 is larger than the request body size limit 10.", ex.Message); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address, "Hello World"); + Assert.Equal(string.Empty, response); + } + } + + [ConditionalFact] + public async Task ContentLengthExceedsLimit_ReadAsync_ThrowsImmidately() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Equal(11, httpContext.Request.ContentLength); + byte[] input = new byte[100]; + var ex = Assert.Throws(() => { var t = httpContext.Request.Body.ReadAsync(input, 0, input.Length); }); + Assert.Equal("The request's Content-Length 11 is larger than the request body size limit 10.", ex.Message); + ex = Assert.Throws(() => { var t = httpContext.Request.Body.ReadAsync(input, 0, input.Length); }); + Assert.Equal("The request's Content-Length 11 is larger than the request body size limit 10.", ex.Message); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address, "Hello World"); + Assert.Equal(string.Empty, response); + } + } + + [ConditionalFact] + public async Task ContentLengthExceedsLimit_ReadBeginEnd_ThrowsImmidately() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Equal(11, httpContext.Request.ContentLength); + byte[] input = new byte[100]; + var ex = Assert.Throws(() => httpContext.Request.Body.BeginRead(input, 0, input.Length, null, null)); + Assert.Equal("The request's Content-Length 11 is larger than the request body size limit 10.", ex.Message); + ex = Assert.Throws(() => httpContext.Request.Body.BeginRead(input, 0, input.Length, null, null)); + Assert.Equal("The request's Content-Length 11 is larger than the request body size limit 10.", ex.Message); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address, "Hello World"); + Assert.Equal(string.Empty, response); + } + } + + [ConditionalFact] + public async Task ChunkedExceedsLimit_ReadSync_ThrowsAtLimit() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Null(httpContext.Request.ContentLength); + byte[] input = new byte[100]; + var ex = Assert.Throws(() => httpContext.Request.Body.Read(input, 0, input.Length)); + Assert.Equal("The total number of bytes read 11 has exceeded the request body size limit 10.", ex.Message); + ex = Assert.Throws(() => httpContext.Request.Body.Read(input, 0, input.Length)); + Assert.Equal("The total number of bytes read 11 has exceeded the request body size limit 10.", ex.Message); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address, "Hello World", chunked: true); + Assert.Equal(string.Empty, response); + } + } + + [ConditionalFact] + public async Task ChunkedExceedsLimit_ReadAsync_ThrowsAtLimit() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, async httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Null(httpContext.Request.ContentLength); + byte[] input = new byte[100]; + var ex = await Assert.ThrowsAsync(() => httpContext.Request.Body.ReadAsync(input, 0, input.Length)); + Assert.Equal("The total number of bytes read 11 has exceeded the request body size limit 10.", ex.Message); + ex = await Assert.ThrowsAsync(() => httpContext.Request.Body.ReadAsync(input, 0, input.Length)); + Assert.Equal("The total number of bytes read 11 has exceeded the request body size limit 10.", ex.Message); + })) + { + var response = await SendRequestAsync(address, "Hello World", chunked: true); + Assert.Equal(string.Empty, response); + } + } + + [ConditionalFact] + public async Task ChunkedExceedsLimit_ReadBeginEnd_ThrowsAtLimit() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Null(httpContext.Request.ContentLength); + byte[] input = new byte[100]; + var body = httpContext.Request.Body; + var ex = Assert.Throws(() => body.EndRead(body.BeginRead(input, 0, input.Length, null, null))); + Assert.Equal("The total number of bytes read 11 has exceeded the request body size limit 10.", ex.Message); + ex = Assert.Throws(() => body.EndRead(body.BeginRead(input, 0, input.Length, null, null))); + Assert.Equal("The total number of bytes read 11 has exceeded the request body size limit 10.", ex.Message); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address, "Hello World", chunked: true); + Assert.Equal(string.Empty, response); + } + } + + [ConditionalFact] + public async Task Chunked_ReadSyncPartialBodyUnderLimit_ThrowsAfterLimit() + { + var content = new StaggardContent(); + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Null(httpContext.Request.ContentLength); + byte[] input = new byte[100]; + int read = httpContext.Request.Body.Read(input, 0, input.Length); + Assert.Equal(10, read); + content.Block.Release(); + var ex = Assert.Throws(() => httpContext.Request.Body.Read(input, 0, input.Length)); + Assert.Equal("The total number of bytes read 20 has exceeded the request body size limit 10.", ex.Message); + return Task.FromResult(0); + })) + { + string response = await SendRequestAsync(address, content, chunked: true); + Assert.Equal(string.Empty, response); + } + } + + [ConditionalFact] + public async Task Chunked_ReadAsyncPartialBodyUnderLimit_ThrowsAfterLimit() + { + var content = new StaggardContent(); + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, async httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Null(httpContext.Request.ContentLength); + byte[] input = new byte[100]; + int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length); + Assert.Equal(10, read); + content.Block.Release(); + var ex = await Assert.ThrowsAsync(() => httpContext.Request.Body.ReadAsync(input, 0, input.Length)); + Assert.Equal("The total number of bytes read 20 has exceeded the request body size limit 10.", ex.Message); + })) + { + string response = await SendRequestAsync(address, content, chunked: true); + Assert.Equal(string.Empty, response); + } + } + + [ConditionalFact] + public async Task AdjustLimitPerRequest_ContentLength_ReadAync_Success() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, async httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Equal(11, feature.MaxRequestBodySize); + feature.MaxRequestBodySize = 12; + Assert.Equal(12, httpContext.Request.ContentLength); + byte[] input = new byte[100]; + int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length); + Assert.True(feature.IsReadOnly); + httpContext.Response.ContentLength = read; + await httpContext.Response.Body.WriteAsync(input, 0, read); + })) + { + var response = await SendRequestAsync(address, "Hello World!"); + Assert.Equal("Hello World!", response); + } + } + + [ConditionalFact] + public async Task AdjustLimitPerRequest_Chunked_ReadAync_Success() + { + string address; + using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, async httpContext => + { + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.False(feature.IsReadOnly); + Assert.Equal(11, feature.MaxRequestBodySize); + feature.MaxRequestBodySize = 12; + Assert.Null(httpContext.Request.ContentLength); + byte[] input = new byte[100]; + int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length); + Assert.True(feature.IsReadOnly); + httpContext.Response.ContentLength = read; + await httpContext.Response.Body.WriteAsync(input, 0, read); + })) + { + var response = await SendRequestAsync(address, "Hello World!", chunked: true); + Assert.Equal("Hello World!", response); + } + } + + private Task SendRequestAsync(string uri, string upload, bool chunked = false) + { + return SendRequestAsync(uri, new StringContent(upload), chunked); + } + + private async Task SendRequestAsync(string uri, HttpContent content, bool chunked = false) + { + using (HttpClient client = new HttpClient()) + { + client.DefaultRequestHeaders.TransferEncodingChunked = chunked; + HttpResponseMessage response = await client.PostAsync(uri, content); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + } + + private class StaggardContent : HttpContent + { + public StaggardContent() + { + Block = new SemaphoreSlim(0, 1); + } + + public SemaphoreSlim Block { get; private set; } + + protected async override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + await stream.WriteAsync(new byte[10], 0, 10); + Assert.True(await Block.WaitAsync(TimeSpan.FromSeconds(10))); + await stream.WriteAsync(new byte[10], 0, 10); + } + + protected override bool TryComputeLength(out long length) + { + length = 10; + return true; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Utilities.cs b/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Utilities.cs index c1e2062890..cecc4e270a 100644 --- a/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Utilities.cs +++ b/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Utilities.cs @@ -27,25 +27,41 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal static IServer CreateHttpServer(out string baseAddress, RequestDelegate app) { string root; - return CreateDynamicHttpServer(string.Empty, AuthenticationSchemes.None, true, out root, out baseAddress, app); + return CreateDynamicHttpServer(string.Empty, out root, out baseAddress, options => { }, app); + } + + internal static IServer CreateHttpServer(out string baseAddress, Action configureOptions, RequestDelegate app) + { + string root; + return CreateDynamicHttpServer(string.Empty, out root, out baseAddress, configureOptions, app); } internal static IServer CreateHttpServerReturnRoot(string path, out string root, RequestDelegate app) { string baseAddress; - return CreateDynamicHttpServer(path, AuthenticationSchemes.None, true, out root, out baseAddress, app); + return CreateDynamicHttpServer(path, out root, out baseAddress, options => { }, app); } internal static IServer CreateHttpAuthServer(AuthenticationSchemes authType, bool allowAnonymous, out string baseAddress, RequestDelegate app) { string root; - return CreateDynamicHttpServer(string.Empty, authType, allowAnonymous, out root, out baseAddress, app); + return CreateDynamicHttpServer(string.Empty, out root, out baseAddress, options => + { + options.Authentication.Schemes = authType; + options.Authentication.AllowAnonymous = allowAnonymous; + }, app); } internal static IWebHost CreateDynamicHost(AuthenticationSchemes authType, bool allowAnonymous, out string root, RequestDelegate app) - => CreateDynamicHost(string.Empty, authType, allowAnonymous, out root, out var baseAddress, app); + { + return CreateDynamicHost(string.Empty, out root, out var baseAddress, options => + { + options.Authentication.Schemes = authType; + options.Authentication.AllowAnonymous = allowAnonymous; + }, app); + } - internal static IWebHost CreateDynamicHost(string basePath, AuthenticationSchemes authType, bool allowAnonymous, out string root, out string baseAddress, RequestDelegate app) + internal static IWebHost CreateDynamicHost(string basePath, out string root, out string baseAddress, Action configureOptions, RequestDelegate app) { lock (PortLock) { @@ -60,8 +76,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys .UseHttpSys(options => { options.UrlPrefixes.Add(prefix); - options.Authentication.Schemes = authType; - options.Authentication.AllowAnonymous = allowAnonymous; + configureOptions(options); }) .Configure(appBuilder => appBuilder.Run(app)); @@ -86,7 +101,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal static MessagePump CreatePump() => new MessagePump(Options.Create(new HttpSysOptions()), new LoggerFactory(), new AuthenticationSchemeProvider(Options.Create(new AuthenticationOptions()))); - internal static IServer CreateDynamicHttpServer(string basePath, AuthenticationSchemes authType, bool allowAnonymous, out string root, out string baseAddress, RequestDelegate app) + internal static IServer CreateDynamicHttpServer(string basePath, out string root, out string baseAddress, Action configureOptions, RequestDelegate app) { lock (PortLock) { @@ -100,8 +115,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys var server = CreatePump(); server.Features.Get().Addresses.Add(baseAddress); - server.Listener.Options.Authentication.Schemes = authType; - server.Listener.Options.Authentication.AllowAnonymous = allowAnonymous; + configureOptions(server.Listener.Options); try { server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();