From aaaaf572fdf11088ee0efd205ff6834e16a36804 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Sun, 5 May 2019 19:30:12 -0700 Subject: [PATCH] Implement MaxRequestBodySize feature for IIS inprocess (#9475) --- .../CommonLib/ConfigurationSource.h | 1 + .../InProcessOptions.cpp | 19 + .../InProcessOptions.h | 7 + .../inprocessapplication.cpp | 2 +- .../managedexports.cpp | 18 +- .../IIS.Performance/PlaintextBenchmark.cs | 2 +- ...oft.AspNetCore.Server.IIS.netcoreapp3.0.cs | 6 + src/Servers/IIS/IIS/src/AssemblyInfo.cs | 1 + .../IIS/IIS/src/BadHttpRequestException.cs | 44 +++ .../IIS/IIS/src/Core/IISConfigurationData.cs | 1 + .../Core/IISHttpContext.FeatureCollection.cs | 35 +- .../IIS/src/Core/IISHttpContext.Features.cs | 15 + .../IIS/IIS/src/Core/IISHttpContext.IO.cs | 10 +- .../IIS/IIS/src/Core/IISHttpContext.Log.cs | 8 + .../IIS/IIS/src/Core/IISHttpContext.cs | 40 ++- .../IIS/IIS/src/Core/IISHttpContextOfT.cs | 8 +- src/Servers/IIS/IIS/src/Core/IISHttpServer.cs | 5 + src/Servers/IIS/IIS/src/CoreStrings.resx | 18 + src/Servers/IIS/IIS/src/IISServerOptions.cs | 31 ++ .../src/Properties/CoreStrings.Designer.cs | 84 +++++ .../IIS/IIS/src/RequestRejectionReason.cs | 10 + .../IIS/src/WebHostBuilderIISExtensions.cs | 1 + .../Inprocess/MaxRequestBodySizeTests.cs | 102 ++++++ ...rwardsCompatibility.FunctionalTests.csproj | 2 +- .../IIS.FunctionalTests.csproj | 2 +- .../test/IIS.Tests/MaxRequestBodySizeTests.cs | 339 ++++++++++++++++++ .../test/IIS.Tests/Utilities/TestServer.cs | 27 +- .../IISExpress.FunctionalTests.csproj | 1 + .../InProcessWebSite.csproj | 1 + .../testassets/InProcessWebSite/Program.cs | 21 ++ .../testassets/InProcessWebSite/Startup.cs | 1 - .../IntegrationTesting.IIS/src/IISDeployer.cs | 3 - .../RequestProcessing/RequestUriBuilder.cs | 2 +- 33 files changed, 832 insertions(+), 35 deletions(-) create mode 100644 src/Servers/IIS/IIS/src/BadHttpRequestException.cs create mode 100644 src/Servers/IIS/IIS/src/RequestRejectionReason.cs create mode 100644 src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/MaxRequestBodySizeTests.cs create mode 100644 src/Servers/IIS/IIS/test/IIS.Tests/MaxRequestBodySizeTests.cs diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ConfigurationSource.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ConfigurationSource.h index 53ab5a5218..a0145c9afe 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ConfigurationSource.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ConfigurationSource.h @@ -13,6 +13,7 @@ #define CS_WINDOWS_AUTHENTICATION_SECTION L"system.webServer/security/authentication/windowsAuthentication" #define CS_BASIC_AUTHENTICATION_SECTION L"system.webServer/security/authentication/basicAuthentication" #define CS_ANONYMOUS_AUTHENTICATION_SECTION L"system.webServer/security/authentication/anonymousAuthentication" +#define CS_MAX_REQUEST_BODY_SIZE_SECTION L"system.webServer/security/requestFiltering" class ConfigurationSource: NonCopyable { diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp index 3c77516e90..d0e264e0f8 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp @@ -44,6 +44,7 @@ InProcessOptions::InProcessOptions(const ConfigurationSource &configurationSourc m_fWindowsAuthEnabled(false), m_fBasicAuthEnabled(false), m_fAnonymousAuthEnabled(false), + m_dwMaxRequestBodySize(INFINITE), m_dwStartupTimeLimitInMS(INFINITE), m_dwShutdownTimeLimitInMS(INFINITE) { @@ -71,6 +72,24 @@ InProcessOptions::InProcessOptions(const ConfigurationSource &configurationSourc const auto anonAuthSection = configurationSource.GetSection(CS_ANONYMOUS_AUTHENTICATION_SECTION); m_fAnonymousAuthEnabled = anonAuthSection && anonAuthSection->GetBool(CS_ENABLED).value_or(false); + const auto requestFilteringSection = configurationSource.GetSection(CS_MAX_REQUEST_BODY_SIZE_SECTION); + if (requestFilteringSection != nullptr) + { + // The requestFiltering section is enabled by default in most scenarios. However, if the value + // maxAllowedContentLength isn't set, it defaults to 30_000_000 in IIS. + // The section element won't be defined if the feature is disabled, so the presence of the section tells + // us whether there should be a default or not. + auto requestLimitSection = requestFilteringSection->GetSection(L"requestLimits").value_or(nullptr); + if (requestLimitSection != nullptr) + { + m_dwMaxRequestBodySize = requestLimitSection->GetLong(L"maxAllowedContentLength").value_or(30000000); + } + else + { + m_dwMaxRequestBodySize = 30000000; + } + } + if (pSite != nullptr) { m_bindingInformation = BindingInformation::Load(configurationSource, *pSite); diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h index d6eec81d19..6cb8fcceef 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h @@ -94,6 +94,12 @@ public: return m_dwShutdownTimeLimitInMS; } + DWORD + QueryMaxRequestBodySizeLimit() const + { + return m_dwMaxRequestBodySize; + } + const std::map& QueryEnvironmentVariables() const { @@ -128,6 +134,7 @@ private: bool m_fAnonymousAuthEnabled; DWORD m_dwStartupTimeLimitInMS; DWORD m_dwShutdownTimeLimitInMS; + DWORD m_dwMaxRequestBodySize; std::map m_environmentVariables; std::vector m_bindingInformation; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp index d8991843d1..afb60a8892 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp @@ -252,7 +252,7 @@ IN_PROCESS_APPLICATION::ExecuteApplication() } // Used to make .NET Runtime always log to event log when there is an unhandled exception. - LOG_LAST_ERROR_IF(SetEnvironmentVariable(L"COMPlus_UseEntryPointFilter", L"1")); + LOG_LAST_ERROR_IF(!SetEnvironmentVariable(L"COMPlus_UseEntryPointFilter", L"1")); bool clrThreadExited; { diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp index 48bddcc827..e52ec8c8e4 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp @@ -191,12 +191,13 @@ struct IISConfigurationData BOOL fBasicAuthEnabled; BOOL fAnonymousAuthEnable; BSTR pwzBindings; + DWORD maxRequestBodySize; }; EXTERN_C __MIDL_DECLSPEC_DLLEXPORT HRESULT http_get_application_properties( - _In_ IISConfigurationData* pIISCofigurationData + _In_ IISConfigurationData* pIISConfigurationData ) { auto pInProcessApplication = IN_PROCESS_APPLICATION::GetInstance(); @@ -207,15 +208,16 @@ http_get_application_properties( const auto& pConfiguration = pInProcessApplication->QueryConfig(); - pIISCofigurationData->pInProcessApplication = pInProcessApplication; - pIISCofigurationData->pwzFullApplicationPath = SysAllocString(pInProcessApplication->QueryApplicationPhysicalPath().c_str()); - pIISCofigurationData->pwzVirtualApplicationPath = SysAllocString(pInProcessApplication->QueryApplicationVirtualPath().c_str()); - pIISCofigurationData->fWindowsAuthEnabled = pConfiguration.QueryWindowsAuthEnabled(); - pIISCofigurationData->fBasicAuthEnabled = pConfiguration.QueryBasicAuthEnabled(); - pIISCofigurationData->fAnonymousAuthEnable = pConfiguration.QueryAnonymousAuthEnabled(); + pIISConfigurationData->pInProcessApplication = pInProcessApplication; + pIISConfigurationData->pwzFullApplicationPath = SysAllocString(pInProcessApplication->QueryApplicationPhysicalPath().c_str()); + pIISConfigurationData->pwzVirtualApplicationPath = SysAllocString(pInProcessApplication->QueryApplicationVirtualPath().c_str()); + pIISConfigurationData->fWindowsAuthEnabled = pConfiguration.QueryWindowsAuthEnabled(); + pIISConfigurationData->fBasicAuthEnabled = pConfiguration.QueryBasicAuthEnabled(); + pIISConfigurationData->fAnonymousAuthEnable = pConfiguration.QueryAnonymousAuthEnabled(); auto const serverAddresses = BindingInformation::Format(pConfiguration.QueryBindings(), pInProcessApplication->QueryApplicationVirtualPath()); - pIISCofigurationData->pwzBindings = SysAllocString(serverAddresses.c_str()); + pIISConfigurationData->pwzBindings = SysAllocString(serverAddresses.c_str()); + pIISConfigurationData->maxRequestBodySize = pInProcessApplication->QueryConfig().QueryMaxRequestBodySizeLimit(); return S_OK; } diff --git a/src/Servers/IIS/IIS/benchmarks/IIS.Performance/PlaintextBenchmark.cs b/src/Servers/IIS/IIS/benchmarks/IIS.Performance/PlaintextBenchmark.cs index 8c6b4b2cd3..be6c8d9da3 100644 --- a/src/Servers/IIS/IIS/benchmarks/IIS.Performance/PlaintextBenchmark.cs +++ b/src/Servers/IIS/IIS/benchmarks/IIS.Performance/PlaintextBenchmark.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Performance [GlobalSetup] public void Setup() { - _server = TestServer.Create(builder => builder.UseMiddleware(), new LoggerFactory()).GetAwaiter().GetResult(); + _server = TestServer.Create(builder => builder.UseMiddleware(), new LoggerFactory(), new IISServerOptions()).GetAwaiter().GetResult(); // Recreate client, TestServer.Client has additional logging that can hurt performance _client = new HttpClient() { diff --git a/src/Servers/IIS/IIS/ref/Microsoft.AspNetCore.Server.IIS.netcoreapp3.0.cs b/src/Servers/IIS/IIS/ref/Microsoft.AspNetCore.Server.IIS.netcoreapp3.0.cs index ec2f2b5b7e..7294851a43 100644 --- a/src/Servers/IIS/IIS/ref/Microsoft.AspNetCore.Server.IIS.netcoreapp3.0.cs +++ b/src/Servers/IIS/IIS/ref/Microsoft.AspNetCore.Server.IIS.netcoreapp3.0.cs @@ -9,6 +9,7 @@ namespace Microsoft.AspNetCore.Builder public bool AllowSynchronousIO { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public string AuthenticationDisplayName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public bool AutomaticAuthentication { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public long? MaxRequestBodySize { get { throw null; } set { } } } } namespace Microsoft.AspNetCore.Hosting @@ -27,6 +28,11 @@ namespace Microsoft.AspNetCore.Http.Features } namespace Microsoft.AspNetCore.Server.IIS { + public sealed partial class BadHttpRequestException : System.IO.IOException + { + internal BadHttpRequestException() { } + public int StatusCode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } public static partial class HttpContextExtensions { public static string GetIISServerVariable(this Microsoft.AspNetCore.Http.HttpContext context, string variableName) { throw null; } diff --git a/src/Servers/IIS/IIS/src/AssemblyInfo.cs b/src/Servers/IIS/IIS/src/AssemblyInfo.cs index f08839f842..5f58cf6c8e 100644 --- a/src/Servers/IIS/IIS/src/AssemblyInfo.cs +++ b/src/Servers/IIS/IIS/src/AssemblyInfo.cs @@ -4,4 +4,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.IISIntegration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("IIS.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Servers/IIS/IIS/src/BadHttpRequestException.cs b/src/Servers/IIS/IIS/src/BadHttpRequestException.cs new file mode 100644 index 0000000000..527692f89c --- /dev/null +++ b/src/Servers/IIS/IIS/src/BadHttpRequestException.cs @@ -0,0 +1,44 @@ +// 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.IO; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Server.IIS +{ + public sealed class BadHttpRequestException : IOException + { + private BadHttpRequestException(string message, int statusCode, RequestRejectionReason reason) + : base(message) + { + StatusCode = statusCode; + Reason = reason; + } + + public int StatusCode { get; } + + internal RequestRejectionReason Reason { get; } + + internal static void Throw(RequestRejectionReason reason) + { + throw GetException(reason); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static BadHttpRequestException GetException(RequestRejectionReason reason) + { + BadHttpRequestException ex; + switch (reason) + { + case RequestRejectionReason.RequestBodyTooLarge: + ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestBodyTooLarge, StatusCodes.Status413PayloadTooLarge, reason); + break; + default: + ex = new BadHttpRequestException(CoreStrings.BadRequest, StatusCodes.Status400BadRequest, reason); + break; + } + return ex; + } + } +} diff --git a/src/Servers/IIS/IIS/src/Core/IISConfigurationData.cs b/src/Servers/IIS/IIS/src/Core/IISConfigurationData.cs index 882651622e..21d00ed097 100644 --- a/src/Servers/IIS/IIS/src/Core/IISConfigurationData.cs +++ b/src/Servers/IIS/IIS/src/Core/IISConfigurationData.cs @@ -19,5 +19,6 @@ namespace Microsoft.AspNetCore.Server.IIS.Core public bool fAnonymousAuthEnable; [MarshalAs(UnmanagedType.BStr)] public string pwzBindings; + public int maxRequestBodySize; } } diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs index 1f872b655f..a208ff48ac 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.AspNetCore.Server.IIS.Core.IO; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.IIS.Core { @@ -28,7 +29,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core IServerVariablesFeature, IHttpBufferingFeature, ITlsConnectionFeature, - IHttpBodyControlFeature + IHttpBodyControlFeature, + IHttpMaxRequestBodySizeFeature { // NOTE: When feature interfaces are added to or removed from this HttpProtocol implementation, // then the list of `implementedFeatures` in the generated code project MUST also be updated. @@ -277,7 +279,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core Debug.Assert(_readBodyTask == null || _readBodyTask.IsCompleted); // Reset reading status to allow restarting with new IO - _hasRequestReadingStarted = false; + HasStartedConsumingRequestBody = false; // Upgrade async will cause the stream processing to go into duplex mode AsyncIO = new WebSocketsAsyncIOEngine(_contextLock, _pInProcessHandler); @@ -322,6 +324,35 @@ namespace Microsoft.AspNetCore.Server.IIS.Core bool IHttpBodyControlFeature.AllowSynchronousIO { get; set; } + 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); + } + + if (value > _options.IisMaxRequestSizeLimit) + { + _logger.LogWarning(CoreStrings.MaxRequestLimitWarning); + } + + MaxRequestBodySize = value; + } + } + void IHttpBufferingFeature.DisableRequestBuffering() { } diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs index 6e107e03f0..71ca7e202e 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs @@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private static readonly Type IISHttpContextType = typeof(IISHttpContext); private static readonly Type IServerVariablesFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IServerVariablesFeature); private static readonly Type IHttpBufferingFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBufferingFeature); + private static readonly Type IHttpMaxRequestBodySizeFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature); private object _currentIHttpRequestFeature; private object _currentIHttpResponseFeature; @@ -48,6 +49,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private object _currentIHttpSendFileFeature; private object _currentIServerVariablesFeature; private object _currentIHttpBufferingFeature; + private object _currentIHttpMaxRequestBodySizeFeature; private void Initialize() { @@ -61,6 +63,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _currentIHttpAuthenticationFeature = this; _currentIServerVariablesFeature = this; _currentIHttpBufferingFeature = this; + _currentIHttpMaxRequestBodySizeFeature = this; _currentITlsConnectionFeature = this; } @@ -146,6 +149,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core { return _currentIHttpBufferingFeature; } + if (key == IHttpMaxRequestBodySizeFeature) + { + return _currentIHttpMaxRequestBodySizeFeature; + } return ExtraFeatureGet(key); } @@ -249,6 +256,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _currentIHttpBufferingFeature = feature; return; } + if (key == IHttpMaxRequestBodySizeFeature) + { + _currentIHttpMaxRequestBodySizeFeature = feature; + } if (key == IISHttpContextType) { throw new InvalidOperationException("Cannot set IISHttpContext in feature collection"); @@ -334,6 +345,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core { yield return new KeyValuePair(IHttpBufferingFeature, _currentIHttpBufferingFeature as global::Microsoft.AspNetCore.Http.Features.IHttpBufferingFeature); } + if (_currentIHttpMaxRequestBodySizeFeature != null) + { + yield return new KeyValuePair(IHttpMaxRequestBodySizeFeature, _currentIHttpMaxRequestBodySizeFeature as global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature); + } if (MaybeExtra != null) { diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs index feea13e2e2..68141058c5 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs @@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core { internal partial class IISHttpContext { + private long _consumedBytes; + /// /// Reads data from the Input pipe to the user. /// @@ -22,7 +24,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core /// internal async ValueTask ReadAsync(Memory memory, CancellationToken cancellationToken) { - if (!_hasRequestReadingStarted) + if (!HasStartedConsumingRequestBody) { InitializeRequestIO(); } @@ -105,9 +107,15 @@ namespace Microsoft.AspNetCore.Server.IIS.Core // Read was not canceled because of incoming write or IO stopping if (read != -1) { + _consumedBytes += read; _bodyInputPipe.Writer.Advance(read); } + if (_consumedBytes > MaxRequestBodySize) + { + BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); + } + var result = await _bodyInputPipe.Writer.FlushAsync(); if (result.IsCompleted || result.IsCanceled) diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs index 1056ac1479..4f84786cf0 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs @@ -20,6 +20,9 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private static readonly Action _unexpectedError = LoggerMessage.Define(LogLevel.Error, new EventId(3, "UnexpectedError"), @"Unexpected exception in ""{ClassName}.{MethodName}""."); + private static readonly Action _connectionBadRequest = + LoggerMessage.Define(LogLevel.Information, new EventId(4, nameof(ConnectionBadRequest)), @"Connection id ""{ConnectionId}"" bad request data: ""{message}"""); + public static void ConnectionDisconnect(ILogger logger, string connectionId) { _connectionDisconnect(logger, connectionId, null); @@ -34,6 +37,11 @@ namespace Microsoft.AspNetCore.Server.IIS.Core { _unexpectedError(logger, className, methodName, ex); } + + public static void ConnectionBadRequest(ILogger logger, string connectionId, BadHttpRequestException ex) + { + _connectionBadRequest(logger, connectionId, ex.Message, ex); + } } } } diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs index 7286288427..a6fbdb8d97 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -39,7 +39,6 @@ namespace Microsoft.AspNetCore.Server.IIS.Core protected Streams _streams; private volatile bool _hasResponseStarted; - private volatile bool _hasRequestReadingStarted; private int _statusCode; private string _reasonPhrase; @@ -50,6 +49,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core protected Stack, object>> _onCompleted; protected Exception _applicationException; + protected BadHttpRequestException _requestRejectedException; + private readonly MemoryPool _memoryPool; private readonly IISHttpServer _server; @@ -112,6 +113,9 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private HeaderCollection HttpResponseHeaders { get; set; } internal HttpApiTypes.HTTP_VERB KnownMethod { get; private set; } + private bool HasStartedConsumingRequestBody { get; set; } + public long? MaxRequestBodySize { get; set; } + protected void InitializeContext() { _thisHandle = GCHandle.Alloc(this); @@ -156,6 +160,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core } } + MaxRequestBodySize = _options.MaxRequestBodySize; + ResetFeatureCollection(); if (!_server.IsWebSocketAvailable(_pInProcessHandler)) @@ -282,9 +288,14 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private void InitializeRequestIO() { - Debug.Assert(!_hasRequestReadingStarted); + Debug.Assert(!HasStartedConsumingRequestBody); - _hasRequestReadingStarted = true; + if (RequestHeaders.ContentLength > MaxRequestBodySize) + { + BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); + } + + HasStartedConsumingRequestBody = true; EnsureIOInitialized(); @@ -308,7 +319,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core protected Task ProduceEnd() { - if (_applicationException != null) + if (_requestRejectedException != null || _applicationException != null) { if (HasResponseStarted) { @@ -318,6 +329,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core // If the request was rejected, the error state has already been set by SetBadRequestState and // that should take precedence. + if (_requestRejectedException != null) + { + SetErrorResponseException(_requestRejectedException); + } else { // 500 Internal Server Error @@ -461,6 +476,23 @@ namespace Microsoft.AspNetCore.Server.IIS.Core } } + public void SetBadRequestState(BadHttpRequestException ex) + { + Log.ConnectionBadRequest(_logger, RequestConnectionId, ex); + + if (!HasResponseStarted) + { + SetErrorResponseException(ex); + } + + _requestRejectedException = ex; + } + + private void SetErrorResponseException(BadHttpRequestException ex) + { + SetErrorResponseHeaders(ex.StatusCode); + } + protected void ReportApplicationError(Exception ex) { if (_applicationException == null) diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs index 8bced196b0..9ac958beea 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs @@ -34,6 +34,12 @@ namespace Microsoft.AspNetCore.Server.IIS.Core await _application.ProcessRequestAsync(context); } + catch (BadHttpRequestException ex) + { + SetBadRequestState(ex); + ReportApplicationError(ex); + success = false; + } catch (Exception ex) { ReportApplicationError(ex); @@ -59,7 +65,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core { await ProduceEnd(); } - else if (!HasResponseStarted) + else if (!HasResponseStarted && _requestRejectedException == null) { // If the request was aborted and no response was sent, there's no // meaningful status code to log. diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs index 8d47492891..cb6c3db598 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs @@ -81,6 +81,11 @@ namespace Microsoft.AspNetCore.Server.IIS.Core } Features.Set(_serverAddressesFeature); + + if (_options.MaxRequestBodySize > _options.IisMaxRequestSizeLimit) + { + _logger.LogWarning(CoreStrings.MaxRequestLimitWarning); + } } public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) diff --git a/src/Servers/IIS/IIS/src/CoreStrings.resx b/src/Servers/IIS/IIS/src/CoreStrings.resx index d6c2e7b974..3f845cc74e 100644 --- a/src/Servers/IIS/IIS/src/CoreStrings.resx +++ b/src/Servers/IIS/IIS/src/CoreStrings.resx @@ -147,4 +147,22 @@ {name} cannot be set because the response has already started. + + Request body too large. + + + The maximum request body size cannot be modified after the app has already started reading from the request body. + + + The maximum request body size cannot be modified after the request has been upgraded. + + + Value must be null or a non-negative number. + + + Bad request. + + + Increasing the MaxRequestBodySize conflicts with the max value for IIS limit maxAllowedContentLength. HTTP requests that have a content length greater than maxAllowedContentLength will still be rejected by IIS. You can disable the limit by either removing or setting the maxAllowedContentLength value to a higher limit. + diff --git a/src/Servers/IIS/IIS/src/IISServerOptions.cs b/src/Servers/IIS/IIS/src/IISServerOptions.cs index 65f86240bd..bf83ca2e2e 100644 --- a/src/Servers/IIS/IIS/src/IISServerOptions.cs +++ b/src/Servers/IIS/IIS/src/IISServerOptions.cs @@ -1,7 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.IIS; namespace Microsoft.AspNetCore.Builder { @@ -34,5 +37,33 @@ namespace Microsoft.AspNetCore.Builder internal bool ForwardWindowsAuthentication { get; set; } = true; internal string[] ServerAddresses { get; set; } + + // Matches the default maxAllowedContentLength in IIS (~28.6 MB) + // https://www.iis.net/configreference/system.webserver/security/requestfiltering/requestlimits#005 + private long? _maxRequestBodySize = 30000000; + + internal long IisMaxRequestSizeLimit; + + /// + /// Gets or sets the maximum allowed size of any request body in bytes. + /// When set to null, the maximum request body size is unlimited. + /// This limit has no effect on upgraded connections which are always unlimited. + /// This can be overridden per-request via . + /// + /// + /// Defaults to null (unlimited). + /// + public long? MaxRequestBodySize + { + get => _maxRequestBodySize; + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.NonNegativeNumberOrNullRequired); + } + _maxRequestBodySize = value; + } + } } } diff --git a/src/Servers/IIS/IIS/src/Properties/CoreStrings.Designer.cs b/src/Servers/IIS/IIS/src/Properties/CoreStrings.Designer.cs index 639480f4a5..77b953c2f7 100644 --- a/src/Servers/IIS/IIS/src/Properties/CoreStrings.Designer.cs +++ b/src/Servers/IIS/IIS/src/Properties/CoreStrings.Designer.cs @@ -150,6 +150,90 @@ namespace Microsoft.AspNetCore.Server.IIS internal static string FormatParameterReadOnlyAfterResponseStarted(object name) => string.Format(CultureInfo.CurrentCulture, GetString("ParameterReadOnlyAfterResponseStarted", "name"), name); + /// + /// Request body too large. + /// + internal static string BadRequest_RequestBodyTooLarge + { + get => GetString("BadRequest_RequestBodyTooLarge"); + } + + /// + /// Request body too large. + /// + internal static string FormatBadRequest_RequestBodyTooLarge() + => GetString("BadRequest_RequestBodyTooLarge"); + + /// + /// The maximum request body size cannot be modified after the app has already started reading from the request body. + /// + internal static string MaxRequestBodySizeCannotBeModifiedAfterRead + { + get => GetString("MaxRequestBodySizeCannotBeModifiedAfterRead"); + } + + /// + /// The maximum request body size cannot be modified after the app has already started reading from the request body. + /// + internal static string FormatMaxRequestBodySizeCannotBeModifiedAfterRead() + => GetString("MaxRequestBodySizeCannotBeModifiedAfterRead"); + + /// + /// The maximum request body size cannot be modified after the request has been upgraded. + /// + internal static string MaxRequestBodySizeCannotBeModifiedForUpgradedRequests + { + get => GetString("MaxRequestBodySizeCannotBeModifiedForUpgradedRequests"); + } + + /// + /// The maximum request body size cannot be modified after the request has been upgraded. + /// + internal static string FormatMaxRequestBodySizeCannotBeModifiedForUpgradedRequests() + => GetString("MaxRequestBodySizeCannotBeModifiedForUpgradedRequests"); + + /// + /// Value must be null or a non-negative number. + /// + internal static string NonNegativeNumberOrNullRequired + { + get => GetString("NonNegativeNumberOrNullRequired"); + } + + /// + /// Value must be null or a non-negative number. + /// + internal static string FormatNonNegativeNumberOrNullRequired() + => GetString("NonNegativeNumberOrNullRequired"); + + /// + /// Bad request. + /// + internal static string BadRequest + { + get => GetString("BadRequest"); + } + + /// + /// Bad request. + /// + internal static string FormatBadRequest() + => GetString("BadRequest"); + + /// + /// Increasing the MaxRequestBodySize conflicts with the max value for IIS limit maxAllowedContentLength. HTTP requests that have a content length greater than maxAllowedContentLength will still be rejected by IIS. You can disable the limit by either removing or setting the maxAllowedContentLength value to a higher limit. + /// + internal static string MaxRequestLimitWarning + { + get => GetString("MaxRequestLimitWarning"); + } + + /// + /// Increasing the MaxRequestBodySize conflicts with the max value for IIS limit maxAllowedContentLength. HTTP requests that have a content length greater than maxAllowedContentLength will still be rejected by IIS. You can disable the limit by either removing or setting the maxAllowedContentLength value to a higher limit. + /// + internal static string FormatMaxRequestLimitWarning() + => GetString("MaxRequestLimitWarning"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Servers/IIS/IIS/src/RequestRejectionReason.cs b/src/Servers/IIS/IIS/src/RequestRejectionReason.cs new file mode 100644 index 0000000000..5a0697d665 --- /dev/null +++ b/src/Servers/IIS/IIS/src/RequestRejectionReason.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Server.IIS +{ + internal enum RequestRejectionReason + { + RequestBodyTooLarge + } +} diff --git a/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs b/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs index aac753dc75..d4d012454f 100644 --- a/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs +++ b/src/Servers/IIS/IIS/src/WebHostBuilderIISExtensions.cs @@ -46,6 +46,7 @@ namespace Microsoft.AspNetCore.Hosting options => { options.ServerAddresses = iisConfigData.pwzBindings.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); options.ForwardWindowsAuthentication = iisConfigData.fWindowsAuthEnabled || iisConfigData.fBasicAuthEnabled; + options.IisMaxRequestSizeLimit = iisConfigData.maxRequestBodySize; } ); }); diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/MaxRequestBodySizeTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/MaxRequestBodySizeTests.cs new file mode 100644 index 0000000000..f14574e172 --- /dev/null +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/MaxRequestBodySizeTests.cs @@ -0,0 +1,102 @@ +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class MaxRequestBodySizeTests : IISFunctionalTestBase + { + public MaxRequestBodySizeTests(PublishedSitesFixture fixture) : base(fixture) + { + } + + [ConditionalFact] + [RequiresNewHandler] + public async Task MaxRequestBodySizeE2EWorks() + { + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.TransformArguments((a, _) => $"{a} DecreaseRequestLimit"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var result = await deploymentResult.HttpClient.PostAsync("/ReadRequestBody", new StringContent("test")); + Assert.Equal(HttpStatusCode.RequestEntityTooLarge, result.StatusCode); + } + + [ConditionalFact] + [RequiresNewHandler] + public async Task SetIISLimitMaxRequestBodySizeE2EWorks() + { + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.ServerConfigActionList.Add( + (config, _) => { + config + .RequiredElement("system.webServer") + .GetOrAdd("security") + .GetOrAdd("requestFiltering") + .GetOrAdd("requestLimits", "maxAllowedContentLength", "1"); + }); + var deploymentResult = await DeployAsync(deploymentParameters); + + var result = await deploymentResult.HttpClient.PostAsync("/ReadRequestBody", new StringContent("test")); + + // IIS returns a 404 instead of a 413... + Assert.Equal(HttpStatusCode.NotFound, result.StatusCode); + } + + [ConditionalFact] + [RequiresNewHandler] + public async Task IISRejectsContentLengthTooLargeByDefault() + { + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + var deploymentResult = await DeployAsync(deploymentParameters); + + using (var connection = new TestConnection(deploymentResult.HttpClient.BaseAddress.Port)) + { + await connection.Send( + "POST /HelloWorld HTTP/1.1", + $"Content-Length: 30000001", + "Host: localhost", + "", + "A"); + await connection.Receive("HTTP/1.1 404 Not Found"); + } + } + + [ConditionalFact] + [RequiresNewHandler] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + public async Task SetIISLimitMaxRequestBodyLogsWarning() + { + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.ServerConfigActionList.Add( + (config, _) => { + config + .RequiredElement("system.webServer") + .GetOrAdd("security") + .GetOrAdd("requestFiltering") + .GetOrAdd("requestLimits", "maxAllowedContentLength", "1"); + }); + var deploymentResult = await DeployAsync(deploymentParameters); + + var result = await deploymentResult.HttpClient.PostAsync("/DecreaseRequestLimit", new StringContent("1")); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + if (deploymentParameters.ServerType == ServerType.IISExpress) + { + Assert.Single(TestSink.Writes, w => w.Message.Contains("Increasing the MaxRequestBodySize conflicts with the max value for IIS limit maxAllowedContentLength." + + " HTTP requests that have a content length greater than maxAllowedContentLength will still be rejected by IIS." + + " You can disable the limit by either removing or setting the maxAllowedContentLength value to a higher limit.")); + } + } + } +} diff --git a/src/Servers/IIS/IIS/test/IIS.ForwardsCompatibility.FunctionalTests/IIS.ForwardsCompatibility.FunctionalTests.csproj b/src/Servers/IIS/IIS/test/IIS.ForwardsCompatibility.FunctionalTests/IIS.ForwardsCompatibility.FunctionalTests.csproj index 12bb180f41..2ae2fe539b 100644 --- a/src/Servers/IIS/IIS/test/IIS.ForwardsCompatibility.FunctionalTests/IIS.ForwardsCompatibility.FunctionalTests.csproj +++ b/src/Servers/IIS/IIS/test/IIS.ForwardsCompatibility.FunctionalTests/IIS.ForwardsCompatibility.FunctionalTests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 diff --git a/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj index 2ee13ffc7e..eca49ea126 100644 --- a/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj +++ b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj @@ -1,4 +1,4 @@ - + diff --git a/src/Servers/IIS/IIS/test/IIS.Tests/MaxRequestBodySizeTests.cs b/src/Servers/IIS/IIS/test/IIS.Tests/MaxRequestBodySizeTests.cs new file mode 100644 index 0000000000..f70d47e198 --- /dev/null +++ b/src/Servers/IIS/IIS/test/IIS.Tests/MaxRequestBodySizeTests.cs @@ -0,0 +1,339 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.IIS; +using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace IIS.Tests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class MaxRequestBodySizeTests : LoggedTest + { + [ConditionalFact] + public async Task RequestBodyTooLargeContentLengthExceedsGlobalLimit() + { + var globalMaxRequestBodySize = 0x100000000; + + BadHttpRequestException exception = null; + using (var testServer = await TestServer.Create( + async ctx => + { + try + { + await ctx.Request.Body.ReadAsync(new byte[2000]); + } + catch (BadHttpRequestException ex) + { + exception = ex; + throw ex; + } + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + $"Content-Length: {globalMaxRequestBodySize + 1}", + "Host: localhost", + "", + ""); + await connection.Receive("HTTP/1.1 413 Payload Too Large"); + } + } + + Assert.Equal(CoreStrings.BadRequest_RequestBodyTooLarge, exception.Message); + } + + [ConditionalFact] + public async Task RequestBodyTooLargeContentLengthExceedingPerRequestLimit() + { + var maxRequestSize = 0x10000; + var perRequestMaxRequestBodySize = 0x100; + + BadHttpRequestException exception = null; + using (var testServer = await TestServer.Create( + async ctx => + { + try + { + var feature = ctx.Features.Get(); + Assert.Equal(maxRequestSize, feature.MaxRequestBodySize); + feature.MaxRequestBodySize = perRequestMaxRequestBodySize; + + await ctx.Request.Body.ReadAsync(new byte[2000]); + } + catch (BadHttpRequestException ex) + { + exception = ex; + throw ex; + } + }, LoggerFactory, new IISServerOptions { MaxRequestBodySize = maxRequestSize })) + { + using (var connection = testServer.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + $"Content-Length: {perRequestMaxRequestBodySize + 1}", + "Host: localhost", + "", + ""); + await connection.Receive("HTTP/1.1 413 Payload Too Large"); + } + } + + Assert.Equal(CoreStrings.BadRequest_RequestBodyTooLarge, exception.Message); + } + + [ConditionalFact] + public async Task DoesNotRejectRequestWithContentLengthHeaderExceedingGlobalLimitIfLimitDisabledPerRequest() + { + using (var testServer = await TestServer.Create( + async ctx => + { + var feature = ctx.Features.Get(); + Assert.Equal(0, feature.MaxRequestBodySize); + feature.MaxRequestBodySize = null; + + await ctx.Request.Body.ReadAsync(new byte[2000]); + + }, LoggerFactory, new IISServerOptions { MaxRequestBodySize = 0 })) + { + using (var connection = testServer.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + $"Content-Length: 1", + "Host: localhost", + "", + "A"); + await connection.Receive("HTTP/1.1 200 OK"); + } + } + } + + [ConditionalFact] + public async Task DoesNotRejectRequestWithChunkedExceedingGlobalLimitIfLimitDisabledPerRequest() + { + using (var testServer = await TestServer.Create( + async ctx => + { + var feature = ctx.Features.Get(); + Assert.Equal(0, feature.MaxRequestBodySize); + feature.MaxRequestBodySize = null; + + await ctx.Request.Body.ReadAsync(new byte[2000]); + + }, LoggerFactory, new IISServerOptions { MaxRequestBodySize = 0 })) + { + using (var connection = testServer.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + $"Transfer-Encoding: chunked", + "Host: localhost", + "", + "1", + "a", + "0", + ""); + await connection.Receive("HTTP/1.1 200 OK"); + } + } + } + + [ConditionalFact] + public async Task DoesNotRejectBodylessGetRequestWithZeroMaxRequestBodySize() + { + using (var testServer = await TestServer.Create( + async ctx => + { + await ctx.Request.Body.ReadAsync(new byte[2000]); + + }, LoggerFactory, new IISServerOptions { MaxRequestBodySize = 0 })) + { + using (var connection = testServer.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host: localhost", + "", + ""); + + await connection.Receive("HTTP/1.1 200 OK"); + } + } + } + + [ConditionalFact] + public async Task DoesNotRejectBodylessPostWithZeroContentLengthRequestWithZeroMaxRequestBodySize() + { + using (var testServer = await TestServer.Create( + async ctx => + { + await ctx.Request.Body.ReadAsync(new byte[2000]); + + }, LoggerFactory, new IISServerOptions { MaxRequestBodySize = 0 })) + { + using (var connection = testServer.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + $"Content-Length: 0", + "Host: localhost", + "", + ""); + + await connection.Receive("HTTP/1.1 200 OK"); + } + } + } + + [ConditionalFact] + public async Task DoesNotRejectBodylessPostWithEmptyChunksRequestWithZeroMaxRequestBodySize() + { + using (var testServer = await TestServer.Create( + async ctx => + { + await ctx.Request.Body.ReadAsync(new byte[2000]); + + }, LoggerFactory, new IISServerOptions { MaxRequestBodySize = 0 })) + { + using (var connection = testServer.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + $"Transfer-Encoding: chunked", + "Host: localhost", + "", + "0", + "", + ""); + + await connection.Receive("HTTP/1.1 200 OK"); + } + } + } + + + [ConditionalFact] + public async Task SettingMaxRequestBodySizeAfterReadingFromRequestBodyThrows() + { + var perRequestMaxRequestBodySize = 0x10; + var payloadSize = perRequestMaxRequestBodySize + 1; + var payload = new string('A', payloadSize); + InvalidOperationException invalidOpEx = null; + + using (var testServer = await TestServer.Create( + async ctx => + { + var buffer = new byte[1]; + Assert.Equal(1, await ctx.Request.Body.ReadAsync(buffer, 0, 1)); + + var feature = ctx.Features.Get(); + Assert.True(feature.IsReadOnly); + + invalidOpEx = Assert.Throws(() => + feature.MaxRequestBodySize = perRequestMaxRequestBodySize); + throw invalidOpEx; + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host: localhost", + "Content-Length: " + payloadSize, + "", + payload); + await connection.Receive( + "HTTP/1.1 500 Internal Server Error"); + } + } + } + + [ConditionalFact] + public async Task RequestBodyTooLargeChunked() + { + var maxRequestSize = 0x1000; + + BadHttpRequestException exception = null; + using (var testServer = await TestServer.Create( + async ctx => + { + try + { + while (true) + { + var num = await ctx.Request.Body.ReadAsync(new byte[2000]); + } + } + catch (BadHttpRequestException ex) + { + exception = ex; + throw ex; + } + }, LoggerFactory, new IISServerOptions { MaxRequestBodySize = maxRequestSize })) + { + using (var connection = testServer.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Transfer-Encoding: chunked", + "Host: localhost", + "", + "1001", + new string('a', 4097), + "0", + ""); + await connection.Receive("HTTP/1.1 413 Payload Too Large"); + } + } + + Assert.NotNull(exception); + Assert.Equal(CoreStrings.BadRequest_RequestBodyTooLarge, exception.Message); + } + + [ConditionalFact] + public async Task EveryReadFailsWhenContentLengthHeaderExceedsGlobalLimit() + { + BadHttpRequestException requestRejectedEx1 = null; + BadHttpRequestException requestRejectedEx2 = null; + using (var testServer = await TestServer.Create( + async ctx => + { + var buffer = new byte[1]; + requestRejectedEx1 = await Assert.ThrowsAsync( + async () => await ctx.Request.Body.ReadAsync(buffer, 0, 1)); + requestRejectedEx2 = await Assert.ThrowsAsync( + async () => await ctx.Request.Body.ReadAsync(buffer, 0, 1)); + throw requestRejectedEx2; + }, LoggerFactory, new IISServerOptions { MaxRequestBodySize = 0 })) + { + using (var connection = testServer.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host: localhost", + "Content-Length: " + (new IISServerOptions().MaxRequestBodySize + 1), + "", + ""); + await connection.Receive( + "HTTP/1.1 413 Payload Too Large"); + } + } + + Assert.NotNull(requestRejectedEx1); + Assert.NotNull(requestRejectedEx2); + Assert.Equal(CoreStrings.BadRequest_RequestBodyTooLarge, requestRejectedEx1.Message); + Assert.Equal(CoreStrings.BadRequest_RequestBodyTooLarge, requestRejectedEx2.Message); + } + } +} diff --git a/src/Servers/IIS/IIS/test/IIS.Tests/Utilities/TestServer.cs b/src/Servers/IIS/IIS/test/IIS.Tests/Utilities/TestServer.cs index cd283c200b..ebd99721d1 100644 --- a/src/Servers/IIS/IIS/test/IIS.Tests/Utilities/TestServer.cs +++ b/src/Servers/IIS/IIS/test/IIS.Tests/Utilities/TestServer.cs @@ -7,7 +7,6 @@ using System.IO; using System.Net.Http; using System.Reflection; using System.Runtime.InteropServices; -using System.Runtime.Loader; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; @@ -15,7 +14,6 @@ using System.Xml.XPath; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Server.IntegrationTesting; using Microsoft.AspNetCore.Server.IntegrationTesting.Common; using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; @@ -51,6 +49,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests public HttpClient HttpClient { get; private set; } public TestConnection CreateConnection() => new TestConnection(_currentPort); + private static IISServerOptions _options; private IWebHost _host; private string _appHostConfigPath; @@ -63,9 +62,10 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests _loggerFactory = loggerFactory; } - public static async Task Create(Action appBuilder, ILoggerFactory loggerFactory) + public static async Task Create(Action appBuilder, ILoggerFactory loggerFactory, IISServerOptions options) { await WebCoreLock.WaitAsync(); + _options = options; var server = new TestServer(appBuilder, loggerFactory); server.Start(); (await server.HttpClient.GetAsync("/start")).EnsureSuccessStatusCode(); @@ -75,7 +75,12 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests public static Task Create(RequestDelegate app, ILoggerFactory loggerFactory) { - return Create(builder => builder.Run(app), loggerFactory); + return Create(builder => builder.Run(app), loggerFactory, new IISServerOptions()); + } + + public static Task Create(RequestDelegate app, ILoggerFactory loggerFactory, IISServerOptions options) + { + return Create(builder => builder.Run(app), loggerFactory, options); } private void Start() @@ -125,14 +130,16 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests private int Main(IntPtr argc, IntPtr argv) { - _host = new WebHostBuilder() + var builder = new WebHostBuilder() .UseIIS() - .ConfigureServices(services => { - services.AddSingleton(this); - services.AddSingleton(_loggerFactory); + .ConfigureServices(services => + { + services.Configure(options => options.MaxRequestBodySize = _options.MaxRequestBodySize); + services.AddSingleton(this); + services.AddSingleton(_loggerFactory); }) - .UseSetting(WebHostDefaults.ApplicationKey, typeof(TestServer).GetTypeInfo().Assembly.FullName) - .Build(); + .UseSetting(WebHostDefaults.ApplicationKey, typeof(TestServer).GetTypeInfo().Assembly.FullName); + _host = builder.Build(); var doneEvent = new ManualResetEventSlim(); var lifetime = _host.Services.GetService(); diff --git a/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj b/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj index ae792eb6e8..80704a53b0 100644 --- a/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj +++ b/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessForwardsCompatWebSite/InProcessWebSite.csproj b/src/Servers/IIS/IIS/test/testassets/InProcessForwardsCompatWebSite/InProcessWebSite.csproj index 233c678ae4..3deabbaa7e 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessForwardsCompatWebSite/InProcessWebSite.csproj +++ b/src/Servers/IIS/IIS/test/testassets/InProcessForwardsCompatWebSite/InProcessWebSite.csproj @@ -5,6 +5,7 @@ netcoreapp3.0 InProcessForwardsCompatWebSite + FORWARDCOMPAT diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Program.cs b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Program.cs index 59e35c3a28..3ece63ede5 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Program.cs +++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Program.cs @@ -100,6 +100,27 @@ namespace TestSite case "ConsoleWriteStartServer": Console.WriteLine("TEST MESSAGE"); return StartServer(); +#if !FORWARDCOMPAT + case "DecreaseRequestLimit": + { + var host = new WebHostBuilder() + .ConfigureLogging((_, factory) => + { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + }) + .UseIIS() + .ConfigureServices(services => + { + services.Configure(options => options.MaxRequestBodySize = 2); + }) + .UseStartup() + .Build(); + + host.Run(); + break; + } +#endif default: return StartServer(); diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs index 0e925106bf..110ac5e625 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs +++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs @@ -284,7 +284,6 @@ namespace TestSite { result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 1); } - } private int _requestsInFlight = 0; diff --git a/src/Servers/IIS/IntegrationTesting.IIS/src/IISDeployer.cs b/src/Servers/IIS/IntegrationTesting.IIS/src/IISDeployer.cs index 0f551b7658..acfcaecc42 100644 --- a/src/Servers/IIS/IntegrationTesting.IIS/src/IISDeployer.cs +++ b/src/Servers/IIS/IntegrationTesting.IIS/src/IISDeployer.cs @@ -153,7 +153,6 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS { try { - // Handle cases where debug file is redirected by test var debugLogLocations = new List(); if (IISDeploymentParameters.HandlerSettings.ContainsKey("debugFile")) @@ -193,8 +192,6 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS return; } } - - throw new InvalidOperationException($"Unable to find non-empty debug log files. Tried: {string.Join(", ", debugLogLocations)}"); } finally { diff --git a/src/Shared/HttpSys/RequestProcessing/RequestUriBuilder.cs b/src/Shared/HttpSys/RequestProcessing/RequestUriBuilder.cs index 13c07baafc..81e7c437a6 100644 --- a/src/Shared/HttpSys/RequestProcessing/RequestUriBuilder.cs +++ b/src/Shared/HttpSys/RequestProcessing/RequestUriBuilder.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal public static string DecodeAndUnescapePath(Span rawUrlBytes) { - Debug.Assert(rawUrlBytes.Length > 0, "Length of the URL cannot be zero."); + Debug.Assert(rawUrlBytes.Length != 0, "Length of the URL cannot be zero."); var rawPath = RawUrlHelper.GetPath(rawUrlBytes); if (rawPath.Length == 0)