Skip to content

Commit

Permalink
Merge pull request #12358 from abpframework/RequestSizeLimit
Browse files Browse the repository at this point in the history
Introduce `AbpRequestSizeLimitMiddleware `
  • Loading branch information
hikalkan authored Apr 27, 2022
2 parents da9ef38 + e17227d commit 2473aeb
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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;
Expand Down Expand Up @@ -111,4 +112,11 @@ public static IApplicationBuilder UseAbpSecurityHeaders(this IApplicationBuilder
{
return app.UseMiddleware<AbpSecurityHeadersMiddleware>();
}

public static IApplicationBuilder UseAbpRequestSizeLimit(this IApplicationBuilder app)
{
return app
.UseAbpExceptionHandling()
.UseMiddleware<AbpRequestSizeLimitMiddleware>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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<AbpRequestSizeLimitMiddleware> _logger;

public AbpRequestSizeLimitMiddleware(ILogger<AbpRequestSizeLimitMiddleware> logger)
{
_logger = logger;
}

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var endpoint = context.GetEndpoint();
if (endpoint != null)
{
if (!HasDisableRequestSizeLimitAttribute(endpoint))
{
var attribute = endpoint.Metadata.GetMetadata<AbpRequestSizeLimitAttribute>();
if (attribute != null)
{
var maxRequestBodySizeFeature = context.Features.Get<IHttpMaxRequestBodySizeFeature>();
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 has DisableRequestSizeLimitAttribute, Skipping.");
}
}
else
{
_logger.LogInformation($"Endpoint is null, Skipping.");
}

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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.MultiTenancy\Volo.Abp.AspNetCore.MultiTenancy.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Mvc.UI\Volo.Abp.AspNetCore.Mvc.UI.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Mvc\Volo.Abp.AspNetCore.Mvc.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
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;
Expand All @@ -29,6 +32,7 @@ namespace Volo.Abp.AspNetCore.Mvc;
typeof(AbpAspNetCoreTestBaseModule),
typeof(AbpMemoryDbTestModule),
typeof(AbpAspNetCoreMvcModule),
typeof(AbpAspNetCoreMultiTenancyModule),
typeof(AbpAutofacModule)
)]
public class AbpAspNetCoreMvcTestModule : AbpModule
Expand Down Expand Up @@ -143,6 +147,22 @@ public override void OnApplicationInitialization(ApplicationInitializationContex
app.UseAuthorization();
app.UseAuditing();
app.UseUnitOfWork();

app.Use((httpContext, next) =>
{
var testHttpMaxRequestBodySizeFeature = new TestHttpMaxRequestBodySizeFeature();
httpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(
testHttpMaxRequestBodySizeFeature);

httpContext.Request.Body = new RequestBodySizeCheckingStream(
httpContext.Request.Body,
testHttpMaxRequestBodySizeFeature);

return next(httpContext);
});
app.UseAbpRequestSizeLimit();
app.UseMultiTenancy();

app.UseConfiguredEndpoints();
}
}
Original file line number Diff line number Diff line change
@@ -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<int> 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; }
}
Original file line number Diff line number Diff line change
@@ -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";
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
}

0 comments on commit 2473aeb

Please sign in to comment.