Add RequestSizeLimitAttribute (#6453)

Addresses #6352
This commit is contained in:
Jass Bagga 2017-06-29 13:04:27 -07:00 committed by GitHub
parent 579aca0121
commit 17f6b17a6d
11 changed files with 622 additions and 8 deletions

View File

@ -176,6 +176,12 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IFilterProvider, DefaultFilterProvider>());
//
// Resource Filters
//
services.TryAddTransient<RequestSizeLimitResourceFilter>();
services.TryAddTransient<DisableRequestSizeLimitResourceFilter>();
//
// ModelBinding, Validation
//

View File

@ -0,0 +1,30 @@
// 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.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc
{
/// <summary>
/// Disables the request body size limit.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class DisableRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter
{
/// <inheritdoc />
public int Order { get; set; }
/// <inheritdoc />
public bool IsReusable => true;
/// <inheritdoc />
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
var filter = serviceProvider.GetRequiredService<DisableRequestSizeLimitResourceFilter>();
return filter;
}
}
}

View File

@ -0,0 +1,14 @@
// 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 Microsoft.AspNetCore.Mvc.Filters;
namespace Microsoft.AspNetCore.Mvc
{
/// <summary>
/// A marker interface for filters which define a policy for maximum size for the request body.
/// </summary>
public interface IRequestSizePolicy : IFilterMetadata
{
}
}

View File

@ -0,0 +1,87 @@
// 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.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.Internal
{
/// <summary>
/// A filter that sets <see cref="IHttpMaxRequestBodySizeFeature.MaxRequestBodySize"/>
/// to <c>null</c>.
/// </summary>
public class DisableRequestSizeLimitResourceFilter : IResourceFilter, IRequestSizePolicy
{
private readonly ILogger _logger;
/// <summary>
/// Creates a new instance of <see cref="DisableRequestSizeLimitResourceFilter"/>.
/// </summary>
public DisableRequestSizeLimitResourceFilter(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<DisableRequestSizeLimitResourceFilter>();
}
/// <inheritdoc />
public int Order { get; set; }
/// <inheritdoc />
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
/// <summary>
/// Sets the <see cref="IHttpMaxRequestBodySizeFeature.MaxRequestBodySize"/>
/// to <c>null</c>.
/// </summary>
/// <param name="context">The <see cref="ResourceExecutingContext"/>.</param>
/// <remarks>If <see cref="IHttpMaxRequestBodySizeFeature"/> is not enabled or is read-only,
/// the <see cref="DisableRequestSizeLimitAttribute"/> is not applied.</remarks>
public void OnResourceExecuting(ResourceExecutingContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (IsClosestRequestSizePolicy(context.Filters))
{
var maxRequestBodySizeFeature = context.HttpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
if (maxRequestBodySizeFeature == null)
{
_logger.FeatureNotFound();
}
else if (maxRequestBodySizeFeature.IsReadOnly)
{
_logger.FeatureIsReadOnly();
}
else
{
maxRequestBodySizeFeature.MaxRequestBodySize = null;
_logger.RequestBodySizeLimitDisabled();
}
}
}
private bool IsClosestRequestSizePolicy(IList<IFilterMetadata> filters)
{
// Determine if this instance is the 'effective' request size policy.
for (var i = filters.Count - 1; i >= 0; i--)
{
var filter = filters[i];
if (filter is IRequestSizePolicy)
{
return ReferenceEquals(this, filter);
}
}
Debug.Fail("The current instance should be in the list of filters.");
return false;
}
}
}

View File

@ -72,6 +72,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private static readonly Action<ILogger, string, Exception> _redirectToPageResultExecuting;
private static readonly Action<ILogger, Exception> _featureNotFound;
private static readonly Action<ILogger, Exception> _featureIsReadOnly;
private static readonly Action<ILogger, string, Exception> _maxRequestBodySizeSet;
private static readonly Action<ILogger, Exception> _requestBodySizeLimitDisabled;
static MvcCoreLoggerExtensions()
{
_actionExecuting = LoggerMessage.Define<string>(
@ -243,6 +248,26 @@ namespace Microsoft.AspNetCore.Mvc.Internal
LogLevel.Debug,
3,
"No actions matched the current request. Route values: {RouteValues}");
_featureNotFound = LoggerMessage.Define(
LogLevel.Warning,
1,
"A request body size limit could not be applied. This server does not support the IHttpRequestBodySizeFeature.");
_featureIsReadOnly = LoggerMessage.Define(
LogLevel.Warning,
2,
"A request body size limit could not be applied. The IHttpRequestBodySizeFeature for the server is read-only.");
_maxRequestBodySizeSet = LoggerMessage.Define<string>(
LogLevel.Debug,
3,
"The maximum request body size has been set to {RequestSize}.");
_requestBodySizeLimitDisabled = LoggerMessage.Define(
LogLevel.Debug,
3,
"The request body size limit has been disabled.");
}
public static IDisposable ActionScope(this ILogger logger, ActionDescriptor action)
@ -522,6 +547,26 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public static void RedirectToPageResultExecuting(this ILogger logger, string page)
=> _redirectToPageResultExecuting(logger, page, null);
public static void FeatureNotFound(this ILogger logger)
{
_featureNotFound(logger, null);
}
public static void FeatureIsReadOnly(this ILogger logger)
{
_featureIsReadOnly(logger, null);
}
public static void MaxRequestBodySizeSet(this ILogger logger, string requestSize)
{
_maxRequestBodySizeSet(logger, requestSize, null);
}
public static void RequestBodySizeLimitDisabled(this ILogger logger)
{
_requestBodySizeLimitDisabled(logger, null);
}
private class ActionLogScope : IReadOnlyList<KeyValuePair<string, object>>
{
private readonly ActionDescriptor _action;

View File

@ -0,0 +1,85 @@
// 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.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.Internal
{
/// <summary>
/// A filter that sets the <see cref="IHttpMaxRequestBodySizeFeature.MaxRequestBodySize"/>
/// to the specified <see cref="Bytes"/>.
/// </summary>
public class RequestSizeLimitResourceFilter : IResourceFilter, IRequestSizePolicy
{
private readonly ILogger _logger;
/// <summary>
/// Creates a new instance of <see cref="RequestSizeLimitResourceFilter"/>.
/// </summary>
public RequestSizeLimitResourceFilter(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<RequestSizeLimitResourceFilter>();
}
public long Bytes { get; set; }
/// <inheritdoc />
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
/// <summary>
/// Sets the <see cref="IHttpMaxRequestBodySizeFeature.MaxRequestBodySize"/> to <see cref="Bytes"/>.
/// </summary>
/// <param name="context">The <see cref="ResourceExecutingContext"/>.</param>
/// <remarks>If <see cref="IHttpMaxRequestBodySizeFeature"/> is not enabled or is read-only,
/// the <see cref="RequestSizeLimitAttribute"/> is not applied.</remarks>
public void OnResourceExecuting(ResourceExecutingContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (IsClosestRequestSizePolicy(context.Filters))
{
var maxRequestBodySizeFeature = context.HttpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
if (maxRequestBodySizeFeature == null)
{
_logger.FeatureNotFound();
}
else if (maxRequestBodySizeFeature.IsReadOnly)
{
_logger.FeatureIsReadOnly();
}
else
{
maxRequestBodySizeFeature.MaxRequestBodySize = Bytes;
_logger.MaxRequestBodySizeSet(Bytes.ToString());
}
}
}
private bool IsClosestRequestSizePolicy(IList<IFilterMetadata> filters)
{
// Determine if this instance is the 'effective' request size policy.
for (var i = filters.Count - 1; i >= 0; i--)
{
var filter = filters[i];
if (filter is IRequestSizePolicy)
{
return ReferenceEquals(this, filter);
}
}
Debug.Fail("The current instance should be in the list of filters.");
return false;
}
}
}

View File

@ -0,0 +1,42 @@
// 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.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc
{
/// <summary>
/// Sets the request body size limit to the specified size.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class RequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter
{
private readonly long _bytes;
/// <summary>
/// Creates a new instance of <see cref="RequestSizeLimitAttribute"/>.
/// </summary>
/// <param name="bytes">The request body size limit.</param>
public RequestSizeLimitAttribute(long bytes)
{
_bytes = bytes;
}
/// <inheritdoc />
public int Order { get; set; }
/// <inheritdoc />
public bool IsReusable => true;
/// <inheritdoc />
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
var filter = serviceProvider.GetRequiredService<RequestSizeLimitResourceFilter>();
filter.Bytes = _bytes;
return filter;
}
}
}

View File

@ -0,0 +1,154 @@
// 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.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class DisableRequestSizeLimitResourceFilterTest
{
[Fact]
public void SetsMaxRequestBodySizeToNull()
{
// Arrange
var disableRequestSizeLimitResourceFilter = new DisableRequestSizeLimitResourceFilter(NullLoggerFactory.Instance);
var resourceExecutingContext = CreateResourceExecutingContext(new IFilterMetadata[] { disableRequestSizeLimitResourceFilter });
var httpMaxRequestBodySize = new TestHttpMaxRequestBodySizeFeature();
resourceExecutingContext.HttpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(httpMaxRequestBodySize);
// Act
disableRequestSizeLimitResourceFilter.OnResourceExecuting(resourceExecutingContext);
// Assert
Assert.Null(httpMaxRequestBodySize.MaxRequestBodySize);
}
[Fact]
public void SkipsWhenOverridden()
{
// Arrange
var disableRequestSizeLimitResourceFilter = new DisableRequestSizeLimitResourceFilter(NullLoggerFactory.Instance);
var disableRequestSizeLimitResourceFilterFinal = new DisableRequestSizeLimitResourceFilter(NullLoggerFactory.Instance);
var resourceExecutingContext = CreateResourceExecutingContext(new IFilterMetadata[] { disableRequestSizeLimitResourceFilter, disableRequestSizeLimitResourceFilterFinal });
var httpMaxRequestBodySize = new TestHttpMaxRequestBodySizeFeature();
resourceExecutingContext.HttpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(httpMaxRequestBodySize);
// Act
disableRequestSizeLimitResourceFilter.OnResourceExecuting(resourceExecutingContext);
disableRequestSizeLimitResourceFilterFinal.OnResourceExecuting(resourceExecutingContext);
// Assert
Assert.Null(httpMaxRequestBodySize.MaxRequestBodySize);
Assert.Equal(1, httpMaxRequestBodySize.Count);
}
[Fact]
public void LogsFeatureNotFound()
{
// Arrange
var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var disableRequestSizeLimitResourceFilter = new DisableRequestSizeLimitResourceFilter(loggerFactory);
var resourceExecutingContext = CreateResourceExecutingContext(new IFilterMetadata[] { disableRequestSizeLimitResourceFilter });
// Act
disableRequestSizeLimitResourceFilter.OnResourceExecuting(resourceExecutingContext);
// Assert
var write = Assert.Single(sink.Writes);
Assert.Equal($"A request body size limit could not be applied. This server does not support the IHttpRequestBodySizeFeature.",
write.State.ToString());
}
[Fact]
public void LogsFeatureIsReadOnly()
{
// Arrange
var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var disableRequestSizeLimitResourceFilter = new DisableRequestSizeLimitResourceFilter(loggerFactory);
var resourceExecutingContext = CreateResourceExecutingContext(new IFilterMetadata[] { disableRequestSizeLimitResourceFilter });
var httpMaxRequestBodySize = new TestHttpMaxRequestBodySizeFeature();
httpMaxRequestBodySize.IsReadOnly = true;
resourceExecutingContext.HttpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(httpMaxRequestBodySize);
// Act
disableRequestSizeLimitResourceFilter.OnResourceExecuting(resourceExecutingContext);
// Assert
var write = Assert.Single(sink.Writes);
Assert.Equal($"A request body size limit could not be applied. The IHttpRequestBodySizeFeature for the server is read-only.", write.State.ToString());
}
[Fact]
public void LogsMaxRequestBodySizeSetToNull()
{
// Arrange
var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var disableRequestSizeLimitResourceFilter = new DisableRequestSizeLimitResourceFilter(loggerFactory);
var resourceExecutingContext = CreateResourceExecutingContext(new IFilterMetadata[] { disableRequestSizeLimitResourceFilter });
var httpMaxRequestBodySize = new TestHttpMaxRequestBodySizeFeature();
resourceExecutingContext.HttpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(httpMaxRequestBodySize);
// Act
disableRequestSizeLimitResourceFilter.OnResourceExecuting(resourceExecutingContext);
// Assert
var write = Assert.Single(sink.Writes);
Assert.Equal($"The request body size limit has been disabled.", write.State.ToString());
}
private static ResourceExecutingContext CreateResourceExecutingContext(IFilterMetadata[] filters)
{
return new ResourceExecutingContext(
CreateActionContext(),
filters,
new List<IValueProviderFactory>());
}
private static ActionContext CreateActionContext()
{
return new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
}
private class TestHttpMaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature
{
private long? _maxRequestBodySize;
public bool IsReadOnly { get; set; }
public long? MaxRequestBodySize
{
get
{
return _maxRequestBodySize;
}
set
{
_maxRequestBodySize = value;
Count++;
}
}
public int Count { get; set; }
}
}
}

View File

@ -0,0 +1,159 @@
// 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.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class RequestSizeLimitResourceFilterTest
{
[Fact]
public void SetsMaxRequestBodySize()
{
// Arrange
var requestSizeLimitResourceFilter = new RequestSizeLimitResourceFilter(NullLoggerFactory.Instance);
requestSizeLimitResourceFilter.Bytes = 12345;
var resourceExecutingContext = CreateResourceExecutingContext(new IFilterMetadata[] { requestSizeLimitResourceFilter });
var httpMaxRequestBodySize = new TestHttpMaxRequestBodySizeFeature();
resourceExecutingContext.HttpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(httpMaxRequestBodySize);
// Act
requestSizeLimitResourceFilter.OnResourceExecuting(resourceExecutingContext);
// Assert
Assert.Equal(12345, httpMaxRequestBodySize.MaxRequestBodySize);
}
[Fact]
public void SkipsWhenOverridden()
{
// Arrange
var requestSizeLimitResourceFilter = new RequestSizeLimitResourceFilter(NullLoggerFactory.Instance);
requestSizeLimitResourceFilter.Bytes = 12345;
var requestSizeLimitResourceFilterFinal = new RequestSizeLimitResourceFilter(NullLoggerFactory.Instance);
requestSizeLimitResourceFilterFinal.Bytes = 0;
var resourceExecutingContext = CreateResourceExecutingContext(new IFilterMetadata[] { requestSizeLimitResourceFilter, requestSizeLimitResourceFilterFinal });
var httpMaxRequestBodySize = new TestHttpMaxRequestBodySizeFeature();
resourceExecutingContext.HttpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(httpMaxRequestBodySize);
// Act
requestSizeLimitResourceFilter.OnResourceExecuting(resourceExecutingContext);
requestSizeLimitResourceFilterFinal.OnResourceExecuting(resourceExecutingContext);
// Assert
Assert.Equal(0, httpMaxRequestBodySize.MaxRequestBodySize);
Assert.Equal(1, httpMaxRequestBodySize.Count);
}
[Fact]
public void LogsFeatureNotFound()
{
// Arrange
var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var requestSizeLimitResourceFilter = new RequestSizeLimitResourceFilter(loggerFactory);
requestSizeLimitResourceFilter.Bytes = 12345;
var resourceExecutingContext = CreateResourceExecutingContext(new IFilterMetadata[] { requestSizeLimitResourceFilter });
// Act
requestSizeLimitResourceFilter.OnResourceExecuting(resourceExecutingContext);
// Assert
var write = Assert.Single(sink.Writes);
Assert.Equal($"A request body size limit could not be applied. This server does not support the IHttpRequestBodySizeFeature.",
write.State.ToString());
}
[Fact]
public void LogsFeatureIsReadOnly()
{
// Arrange
var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var requestSizeLimitResourceFilter = new RequestSizeLimitResourceFilter(loggerFactory);
requestSizeLimitResourceFilter.Bytes = 12345;
var resourceExecutingContext = CreateResourceExecutingContext(new IFilterMetadata[] { requestSizeLimitResourceFilter });
var httpMaxRequestBodySize = new TestHttpMaxRequestBodySizeFeature();
httpMaxRequestBodySize.IsReadOnly = true;
resourceExecutingContext.HttpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(httpMaxRequestBodySize);
// Act
requestSizeLimitResourceFilter.OnResourceExecuting(resourceExecutingContext);
// Assert
var write = Assert.Single(sink.Writes);
Assert.Equal($"A request body size limit could not be applied. The IHttpRequestBodySizeFeature for the server is read-only.", write.State.ToString());
}
[Fact]
public void LogsMaxRequestBodySizeSet()
{
// Arrange
var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var requestSizeLimitResourceFilter = new RequestSizeLimitResourceFilter(loggerFactory);
requestSizeLimitResourceFilter.Bytes = 12345;
var resourceExecutingContext = CreateResourceExecutingContext(new IFilterMetadata[] { requestSizeLimitResourceFilter });
var httpMaxRequestBodySize = new TestHttpMaxRequestBodySizeFeature();
resourceExecutingContext.HttpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(httpMaxRequestBodySize);
// Act
requestSizeLimitResourceFilter.OnResourceExecuting(resourceExecutingContext);
// Assert
var write = Assert.Single(sink.Writes);
Assert.Equal($"The maximum request body size has been set to 12345.", write.State.ToString());
}
private static ResourceExecutingContext CreateResourceExecutingContext(IFilterMetadata[] filters)
{
return new ResourceExecutingContext(
CreateActionContext(),
filters,
new List<IValueProviderFactory>());
}
private static ActionContext CreateActionContext()
{
return new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
}
private class TestHttpMaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature
{
private long? _maxRequestBodySize;
public bool IsReadOnly { get; set; }
public long? MaxRequestBodySize
{
get
{
return _maxRequestBodySize;
}
set
{
_maxRequestBodySize = value;
Count++;
}
}
public int Count { get; set; }
}
}
}

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;

View File

@ -1,15 +1,8 @@
// 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.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace FiltersWebSite
{