Implement MaxRequestBodySize feature for IIS inprocess (#9475)

This commit is contained in:
Justin Kotalik 2019-05-05 19:30:12 -07:00 committed by GitHub
parent 93d82b0883
commit aaaaf572fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 832 additions and 35 deletions

View File

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

View File

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

View File

@ -94,6 +94,12 @@ public:
return m_dwShutdownTimeLimitInMS;
}
DWORD
QueryMaxRequestBodySizeLimit() const
{
return m_dwMaxRequestBodySize;
}
const std::map<std::wstring, std::wstring, ignore_case_comparer>&
QueryEnvironmentVariables() const
{
@ -128,6 +134,7 @@ private:
bool m_fAnonymousAuthEnabled;
DWORD m_dwStartupTimeLimitInMS;
DWORD m_dwShutdownTimeLimitInMS;
DWORD m_dwMaxRequestBodySize;
std::map<std::wstring, std::wstring, ignore_case_comparer> m_environmentVariables;
std::vector<BindingInformation> m_bindingInformation;

View File

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

View File

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

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Performance
[GlobalSetup]
public void Setup()
{
_server = TestServer.Create(builder => builder.UseMiddleware<PlaintextMiddleware>(), new LoggerFactory()).GetAwaiter().GetResult();
_server = TestServer.Create(builder => builder.UseMiddleware<PlaintextMiddleware>(), new LoggerFactory(), new IISServerOptions()).GetAwaiter().GetResult();
// Recreate client, TestServer.Client has additional logging that can hurt performance
_client = new HttpClient()
{

View File

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

View File

@ -4,4 +4,5 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.IISIntegration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("IIS.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

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

View File

@ -19,5 +19,6 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
public bool fAnonymousAuthEnable;
[MarshalAs(UnmanagedType.BStr)]
public string pwzBindings;
public int maxRequestBodySize;
}
}

View File

@ -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()
{
}

View File

@ -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<Type, object>(IHttpBufferingFeature, _currentIHttpBufferingFeature as global::Microsoft.AspNetCore.Http.Features.IHttpBufferingFeature);
}
if (_currentIHttpMaxRequestBodySizeFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpMaxRequestBodySizeFeature, _currentIHttpMaxRequestBodySizeFeature as global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
}
if (MaybeExtra != null)
{

View File

@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
{
internal partial class IISHttpContext
{
private long _consumedBytes;
/// <summary>
/// Reads data from the Input pipe to the user.
/// </summary>
@ -22,7 +24,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
/// <returns></returns>
internal async ValueTask<int> ReadAsync(Memory<byte> 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)

View File

@ -20,6 +20,9 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
private static readonly Action<ILogger, string, string, Exception> _unexpectedError =
LoggerMessage.Define<string, string>(LogLevel.Error, new EventId(3, "UnexpectedError"), @"Unexpected exception in ""{ClassName}.{MethodName}"".");
private static readonly Action<ILogger, string, string, Exception> _connectionBadRequest =
LoggerMessage.Define<string, string>(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);
}
}
}
}

View File

@ -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<KeyValuePair<Func<object, Task>, object>> _onCompleted;
protected Exception _applicationException;
protected BadHttpRequestException _requestRejectedException;
private readonly MemoryPool<byte> _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)

View File

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

View File

@ -81,6 +81,11 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
}
Features.Set<IServerAddressesFeature>(_serverAddressesFeature);
if (_options.MaxRequestBodySize > _options.IisMaxRequestSizeLimit)
{
_logger.LogWarning(CoreStrings.MaxRequestLimitWarning);
}
}
public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)

View File

@ -147,4 +147,22 @@
<data name="ParameterReadOnlyAfterResponseStarted" xml:space="preserve">
<value>{name} cannot be set because the response has already started.</value>
</data>
<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>
<data name="NonNegativeNumberOrNullRequired" xml:space="preserve">
<value>Value must be null or a non-negative number.</value>
</data>
<data name="BadRequest" xml:space="preserve">
<value>Bad request.</value>
</data>
<data name="MaxRequestLimitWarning" xml:space="preserve">
<value>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.</value>
</data>
</root>

View File

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

View File

@ -150,6 +150,90 @@ namespace Microsoft.AspNetCore.Server.IIS
internal static string FormatParameterReadOnlyAfterResponseStarted(object name)
=> string.Format(CultureInfo.CurrentCulture, GetString("ParameterReadOnlyAfterResponseStarted", "name"), name);
/// <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 been upgraded.
/// </summary>
internal static string MaxRequestBodySizeCannotBeModifiedForUpgradedRequests
{
get => GetString("MaxRequestBodySizeCannotBeModifiedForUpgradedRequests");
}
/// <summary>
/// The maximum request body size cannot be modified after the request has been upgraded.
/// </summary>
internal static string FormatMaxRequestBodySizeCannotBeModifiedForUpgradedRequests()
=> GetString("MaxRequestBodySizeCannotBeModifiedForUpgradedRequests");
/// <summary>
/// Value must be null or a non-negative number.
/// </summary>
internal static string NonNegativeNumberOrNullRequired
{
get => GetString("NonNegativeNumberOrNullRequired");
}
/// <summary>
/// Value must be null or a non-negative number.
/// </summary>
internal static string FormatNonNegativeNumberOrNullRequired()
=> GetString("NonNegativeNumberOrNullRequired");
/// <summary>
/// Bad request.
/// </summary>
internal static string BadRequest
{
get => GetString("BadRequest");
}
/// <summary>
/// Bad request.
/// </summary>
internal static string FormatBadRequest()
=> GetString("BadRequest");
/// <summary>
/// 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.
/// </summary>
internal static string MaxRequestLimitWarning
{
get => GetString("MaxRequestLimitWarning");
}
/// <summary>
/// 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.
/// </summary>
internal static string FormatMaxRequestLimitWarning()
=> GetString("MaxRequestLimitWarning");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

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

View File

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

View File

@ -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."));
}
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>

View File

@ -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<IHttpMaxRequestBodySizeFeature>();
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<IHttpMaxRequestBodySizeFeature>();
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<IHttpMaxRequestBodySizeFeature>();
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<IHttpMaxRequestBodySizeFeature>();
Assert.True(feature.IsReadOnly);
invalidOpEx = Assert.Throws<InvalidOperationException>(() =>
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<BadHttpRequestException>(
async () => await ctx.Request.Body.ReadAsync(buffer, 0, 1));
requestRejectedEx2 = await Assert.ThrowsAsync<BadHttpRequestException>(
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);
}
}
}

View File

@ -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<TestServer> Create(Action<IApplicationBuilder> appBuilder, ILoggerFactory loggerFactory)
public static async Task<TestServer> Create(Action<IApplicationBuilder> 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<TestServer> Create(RequestDelegate app, ILoggerFactory loggerFactory)
{
return Create(builder => builder.Run(app), loggerFactory);
return Create(builder => builder.Run(app), loggerFactory, new IISServerOptions());
}
public static Task<TestServer> 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<IStartup>(this);
services.AddSingleton(_loggerFactory);
.ConfigureServices(services =>
{
services.Configure<IISServerOptions>(options => options.MaxRequestBodySize = _options.MaxRequestBodySize);
services.AddSingleton<IStartup>(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<IHostApplicationLifetime>();

View File

@ -11,6 +11,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Server.IIS.csproj" />
<ProjectReference Include="..\Common.Tests\Common.Tests.csproj" />
<ProjectReference Include="$(RepositoryRoot)src\Hosting\Server.IntegrationTesting\src\Microsoft.AspNetCore.Server.IntegrationTesting.csproj" />
<ProjectReference Include="$(RepositoryRoot)src\Servers\IIS\IntegrationTesting.IIS\src\Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj" />

View File

@ -5,6 +5,7 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TestAssetOutputName>InProcessForwardsCompatWebSite</TestAssetOutputName>
<DefineConstants>FORWARDCOMPAT</DefineConstants>
</PropertyGroup>
<ItemGroup Condition="'$(OS)' == 'Windows_NT'">

View File

@ -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<IISServerOptions>(options => options.MaxRequestBodySize = 2);
})
.UseStartup<Startup>()
.Build();
host.Run();
break;
}
#endif
default:
return StartServer();

View File

@ -284,7 +284,6 @@ namespace TestSite
{
result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 1);
}
}
private int _requestsInFlight = 0;

View File

@ -153,7 +153,6 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS
{
try
{
// Handle cases where debug file is redirected by test
var debugLogLocations = new List<string>();
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
{

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
public static string DecodeAndUnescapePath(Span<byte> 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)