From f70855332147ada20619f9b9a3d2bed85e616733 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 26 Apr 2022 10:25:06 +0800 Subject: [PATCH 1/2] Introduce `AbpRequestSizeLimitMiddleware ` Resolve #12348 --- .../AbpApplicationBuilderExtensions.cs | 8 +++ .../AbpRequestSizeLimitMiddleware.cs | 63 +++++++++++++++++++ .../RequestSizeLimit/RequestSizeLimit.cs | 19 ++++++ 3 files changed, 90 insertions(+) create mode 100644 framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RequestSizeLimit/AbpRequestSizeLimitMiddleware.cs create mode 100644 framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RequestSizeLimit/RequestSizeLimit.cs diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs index 8ef3ec8d99..0e745e9669 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting; using Volo.Abp; using Volo.Abp.AspNetCore.Auditing; using Volo.Abp.AspNetCore.ExceptionHandling; +using Volo.Abp.AspNetCore.RequestSizeLimit; using Volo.Abp.AspNetCore.Security; using Volo.Abp.AspNetCore.Security.Claims; using Volo.Abp.AspNetCore.Tracing; @@ -111,4 +112,11 @@ public static class AbpApplicationBuilderExtensions { return app.UseMiddleware(); } + + public static IApplicationBuilder UseAbpRequestSizeLimit(this IApplicationBuilder app) + { + return app + .UseAbpExceptionHandling() + .UseMiddleware(); + } } diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RequestSizeLimit/AbpRequestSizeLimitMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RequestSizeLimit/AbpRequestSizeLimitMiddleware.cs new file mode 100644 index 0000000000..77e99599c4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RequestSizeLimit/AbpRequestSizeLimitMiddleware.cs @@ -0,0 +1,63 @@ +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.RequestSizeLimit; + +public class AbpRequestSizeLimitMiddleware : IMiddleware, ITransientDependency +{ + private readonly ILogger _logger; + + public AbpRequestSizeLimitMiddleware(ILogger logger) + { + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var endpoint = context.GetEndpoint(); + if (endpoint != null) + { + if (!endpoint.Metadata.Any(x => x is RequestSizeLimitAttribute || x is DisableRequestSizeLimitAttribute)) + { + var attribute = endpoint.Metadata.GetMetadata(); + if (attribute != null) + { + var maxRequestBodySizeFeature = context.Features.Get(); + if (maxRequestBodySizeFeature == null) + { + _logger.LogInformation("A request body size limit could not be applied. This server does not support the IHttpRequestBodySizeFeature."); + } + else if (maxRequestBodySizeFeature.IsReadOnly) + { + _logger.LogInformation("A request body size limit could not be applied. The IHttpRequestBodySizeFeature for the server is read-only."); + } + else + { + _logger.LogInformation($"The maximum request body size has been set to {attribute.GetBytes().ToString(CultureInfo.InvariantCulture)}."); + maxRequestBodySizeFeature.MaxRequestBodySize = attribute.GetBytes(); + } + } + else + { + _logger.LogInformation($"AbpRequestSizeLimitAttribute does not exist in endpoint, Skipping."); + } + } + else + { + _logger.LogInformation($"Endpoint already exists IRequestSizePolicy, Skipping."); + } + } + else + { + _logger.LogInformation($"Endpoint is null, Skipping."); + } + + await next(context); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RequestSizeLimit/RequestSizeLimit.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RequestSizeLimit/RequestSizeLimit.cs new file mode 100644 index 0000000000..5d6db19220 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RequestSizeLimit/RequestSizeLimit.cs @@ -0,0 +1,19 @@ +using System; + +namespace Volo.Abp.AspNetCore.RequestSizeLimit; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +public class AbpRequestSizeLimitAttribute : Attribute +{ + private readonly long _bytes; + + public AbpRequestSizeLimitAttribute(long bytes) + { + _bytes = bytes; + } + + public long GetBytes() + { + return _bytes; + } +} From e17227d4300f3e0414527d11be1abbbe78128549 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 26 Apr 2022 11:11:10 +0800 Subject: [PATCH 2/2] Add `RequestSizeLimit` unit tests. --- .../AbpRequestSizeLimitMiddleware.cs | 22 ++++- .../Volo.Abp.AspNetCore.Mvc.Tests.csproj | 1 + .../Mvc/AbpAspNetCoreMvcTestModule.cs | 20 ++++ .../RequestBodySizeCheckingStream.cs | 94 +++++++++++++++++++ .../RequestSizeLimitController.cs | 25 +++++ .../RequestSizeLimitController_Tests.cs | 53 +++++++++++ 6 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/RequestSizeLimit/RequestBodySizeCheckingStream.cs create mode 100644 framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/RequestSizeLimit/RequestSizeLimitController.cs create mode 100644 framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/RequestSizeLimit/RequestSizeLimitController_Tests.cs diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RequestSizeLimit/AbpRequestSizeLimitMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RequestSizeLimit/AbpRequestSizeLimitMiddleware.cs index 77e99599c4..56accf7883 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RequestSizeLimit/AbpRequestSizeLimitMiddleware.cs +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RequestSizeLimit/AbpRequestSizeLimitMiddleware.cs @@ -23,7 +23,7 @@ public class AbpRequestSizeLimitMiddleware : IMiddleware, ITransientDependency var endpoint = context.GetEndpoint(); if (endpoint != null) { - if (!endpoint.Metadata.Any(x => x is RequestSizeLimitAttribute || x is DisableRequestSizeLimitAttribute)) + if (!HasDisableRequestSizeLimitAttribute(endpoint)) { var attribute = endpoint.Metadata.GetMetadata(); if (attribute != null) @@ -50,7 +50,7 @@ public class AbpRequestSizeLimitMiddleware : IMiddleware, ITransientDependency } else { - _logger.LogInformation($"Endpoint already exists IRequestSizePolicy, Skipping."); + _logger.LogInformation($"Endpoint has DisableRequestSizeLimitAttribute, Skipping."); } } else @@ -60,4 +60,22 @@ public class AbpRequestSizeLimitMiddleware : IMiddleware, ITransientDependency await next(context); } + + protected virtual bool HasDisableRequestSizeLimitAttribute(Endpoint endpoint) + { + foreach (var metadata in endpoint.Metadata.Reverse()) + { + if (metadata is AbpRequestSizeLimitAttribute) + { + return false; + } + + if (metadata is DisableRequestSizeLimitAttribute) + { + return true; + } + } + + return false; + } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj index 4e1b5e1334..031c342240 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj @@ -24,6 +24,7 @@ + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs index 2a48bbc8e7..d725d3c437 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs @@ -3,13 +3,16 @@ using System.Collections.Generic; using System.Security.Claims; using Localization.Resources.AbpUi; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.MultiTenancy; using Volo.Abp.AspNetCore.Mvc.Authorization; using Volo.Abp.AspNetCore.Mvc.GlobalFeatures; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.Localization.Resource; +using Volo.Abp.AspNetCore.Mvc.RequestSizeLimit; using Volo.Abp.AspNetCore.Security.Claims; using Volo.Abp.AspNetCore.TestBase; using Volo.Abp.Authorization; @@ -29,6 +32,7 @@ namespace Volo.Abp.AspNetCore.Mvc; typeof(AbpAspNetCoreTestBaseModule), typeof(AbpMemoryDbTestModule), typeof(AbpAspNetCoreMvcModule), + typeof(AbpAspNetCoreMultiTenancyModule), typeof(AbpAutofacModule) )] public class AbpAspNetCoreMvcTestModule : AbpModule @@ -143,6 +147,22 @@ public class AbpAspNetCoreMvcTestModule : AbpModule app.UseAuthorization(); app.UseAuditing(); app.UseUnitOfWork(); + + app.Use((httpContext, next) => + { + var testHttpMaxRequestBodySizeFeature = new TestHttpMaxRequestBodySizeFeature(); + httpContext.Features.Set( + testHttpMaxRequestBodySizeFeature); + + httpContext.Request.Body = new RequestBodySizeCheckingStream( + httpContext.Request.Body, + testHttpMaxRequestBodySizeFeature); + + return next(httpContext); + }); + app.UseAbpRequestSizeLimit(); + app.UseMultiTenancy(); + app.UseConfiguredEndpoints(); } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/RequestSizeLimit/RequestBodySizeCheckingStream.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/RequestSizeLimit/RequestBodySizeCheckingStream.cs new file mode 100644 index 0000000000..bbd05f94d6 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/RequestSizeLimit/RequestBodySizeCheckingStream.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Volo.Abp.AspNetCore.Mvc.RequestSizeLimit; + +public class RequestBodySizeCheckingStream : Stream +{ + private readonly Stream _innerStream; + private readonly IHttpMaxRequestBodySizeFeature _maxRequestBodySizeFeature; + private long _totalRead; + + public RequestBodySizeCheckingStream(Stream innerStream, IHttpMaxRequestBodySizeFeature maxRequestBodySizeFeature) + { + _innerStream = innerStream; + _maxRequestBodySizeFeature = maxRequestBodySizeFeature; + } + + public override bool CanRead => _innerStream.CanRead; + + public override bool CanSeek => _innerStream.CanSeek; + + public override bool CanWrite => _innerStream.CanWrite; + + public override long Length => _innerStream.Length; + + public override long Position + { + get { return _innerStream.Position; } + set { _innerStream.Position = value; } + } + + public override void Flush() + { + _innerStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (_maxRequestBodySizeFeature.MaxRequestBodySize != null && _innerStream.CanSeek && _innerStream.Length > _maxRequestBodySizeFeature.MaxRequestBodySize) + { + throw new BusinessException("Request content size is greater than the limit size"); + } + + var read = _innerStream.Read(buffer, offset, count); + _totalRead += read; + + if (_maxRequestBodySizeFeature.MaxRequestBodySize != null && _totalRead > _maxRequestBodySizeFeature.MaxRequestBodySize) + { + throw new BusinessException("Request content size is greater than the limit size"); + } + return read; + } + + public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_maxRequestBodySizeFeature.MaxRequestBodySize != null && _innerStream.CanSeek && _innerStream.Length > _maxRequestBodySizeFeature.MaxRequestBodySize) + { + throw new BusinessException("Request content size is greater than the limit size"); + } + + var read = await _innerStream.ReadAsync(buffer, offset, count, cancellationToken); + _totalRead += read; + + if (_maxRequestBodySizeFeature.MaxRequestBodySize != null && _totalRead > _maxRequestBodySizeFeature.MaxRequestBodySize) + { + throw new BusinessException("Request content size is greater than the limit size"); + } + return read; + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _innerStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _innerStream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _innerStream.Write(buffer, offset, count); + } +} + +public class TestHttpMaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature +{ + public bool IsReadOnly => false; + public long? MaxRequestBodySize { get; set; } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/RequestSizeLimit/RequestSizeLimitController.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/RequestSizeLimit/RequestSizeLimitController.cs new file mode 100644 index 0000000000..9c7ef6bcf5 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/RequestSizeLimit/RequestSizeLimitController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.RequestSizeLimit; +using Volo.Abp.Content; + +namespace Volo.Abp.AspNetCore.Mvc.RequestSizeLimit; + +[Route("api/request-siz-limit")] +[AbpRequestSizeLimit(5)] +public class RequestSizeLimitController : AbpController +{ + [HttpPost] + [Route("check")] + public string RequestSizeLimitCheck(IRemoteStreamContent file) + { + return !ModelState.IsValid ? ModelState.ToString() : "ok"; + } + + [HttpPost] + [DisableRequestSizeLimit] + [Route("disable")] + public string DisableRequestSizeLimit(IRemoteStreamContent file) + { + return !ModelState.IsValid ? ModelState.ToString() : "ok"; + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/RequestSizeLimit/RequestSizeLimitController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/RequestSizeLimit/RequestSizeLimitController_Tests.cs new file mode 100644 index 0000000000..875a802a12 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/RequestSizeLimit/RequestSizeLimitController_Tests.cs @@ -0,0 +1,53 @@ +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; + +using Shouldly; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.RequestSizeLimit; + +public class RequestSizeLimitController_Tests : AspNetCoreMvcTestBase +{ + [Fact] + public async Task RequestSizeLimitCheck_Test() + { + using (var requestMessage = new HttpRequestMessage(HttpMethod.Post, "api/request-siz-limit/check")) + { + var memoryStream = new MemoryStream(); + await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("0123456789")); + memoryStream.Position = 0; + + var streamContent = new StreamContent(memoryStream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + requestMessage.Content = new MultipartFormDataContent { { streamContent, "file", "text.txt" } }; + + var response = await Client.SendAsync(requestMessage); + + (await response.Content.ReadAsStringAsync()).ShouldContain("Request content size is greater than the limit size"); + } + } + + [Fact] + public async Task DisableRequestSizeLimit_Test() + { + using (var requestMessage = new HttpRequestMessage(HttpMethod.Post, "api/request-siz-limit/disable")) + { + var memoryStream = new MemoryStream(); + await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("0123456789")); + memoryStream.Position = 0; + + var streamContent = new StreamContent(memoryStream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + requestMessage.Content = new MultipartFormDataContent { { streamContent, "file", "text.txt" } }; + + var response = await Client.SendAsync(requestMessage); + + (await response.Content.ReadAsStringAsync()).ShouldContain("ok"); + } + } +}