Skip to content

Implement MaxRequestBodySize feature for IIS inprocess #9475

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
May 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ class InProcessOptions: NonCopyable
return m_dwShutdownTimeLimitInMS;
}

DWORD
QueryMaxRequestBodySizeLimit() const
{
return m_dwMaxRequestBodySize;
}

const std::map<std::wstring, std::wstring, ignore_case_comparer>&
QueryEnvironmentVariables() const
{
Expand Down Expand Up @@ -128,6 +134,7 @@ class InProcessOptions: NonCopyable
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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class PlaintextBenchmark
[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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public IISServerOptions() { }
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
Expand All @@ -27,6 +28,11 @@ public partial interface IServerVariablesFeature
}
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; }
Expand Down
1 change: 1 addition & 0 deletions src/Servers/IIS/IIS/src/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.IISIntegration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("IIS.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

44 changes: 44 additions & 0 deletions src/Servers/IIS/IIS/src/BadHttpRequestException.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
1 change: 1 addition & 0 deletions src/Servers/IIS/IIS/src/Core/IISConfigurationData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ internal struct IISConfigurationData
public bool fAnonymousAuthEnable;
[MarshalAs(UnmanagedType.BStr)]
public string pwzBindings;
public int maxRequestBodySize;
}
}
35 changes: 33 additions & 2 deletions src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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
{
Expand All @@ -28,7 +29,8 @@ internal partial class IISHttpContext : IFeatureCollection,
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.
Expand Down Expand Up @@ -277,7 +279,7 @@ async Task<Stream> IHttpUpgradeFeature.UpgradeAsync()
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);
Expand Down Expand Up @@ -322,6 +324,35 @@ unsafe X509Certificate2 ITlsConnectionFeature.ClientCertificate

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()
{
}
Expand Down
15 changes: 15 additions & 0 deletions src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal partial class IISHttpContext
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;
Expand All @@ -48,6 +49,7 @@ internal partial class IISHttpContext
private object _currentIHttpSendFileFeature;
private object _currentIServerVariablesFeature;
private object _currentIHttpBufferingFeature;
private object _currentIHttpMaxRequestBodySizeFeature;

private void Initialize()
{
Expand All @@ -61,6 +63,7 @@ private void Initialize()
_currentIHttpAuthenticationFeature = this;
_currentIServerVariablesFeature = this;
_currentIHttpBufferingFeature = this;
_currentIHttpMaxRequestBodySizeFeature = this;
_currentITlsConnectionFeature = this;
}

Expand Down Expand Up @@ -146,6 +149,10 @@ internal object FastFeatureGet(Type key)
{
return _currentIHttpBufferingFeature;
}
if (key == IHttpMaxRequestBodySizeFeature)
{
return _currentIHttpMaxRequestBodySizeFeature;
}

return ExtraFeatureGet(key);
}
Expand Down Expand Up @@ -249,6 +256,10 @@ internal void FastFeatureSet(Type key, object feature)
_currentIHttpBufferingFeature = feature;
return;
}
if (key == IHttpMaxRequestBodySizeFeature)
{
_currentIHttpMaxRequestBodySizeFeature = feature;
}
if (key == IISHttpContextType)
{
throw new InvalidOperationException("Cannot set IISHttpContext in feature collection");
Expand Down Expand Up @@ -334,6 +345,10 @@ private IEnumerable<KeyValuePair<Type, object>> FastEnumerable()
{
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)
{
Expand Down
10 changes: 9 additions & 1 deletion src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -22,7 +24,7 @@ internal partial class IISHttpContext
/// <returns></returns>
internal async ValueTask<int> ReadAsync(Memory<byte> memory, CancellationToken cancellationToken)
{
if (!_hasRequestReadingStarted)
if (!HasStartedConsumingRequestBody)
{
InitializeRequestIO();
}
Expand Down Expand Up @@ -105,9 +107,15 @@ private async Task ReadBody()
// 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)
Expand Down
8 changes: 8 additions & 0 deletions src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ private static class Log
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);
Expand All @@ -34,6 +37,11 @@ public static void UnexpectedError(ILogger logger, string className, Exception e
{
_unexpectedError(logger, className, methodName, ex);
}

public static void ConnectionBadRequest(ILogger logger, string connectionId, BadHttpRequestException ex)
{
_connectionBadRequest(logger, connectionId, ex.Message, ex);
}
}
}
}
Loading