Add Request Decompression middleware (#40279)

Co-authored-by: Pranav K <prkrishn@hotmail.com>
Co-authored-by: Sébastien Ros <sebastienros@gmail.com>
This commit is contained in:
David Acker 2022-06-03 18:22:04 -04:00 коммит произвёл GitHub
Родитель d96a100bdd
Коммит d5a539f19b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
37 изменённых файлов: 2217 добавлений и 5 удалений

Просмотреть файл

@ -1654,6 +1654,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Compon
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.SdkAnalyzers.Tests", "src\Tools\SDK-Analyzers\Components\test\Microsoft.AspNetCore.Components.SdkAnalyzers.Tests.csproj", "{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RequestDecompression", "RequestDecompression", "{5465F96F-33D5-454E-9C40-494E58AEEE5D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestDecompression.Tests", "src\Middleware\RequestDecompression\test\Microsoft.AspNetCore.RequestDecompression.Tests.csproj", "{97996D39-7722-4AFC-A41A-AD61CA7A413D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RequestDecompressionSample", "src\Middleware\RequestDecompression\sample\RequestDecompressionSample.csproj", "{37144E52-611B-40E8-807C-2821F5A814CB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestDecompression", "src\Middleware\RequestDecompression\src\Microsoft.AspNetCore.RequestDecompression.csproj", "{559FE354-7E08-4310-B4F3-AE30F34DEED5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LinkabilityChecker", "LinkabilityChecker", "{94F95276-7CDF-44A8-B159-D09702EF6794}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinkabilityChecker", "src\Tools\LinkabilityChecker\LinkabilityChecker.csproj", "{EA7D844B-C180-41C7-9D55-273AD88BF71F}"
@ -1710,6 +1718,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Html.A
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RateLimiting", "RateLimiting", "{1D865E78-7A66-4CA9-92EE-2B350E45281F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestDecompression.Microbenchmarks", "src\Middleware\RequestDecompression\perf\Microbenchmarks\Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj", "{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts", "src\Tools\dotnet-user-jwts\src\dotnet-user-jwts.csproj", "{B34CB502-0286-4939-B25F-45998528A802}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet-user-jwts", "dotnet-user-jwts", "{AB4B9E75-719C-4589-B852-20FBFD727730}"
@ -9967,6 +9977,54 @@ Global
{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x64.Build.0 = Release|Any CPU
{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x86.ActiveCfg = Release|Any CPU
{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x86.Build.0 = Release|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|arm64.ActiveCfg = Debug|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|arm64.Build.0 = Debug|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|x64.ActiveCfg = Debug|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|x64.Build.0 = Debug|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|x86.ActiveCfg = Debug|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|x86.Build.0 = Debug|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|Any CPU.Build.0 = Release|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|arm64.ActiveCfg = Release|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|arm64.Build.0 = Release|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|x64.ActiveCfg = Release|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|x64.Build.0 = Release|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|x86.ActiveCfg = Release|Any CPU
{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|x86.Build.0 = Release|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|arm64.ActiveCfg = Debug|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|arm64.Build.0 = Debug|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|x64.ActiveCfg = Debug|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|x64.Build.0 = Debug|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|x86.ActiveCfg = Debug|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|x86.Build.0 = Debug|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Release|Any CPU.Build.0 = Release|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Release|arm64.ActiveCfg = Release|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Release|arm64.Build.0 = Release|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Release|x64.ActiveCfg = Release|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Release|x64.Build.0 = Release|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Release|x86.ActiveCfg = Release|Any CPU
{37144E52-611B-40E8-807C-2821F5A814CB}.Release|x86.Build.0 = Release|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|arm64.ActiveCfg = Debug|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|arm64.Build.0 = Debug|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|x64.ActiveCfg = Debug|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|x64.Build.0 = Debug|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|x86.ActiveCfg = Debug|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|x86.Build.0 = Debug|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|Any CPU.Build.0 = Release|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|arm64.ActiveCfg = Release|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|arm64.Build.0 = Release|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|x64.ActiveCfg = Release|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|x64.Build.0 = Release|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|x86.ActiveCfg = Release|Any CPU
{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|x86.Build.0 = Release|Any CPU
{EA7D844B-C180-41C7-9D55-273AD88BF71F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EA7D844B-C180-41C7-9D55-273AD88BF71F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA7D844B-C180-41C7-9D55-273AD88BF71F}.Debug|arm64.ActiveCfg = Debug|Any CPU
@ -10255,6 +10313,22 @@ Global
{487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x64.Build.0 = Release|Any CPU
{487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x86.ActiveCfg = Release|Any CPU
{487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x86.Build.0 = Release|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|arm64.ActiveCfg = Debug|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|arm64.Build.0 = Debug|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|x64.ActiveCfg = Debug|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|x64.Build.0 = Debug|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|x86.ActiveCfg = Debug|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|x86.Build.0 = Debug|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|Any CPU.Build.0 = Release|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|arm64.ActiveCfg = Release|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|arm64.Build.0 = Release|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|x64.ActiveCfg = Release|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|x64.Build.0 = Release|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|x86.ActiveCfg = Release|Any CPU
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|x86.Build.0 = Release|Any CPU
{B34CB502-0286-4939-B25F-45998528A802}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B34CB502-0286-4939-B25F-45998528A802}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B34CB502-0286-4939-B25F-45998528A802}.Debug|arm64.ActiveCfg = Debug|Any CPU
@ -11122,6 +11196,10 @@ Global
{CC45FA2D-128B-485D-BA6D-DFD9735CB3C3} = {6C06163A-80E9-49C1-817C-B391852BA563}
{825BCF97-67A9-4834-B3A8-C3DC97A90E41} = {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3}
{DC349A25-0DBF-4468-99E1-B95C22D3A7EF} = {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3}
{5465F96F-33D5-454E-9C40-494E58AEEE5D} = {E5963C9F-20A6-4385-B364-814D2581FADF}
{97996D39-7722-4AFC-A41A-AD61CA7A413D} = {5465F96F-33D5-454E-9C40-494E58AEEE5D}
{37144E52-611B-40E8-807C-2821F5A814CB} = {5465F96F-33D5-454E-9C40-494E58AEEE5D}
{559FE354-7E08-4310-B4F3-AE30F34DEED5} = {5465F96F-33D5-454E-9C40-494E58AEEE5D}
{94F95276-7CDF-44A8-B159-D09702EF6794} = {0B200A66-B809-4ED3-A790-CB1C2E80975E}
{EA7D844B-C180-41C7-9D55-273AD88BF71F} = {94F95276-7CDF-44A8-B159-D09702EF6794}
{7A331A1C-E2C4-4E37-B0A0-B5AA10661229} = {8DAC59BE-CB96-4F04-909C-56C22E7665EB}
@ -11150,6 +11228,7 @@ Global
{51D07AA9-6297-4F66-A7BD-71CE7E3F4A3F} = {0F84F170-57D0-496B-8E2C-7984178EF69F}
{487EF7BE-5009-4C70-B79E-45519BDD9603} = {412D4C15-F48F-4DB1-940A-131D1AA87088}
{1D865E78-7A66-4CA9-92EE-2B350E45281F} = {E5963C9F-20A6-4385-B364-814D2581FADF}
{3309FA1E-4E95-49E9-BE2A-827D01FD63C0} = {5465F96F-33D5-454E-9C40-494E58AEEE5D}
{B34CB502-0286-4939-B25F-45998528A802} = {AB4B9E75-719C-4589-B852-20FBFD727730}
{AB4B9E75-719C-4589-B852-20FBFD727730} = {0B200A66-B809-4ED3-A790-CB1C2E80975E}
{7F079E92-32D5-4257-B95B-CFFB0D49C160} = {7FD32066-C831-4E29-978C-9A2215E85C67}

Просмотреть файл

@ -90,6 +90,7 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Localization" ProjectPath="$(RepoRoot)src\Middleware\Localization\src\Microsoft.AspNetCore.Localization.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.MiddlewareAnalysis" ProjectPath="$(RepoRoot)src\Middleware\MiddlewareAnalysis\src\Microsoft.AspNetCore.MiddlewareAnalysis.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.RateLimiting" ProjectPath="$(RepoRoot)src\Middleware\RateLimiting\src\Microsoft.AspNetCore.RateLimiting.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.RequestDecompression" ProjectPath="$(RepoRoot)src\Middleware\RequestDecompression\src\Microsoft.AspNetCore.RequestDecompression.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCaching.Abstractions" ProjectPath="$(RepoRoot)src\Middleware\ResponseCaching.Abstractions\src\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCaching" ProjectPath="$(RepoRoot)src\Middleware\ResponseCaching\src\Microsoft.AspNetCore.ResponseCaching.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCompression" ProjectPath="$(RepoRoot)src\Middleware\ResponseCompression\src\Microsoft.AspNetCore.ResponseCompression.csproj" />

Просмотреть файл

@ -77,6 +77,7 @@
<AspNetCoreAppReference Include="Microsoft.AspNetCore.HttpsPolicy" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.Localization.Routing" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.Localization" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.RequestDecompression" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.ResponseCaching.Abstractions" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.ResponseCaching" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.ResponseCompression" />

Просмотреть файл

@ -75,6 +75,7 @@ public static class TestData
"Microsoft.AspNetCore.Mvc.ViewFeatures",
"Microsoft.AspNetCore.Razor",
"Microsoft.AspNetCore.Razor.Runtime",
"Microsoft.AspNetCore.RequestDecompression",
"Microsoft.AspNetCore.ResponseCaching",
"Microsoft.AspNetCore.ResponseCaching.Abstractions",
"Microsoft.AspNetCore.ResponseCompression",
@ -210,6 +211,7 @@ public static class TestData
{ "Microsoft.AspNetCore.Mvc.ViewFeatures", "7.0.0.0" },
{ "Microsoft.AspNetCore.Razor", "7.0.0.0" },
{ "Microsoft.AspNetCore.Razor.Runtime", "7.0.0.0" },
{ "Microsoft.AspNetCore.RequestDecompression", "7.0.0.0" },
{ "Microsoft.AspNetCore.ResponseCaching", "7.0.0.0" },
{ "Microsoft.AspNetCore.ResponseCaching.Abstractions", "7.0.0.0" },
{ "Microsoft.AspNetCore.ResponseCompression", "7.0.0.0" },

Просмотреть файл

@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.AspNetCore.Http.Metadata;
/// <summary>
/// Interface marking attributes that specify the maximum allowed size of the request body.
/// </summary>
public interface IRequestSizeLimitMetadata
{
/// <summary>
/// The maximum allowed size of the current request body in bytes.
/// </summary>
long? MaxRequestBodySize { get; }
}

Просмотреть файл

@ -12,6 +12,8 @@ Microsoft.AspNetCore.Http.EndpointMetadataCollection.GetRequiredMetadata<T>() ->
Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask<object?>
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string?
Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata
Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata.MaxRequestBodySize.get -> long?
Microsoft.AspNetCore.Http.RouteHandlerContext
Microsoft.AspNetCore.Http.RouteHandlerContext.EndpointMetadata.get -> Microsoft.AspNetCore.Http.EndpointMetadataCollection!
Microsoft.AspNetCore.Http.RouteHandlerContext.MethodInfo.get -> System.Reflection.MethodInfo!

Просмотреть файл

@ -78,6 +78,10 @@
"src\\Middleware\\MiddlewareAnalysis\\test\\Microsoft.AspNetCore.MiddlewareAnalysis.Tests.csproj",
"src\\Middleware\\RateLimiting\\src\\Microsoft.AspNetCore.RateLimiting.csproj",
"src\\Middleware\\RateLimiting\\test\\Microsoft.AspNetCore.RateLimiting.Tests.csproj",
"src\\Middleware\\RequestDecompression\\perf\\Microbenchmarks\\Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj",
"src\\Middleware\\RequestDecompression\\sample\\RequestDecompressionSample.csproj",
"src\\Middleware\\RequestDecompression\\src\\Microsoft.AspNetCore.RequestDecompression.csproj",
"src\\Middleware\\RequestDecompression\\test\\Microsoft.AspNetCore.RequestDecompression.Tests.csproj",
"src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj",
"src\\Middleware\\ResponseCaching\\samples\\ResponseCachingSample\\ResponseCachingSample.csproj",
"src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj",
@ -117,4 +121,4 @@
"src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj"
]
}
}
}

Просмотреть файл

@ -0,0 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]

Просмотреть файл

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Reference Include="BenchmarkDotNet" />
<Reference Include="Microsoft.AspNetCore.RequestDecompression" />
<Compile Include="$(SharedSourceRoot)BenchmarkRunner\*.cs" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,92 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.RequestDecompression.Benchmarks;
public class RequestDecompressionMiddlewareBenchmark
{
private RequestDecompressionMiddleware _middleware;
[GlobalSetup]
public void GlobalSetup()
{
var requestDecompressionProvider = new DefaultRequestDecompressionProvider(
NullLogger<DefaultRequestDecompressionProvider>.Instance,
Options.Create(new RequestDecompressionOptions())
);
_middleware = new RequestDecompressionMiddleware(
context => Task.CompletedTask,
NullLogger<RequestDecompressionMiddleware>.Instance,
requestDecompressionProvider
);
}
[Params(true, false)]
public bool HasRequestSizeLimitMetadata { get; set; }
[Benchmark]
public async Task HandleRequest_Compressed()
{
var context = CreateHttpContext(HasRequestSizeLimitMetadata);
context.Request.Headers.ContentEncoding = "gzip";
await _middleware.Invoke(context);
}
[Benchmark]
public async Task HandleRequest_Uncompressed()
{
var context = CreateHttpContext(HasRequestSizeLimitMetadata);
await _middleware.Invoke(context);
}
private static DefaultHttpContext CreateHttpContext(bool hasRequestSizeLimitMetadata)
{
var features = new FeatureCollection();
features.Set<IHttpRequestFeature>(new HttpRequestFeature());
features.Set<IHttpResponseFeature>(new HttpResponseFeature());
features.Set<IHttpMaxRequestBodySizeFeature>(new MaxRequestBodySizeFeature());
features.Set<IEndpointFeature>(new EndpointFeature(hasRequestSizeLimitMetadata));
var context = new DefaultHttpContext(features);
return context;
}
private sealed class MaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature
{
public bool IsReadOnly => false;
public long? MaxRequestBodySize { get; set; } = 30_000_000;
}
private sealed class EndpointFeature : IEndpointFeature
{
public Endpoint Endpoint { get; set; }
public EndpointFeature(bool hasRequestSizeLimitMetadata)
{
var metadataCollection = hasRequestSizeLimitMetadata
? new EndpointMetadataCollection(new SizeLimitMetadata())
: new EndpointMetadataCollection();
Endpoint = new Endpoint(
requestDelegate: null,
metadata: metadataCollection,
displayName: null);
}
}
private sealed class SizeLimitMetadata : IRequestSizeLimitMetadata
{
public long? MaxRequestBodySize { get; set; } = 50_000_000;
}
}

Просмотреть файл

@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.RequestDecompression;
namespace RequestDecompressionSample;
public class CustomDecompressionProvider : IDecompressionProvider
{
public Stream GetDecompressionStream(Stream stream)
{
// Create a custom decompression stream wrapper here.
return stream;
}
}

Просмотреть файл

@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:6164/",
"sslPort": 0
}
},
"profiles": {
"RequestDecompressionSample": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:5000/",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

Просмотреть файл

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.RequestDecompression" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
<Reference Include="Microsoft.Extensions.Logging.Console" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,48 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace RequestDecompressionSample;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddRequestDecompression(options =>
{
options.DecompressionProviders.Add("custom", new CustomDecompressionProvider());
});
}
public void Configure(IApplicationBuilder app)
{
app.UseRequestDecompression();
app.Map("/test", testApp =>
{
testApp.Run(async context =>
{
using var reader = new StreamReader(context.Request.Body);
var decompressedBody = await reader.ReadToEndAsync(context.RequestAborted);
await context.Response.WriteAsync(decompressedBody, context.RequestAborted);
});
});
}
public static Task Main(string[] args)
{
var host = new HostBuilder()
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
.UseKestrel()
.ConfigureLogging(factory =>
{
factory.AddConsole()
.SetMinimumLevel(LogLevel.Debug);
})
.UseStartup<Startup>();
}).Build();
return host.RunAsync();
}
}

Просмотреть файл

@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO.Compression;
namespace Microsoft.AspNetCore.RequestDecompression;
/// <summary>
/// Brotli decompression provider.
/// </summary>
internal sealed class BrotliDecompressionProvider : IDecompressionProvider
{
/// <inheritdoc />
public Stream GetDecompressionStream(Stream stream)
{
return new BrotliStream(stream, CompressionMode.Decompress, leaveOpen: true);
}
}

Просмотреть файл

@ -0,0 +1,90 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.RequestDecompression;
/// <inheritdoc />
internal sealed partial class DefaultRequestDecompressionProvider : IRequestDecompressionProvider
{
private readonly ILogger _logger;
private readonly IDictionary<string, IDecompressionProvider> _providers;
public DefaultRequestDecompressionProvider(
ILogger<DefaultRequestDecompressionProvider> logger,
IOptions<RequestDecompressionOptions> options)
{
if (logger is null)
{
throw new ArgumentNullException(nameof(logger));
}
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
_logger = logger;
_providers = options.Value.DecompressionProviders;
}
/// <inheritdoc />
public Stream? GetDecompressionStream(HttpContext context)
{
var encodings = context.Request.Headers.ContentEncoding;
if (StringValues.IsNullOrEmpty(encodings))
{
Log.NoContentEncoding(_logger);
return null;
}
if (encodings.Count > 1)
{
Log.MultipleContentEncodingsSpecified(_logger);
return null;
}
string encodingName = encodings!;
if (_providers.TryGetValue(encodingName, out var matchingProvider))
{
Log.DecompressingWith(_logger, encodingName);
context.Request.Headers.Remove(HeaderNames.ContentEncoding);
return matchingProvider.GetDecompressionStream(context.Request.Body);
}
Log.NoDecompressionProvider(_logger);
return null;
}
private static partial class Log
{
[LoggerMessage(1, LogLevel.Trace, "The Content-Encoding header is empty or not specified. Skipping request decompression.", EventName = "NoContentEncoding")]
public static partial void NoContentEncoding(ILogger logger);
[LoggerMessage(2, LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings.", EventName = "MultipleContentEncodingsSpecified")]
public static partial void MultipleContentEncodingsSpecified(ILogger logger);
[LoggerMessage(3, LogLevel.Debug, "No matching request decompression provider found.", EventName = "NoDecompressionProvider")]
public static partial void NoDecompressionProvider(ILogger logger);
public static void DecompressingWith(ILogger logger, string contentEncoding)
{
if (logger.IsEnabled(LogLevel.Debug))
{
DecompressingWithCore(logger, contentEncoding.ToLowerInvariant());
}
}
[LoggerMessage(4, LogLevel.Debug, "The request will be decompressed with '{ContentEncoding}'.", EventName = "DecompressingWith", SkipEnabledCheck = true)]
private static partial void DecompressingWithCore(ILogger logger, string contentEncoding);
}
}

Просмотреть файл

@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO.Compression;
namespace Microsoft.AspNetCore.RequestDecompression;
/// <summary>
/// DEFLATE decompression provider.
/// </summary>
internal sealed class DeflateDecompressionProvider : IDecompressionProvider
{
/// <inheritdoc />
public Stream GetDecompressionStream(Stream stream)
{
return new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true);
}
}

Просмотреть файл

@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO.Compression;
namespace Microsoft.AspNetCore.RequestDecompression;
/// <summary>
/// GZip decompression provider.
/// </summary>
internal sealed class GZipDecompressionProvider : IDecompressionProvider
{
/// <inheritdoc />
public Stream GetDecompressionStream(Stream stream)
{
return new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true);
}
}

Просмотреть файл

@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.AspNetCore.RequestDecompression;
/// <summary>
/// Provides a specific decompression implementation to decompress HTTP request bodies.
/// </summary>
public interface IDecompressionProvider
{
/// <summary>
/// Creates a new decompression stream.
/// </summary>
/// <param name="stream">The compressed request body stream.</param>
/// <returns>The decompression stream.</returns>
Stream GetDecompressionStream(Stream stream);
}

Просмотреть файл

@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.RequestDecompression;
/// <summary>
/// Used to examine requests to see if decompression should be used.
/// </summary>
public interface IRequestDecompressionProvider
{
/// <summary>
/// Examines the request and selects an acceptable decompression provider, if any.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <returns>The decompression stream when the provider is capable of decompressing the HTTP request body, otherwise <see langword="null" />.</returns>
Stream? GetDecompressionStream(HttpContext context);
}

Просмотреть файл

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core middleware for HTTP Request decompression.</Description>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<IsAspNetCoreApp>true</IsAspNetCoreApp>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore</PackageTags>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.Options" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.RequestDecompression.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.RequestDecompression.Microbenchmarks" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1 @@
#nullable enable

Просмотреть файл

@ -0,0 +1,13 @@
#nullable enable
Microsoft.AspNetCore.Builder.RequestDecompressionBuilderExtensions
Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider
Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider.GetDecompressionStream(System.IO.Stream! stream) -> System.IO.Stream!
Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider
Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.GetDecompressionStream(Microsoft.AspNetCore.Http.HttpContext! context) -> System.IO.Stream?
Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions
Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.DecompressionProviders.get -> System.Collections.Generic.IDictionary<string!, Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider!>!
Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.RequestDecompressionOptions() -> void
Microsoft.Extensions.DependencyInjection.RequestDecompressionServiceExtensions
static Microsoft.AspNetCore.Builder.RequestDecompressionBuilderExtensions.UseRequestDecompression(this Microsoft.AspNetCore.Builder.IApplicationBuilder! builder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!
static Microsoft.Extensions.DependencyInjection.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Microsoft.Extensions.DependencyInjection.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions!>! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!

Просмотреть файл

@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.RequestDecompression;
namespace Microsoft.AspNetCore.Builder;
/// <summary>
/// Extension methods for the HTTP request decompression middleware.
/// </summary>
public static class RequestDecompressionBuilderExtensions
{
/// <summary>
/// Adds middleware for dynamically decompressing HTTP request bodies.
/// </summary>
/// <param name="builder">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
public static IApplicationBuilder UseRequestDecompression(this IApplicationBuilder builder)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
return builder.UseMiddleware<RequestDecompressionMiddleware>();
}
}

Просмотреть файл

@ -0,0 +1,141 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.RequestDecompression;
/// <summary>
/// Enables HTTP request decompression.
/// </summary>
internal sealed partial class RequestDecompressionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestDecompressionMiddleware> _logger;
private readonly IRequestDecompressionProvider _provider;
/// <summary>
/// Initialize the request decompression middleware.
/// </summary>
/// <param name="next">The delegate representing the remaining middleware in the request pipeline.</param>
/// <param name="logger">The logger.</param>
/// <param name="provider">The <see cref="IRequestDecompressionProvider"/>.</param>
public RequestDecompressionMiddleware(
RequestDelegate next,
ILogger<RequestDecompressionMiddleware> logger,
IRequestDecompressionProvider provider)
{
if (next is null)
{
throw new ArgumentNullException(nameof(next));
}
if (logger is null)
{
throw new ArgumentNullException(nameof(logger));
}
if (provider is null)
{
throw new ArgumentNullException(nameof(provider));
}
_next = next;
_logger = logger;
_provider = provider;
}
/// <summary>
/// Invoke the middleware.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <returns>A task that represents the execution of this middleware.</returns>
public Task Invoke(HttpContext context)
{
SetMaxRequestBodySize(context);
var decompressionStream = _provider.GetDecompressionStream(context);
if (decompressionStream is null)
{
return _next(context);
}
return InvokeCore(context, decompressionStream);
}
private async Task InvokeCore(HttpContext context, Stream decompressionStream)
{
var request = context.Request.Body;
try
{
var sizeLimit =
context.GetEndpoint()?.Metadata?.GetMetadata<IRequestSizeLimitMetadata>()?.MaxRequestBodySize
?? context.Features.Get<IHttpMaxRequestBodySizeFeature>()?.MaxRequestBodySize;
context.Request.Body = new SizeLimitedStream(decompressionStream, sizeLimit);
await _next(context);
}
finally
{
context.Request.Body = request;
await decompressionStream.DisposeAsync();
}
}
private void SetMaxRequestBodySize(HttpContext context)
{
var sizeLimitMetadata = context.GetEndpoint()?.Metadata?.GetMetadata<IRequestSizeLimitMetadata>();
if (sizeLimitMetadata == null)
{
Log.MetadataNotFound(_logger);
return;
}
var maxRequestBodySizeFeature = context.Features.Get<IHttpMaxRequestBodySizeFeature>();
if (maxRequestBodySizeFeature == null)
{
Log.FeatureNotFound(_logger);
}
else if (maxRequestBodySizeFeature.IsReadOnly)
{
Log.FeatureIsReadOnly(_logger);
}
else
{
var maxRequestBodySize = sizeLimitMetadata.MaxRequestBodySize;
maxRequestBodySizeFeature.MaxRequestBodySize = maxRequestBodySize;
if (maxRequestBodySize.HasValue)
{
Log.MaxRequestBodySizeSet(_logger,
maxRequestBodySize.Value.ToString(CultureInfo.InvariantCulture));
}
else
{
Log.MaxRequestBodySizeDisabled(_logger);
}
}
}
private static partial class Log
{
[LoggerMessage(1, LogLevel.Debug, $"The endpoint does not specify the {nameof(IRequestSizeLimitMetadata)}.", EventName = "MetadataNotFound")]
public static partial void MetadataNotFound(ILogger logger);
[LoggerMessage(2, LogLevel.Warning, $"A request body size limit could not be applied. This server does not support the {nameof(IHttpMaxRequestBodySizeFeature)}.", EventName = "FeatureNotFound")]
public static partial void FeatureNotFound(ILogger logger);
[LoggerMessage(3, LogLevel.Warning, $"A request body size limit could not be applied. The {nameof(IHttpMaxRequestBodySizeFeature)} for the server is read-only.", EventName = "FeatureIsReadOnly")]
public static partial void FeatureIsReadOnly(ILogger logger);
[LoggerMessage(4, LogLevel.Debug, "The maximum request body size has been set to {RequestSize}.", EventName = "MaxRequestBodySizeSet")]
public static partial void MaxRequestBodySizeSet(ILogger logger, string requestSize);
[LoggerMessage(5, LogLevel.Debug, "The maximum request body size as been disabled.", EventName = "MaxRequestBodySizeDisabled")]
public static partial void MaxRequestBodySizeDisabled(ILogger logger);
}
}

Просмотреть файл

@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.AspNetCore.RequestDecompression;
/// <summary>
/// Options for the HTTP request decompression middleware.
/// </summary>
public sealed class RequestDecompressionOptions
{
/// <summary>
/// The <see cref="IDecompressionProvider"/> types to use for request decompression.
/// </summary>
public IDictionary<string, IDecompressionProvider> DecompressionProviders { get; } = new Dictionary<string, IDecompressionProvider>(StringComparer.OrdinalIgnoreCase)
{
["br"] = new BrotliDecompressionProvider(),
["deflate"] = new DeflateDecompressionProvider(),
["gzip"] = new GZipDecompressionProvider()
};
}

Просмотреть файл

@ -0,0 +1,52 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.RequestDecompression;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Extension methods for the HTTP request decompression middleware.
/// </summary>
public static class RequestDecompressionServiceExtensions
{
/// <summary>
/// Add request decompression services.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddRequestDecompression(this IServiceCollection services)
{
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}
services.TryAddSingleton<IRequestDecompressionProvider, DefaultRequestDecompressionProvider>();
return services;
}
/// <summary>
/// Add request decompression services and configure the related options.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
/// <param name="configureOptions">A delegate to configure the <see cref="RequestDecompressionOptions"/>.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddRequestDecompression(this IServiceCollection services, Action<RequestDecompressionOptions> configureOptions)
{
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}
if (configureOptions is null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
services.Configure(configureOptions);
services.TryAddSingleton<IRequestDecompressionProvider, DefaultRequestDecompressionProvider>();
return services;
}
}

Просмотреть файл

@ -0,0 +1,94 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.AspNetCore.RequestDecompression;
internal sealed class SizeLimitedStream : Stream
{
private readonly Stream _innerStream;
private readonly long? _sizeLimit;
private long _totalBytesRead;
public SizeLimitedStream(Stream innerStream, long? sizeLimit)
{
if (innerStream is null)
{
throw new ArgumentNullException(nameof(innerStream));
}
_innerStream = innerStream;
_sizeLimit = sizeLimit;
}
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)
{
var bytesRead = _innerStream.Read(buffer, offset, count);
_totalBytesRead += bytesRead;
if (_totalBytesRead > _sizeLimit)
{
throw new InvalidOperationException("The maximum number of bytes have been read.");
}
return bytesRead;
}
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 override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
}
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
var bytesRead = await _innerStream.ReadAsync(buffer, cancellationToken);
_totalBytesRead += bytesRead;
if (_totalBytesRead > _sizeLimit)
{
throw new InvalidOperationException("The maximum number of bytes have been read.");
}
return bytesRead;
}
}

Просмотреть файл

@ -0,0 +1,176 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Microsoft.Extensions.Primitives;
using System.IO.Compression;
namespace Microsoft.AspNetCore.RequestDecompression.Tests;
public class DefaultRequestDecompressionProviderTests
{
[Theory]
[InlineData("br", typeof(BrotliStream))]
[InlineData("BR", typeof(BrotliStream))]
[InlineData("deflate", typeof(DeflateStream))]
[InlineData("DEFLATE", typeof(DeflateStream))]
[InlineData("gzip", typeof(GZipStream))]
[InlineData("GZIP", typeof(GZipStream))]
public void GetDecompressionProvider_SupportedContentEncoding_ReturnsProvider(
string contentEncoding,
Type expectedProviderType)
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.Headers.Add(HeaderNames.ContentEncoding, contentEncoding);
var (logger, sink) = GetTestLogger();
var options = Options.Create(new RequestDecompressionOptions());
var provider = new DefaultRequestDecompressionProvider(logger, options);
// Act
var matchingProvider = provider.GetDecompressionStream(httpContext);
// Assert
Assert.NotNull(matchingProvider);
Assert.IsType(expectedProviderType, matchingProvider);
var logMessages = sink.Writes.ToList();
AssertLog(logMessages.Single(), LogLevel.Debug,
$"The request will be decompressed with '{contentEncoding.ToLowerInvariant()}'.");
var contentEncodingHeader = httpContext.Request.Headers.ContentEncoding;
Assert.Empty(contentEncodingHeader);
}
[Fact]
public void GetDecompressionProvider_NoContentEncoding_ReturnsNull()
{
// Arrange
var httpContext = new DefaultHttpContext();
var (logger, sink) = GetTestLogger();
var options = Options.Create(new RequestDecompressionOptions());
var provider = new DefaultRequestDecompressionProvider(logger, options);
// Act
var matchingProvider = provider.GetDecompressionStream(httpContext);
// Assert
Assert.Null(matchingProvider);
var logMessages = sink.Writes.ToList();
AssertLog(logMessages.Single(), LogLevel.Trace,
"The Content-Encoding header is empty or not specified. Skipping request decompression.");
var contentEncodingHeader = httpContext.Request.Headers.ContentEncoding;
Assert.Empty(contentEncodingHeader);
}
[Fact]
public void GetDecompressionProvider_UnsupportedContentEncoding_ReturnsNull()
{
// Arrange
var contentEncoding = "custom";
var httpContext = new DefaultHttpContext();
httpContext.Request.Headers.Add(HeaderNames.ContentEncoding, contentEncoding);
var (logger, sink) = GetTestLogger();
var options = Options.Create(new RequestDecompressionOptions());
var provider = new DefaultRequestDecompressionProvider(logger, options);
// Act
var matchingProvider = provider.GetDecompressionStream(httpContext);
// Assert
Assert.Null(matchingProvider);
var logMessages = sink.Writes.ToList();
AssertLog(logMessages.Single(),
LogLevel.Debug, "No matching request decompression provider found.");
var contentEncodingHeader = httpContext.Request.Headers.ContentEncoding;
Assert.Equal(contentEncoding, contentEncodingHeader);
}
[Fact]
public void GetDecompressionProvider_MultipleContentEncodings_ReturnsNull()
{
// Arrange
var contentEncodings = new StringValues(new[] { "br", "gzip" });
var httpContext = new DefaultHttpContext();
httpContext.Request.Headers.Add(HeaderNames.ContentEncoding, contentEncodings);
var (logger, sink) = GetTestLogger();
var options = Options.Create(new RequestDecompressionOptions());
var provider = new DefaultRequestDecompressionProvider(logger, options);
// Act
var matchingProvider = provider.GetDecompressionStream(httpContext);
// Assert
Assert.Null(matchingProvider);
var logMessages = sink.Writes.ToList();
AssertLog(logMessages.Single(), LogLevel.Debug,
"Request decompression is not supported for multiple Content-Encodings.");
var contentEncodingHeader = httpContext.Request.Headers.ContentEncoding;
Assert.Equal(contentEncodings, contentEncodingHeader);
}
[Fact]
public void Ctor_NullLogger_Throws()
{
// Arrange
var (logger, _) = GetTestLogger();
IOptions<RequestDecompressionOptions> options = null;
// Act + Assert
Assert.Throws<ArgumentNullException>(() =>
{
new DefaultRequestDecompressionProvider(logger, options);
});
}
[Fact]
public void Ctor_NullOptions_Throws()
{
// Arrange
ILogger<DefaultRequestDecompressionProvider> logger = null;
var options = Options.Create(new RequestDecompressionOptions());
// Act + Assert
Assert.Throws<ArgumentNullException>(() =>
{
new DefaultRequestDecompressionProvider(logger, options);
});
}
private static (ILogger<DefaultRequestDecompressionProvider>, TestSink) GetTestLogger()
{
var sink = new TestSink(
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var logger = loggerFactory.CreateLogger<DefaultRequestDecompressionProvider>();
return (logger, sink);
}
private static void AssertLog(WriteContext log, LogLevel level, string message)
{
Assert.Equal(level, log.LogLevel);
Assert.Equal(message, log.State.ToString());
}
}

Просмотреть файл

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.AspNetCore.RequestDecompression" />
<Reference Include="Microsoft.AspNetCore.TestHost" />
<Reference Include="Microsoft.Net.Http.Headers" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Builder;
namespace Microsoft.AspNetCore.RequestDecompression.Tests;
public class RequestDecompressionBuilderExtensionsTests
{
[Fact]
public void UseRequestDecompression_NullApplicationBuilder_Throws()
{
// Arrange
IApplicationBuilder builder = null;
// Act + Assert
Assert.Throws<ArgumentNullException>(() =>
{
builder.UseRequestDecompression();
});
}
}

Просмотреть файл

@ -0,0 +1,945 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using System.IO.Compression;
using System.Net.Http;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.RequestDecompression.Tests;
public class RequestDecompressionMiddlewareTests
{
private const string TestRequestBodyData = "Test Request Body Data";
private static byte[] GetUncompressedContent(string input = TestRequestBodyData)
{
return Encoding.UTF8.GetBytes(input);
}
private static async Task<byte[]> GetCompressedContent(
Func<Stream, Stream> compressorDelegate,
byte[] uncompressedBytes)
{
await using var uncompressedStream = new MemoryStream(uncompressedBytes);
await using var compressedStream = new MemoryStream();
await using (var compressor = compressorDelegate(compressedStream))
{
await uncompressedStream.CopyToAsync(compressor);
}
return compressedStream.ToArray();
}
private static async Task<byte[]> GetBrotliCompressedContent(byte[] uncompressedBytes)
{
static Stream compressorDelegate(Stream compressedContent) =>
new BrotliStream(compressedContent, CompressionMode.Compress);
return await GetCompressedContent(compressorDelegate, uncompressedBytes);
}
private static async Task<byte[]> GetDeflateCompressedContent(byte[] uncompressedBytes)
{
static Stream compressorDelegate(Stream compressedContent) =>
new DeflateStream(compressedContent, CompressionMode.Compress);
return await GetCompressedContent(compressorDelegate, uncompressedBytes);
}
private static async Task<byte[]> GetGZipCompressedContent(byte[] uncompressedBytes)
{
static Stream compressorDelegate(Stream compressedContent) =>
new GZipStream(compressedContent, CompressionMode.Compress);
return await GetCompressedContent(compressorDelegate, uncompressedBytes);
}
[Fact]
public async Task Request_ContentEncodingBrotli_Decompressed()
{
// Arrange
var contentEncoding = "br";
var uncompressedBytes = GetUncompressedContent();
var compressedBytes = await GetBrotliCompressedContent(uncompressedBytes);
// Act
var (logMessages, decompressedBytes) = await InvokeMiddleware(compressedBytes, new[] { contentEncoding });
// Assert
AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant());
Assert.Equal(uncompressedBytes, decompressedBytes);
}
[Fact]
public async Task Request_ContentEncodingDeflate_Decompressed()
{
// Arrange
var contentEncoding = "deflate";
var uncompressedBytes = GetUncompressedContent();
var compressedBytes = await GetDeflateCompressedContent(uncompressedBytes);
// Act
var (logMessages, decompressedBytes) = await InvokeMiddleware(compressedBytes, new[] { contentEncoding });
// Assert
AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant());
Assert.Equal(uncompressedBytes, decompressedBytes);
}
[Fact]
public async Task Request_ContentEncodingGzip_Decompressed()
{
// Arrange
var contentEncoding = "gzip";
var uncompressedBytes = GetUncompressedContent();
var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
// Act
var (logMessages, decompressedBytes) = await InvokeMiddleware(compressedBytes, new[] { contentEncoding });
// Assert
AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant());
Assert.Equal(uncompressedBytes, decompressedBytes);
}
[Fact]
public async Task Request_NoContentEncoding_NotDecompressed()
{
// Arrange
var uncompressedBytes = GetUncompressedContent();
// Act
var (logMessages, outputBytes) = await InvokeMiddleware(uncompressedBytes);
// Assert
var logMessage = Assert.Single(logMessages);
AssertLog(logMessage, LogLevel.Trace, "The Content-Encoding header is empty or not specified. Skipping request decompression.");
Assert.Equal(uncompressedBytes, outputBytes);
}
[Fact]
public async Task Request_UnsupportedContentEncoding_NotDecompressed()
{
// Arrange
var uncompressedBytes = GetUncompressedContent();
var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
var contentEncoding = "custom";
// Act
var (logMessages, outputBytes) = await InvokeMiddleware(compressedBytes, new[] { contentEncoding });
// Assert
AssertNoDecompressionProviderLog(logMessages);
Assert.Equal(compressedBytes, outputBytes);
}
[Fact]
public async Task Request_MultipleContentEncodings_NotDecompressed()
{
// Arrange
var uncompressedBytes = GetUncompressedContent();
var inputBytes = await GetGZipCompressedContent(uncompressedBytes);
var contentEncodings = new[] { "br", "gzip" };
// Act
var (logMessages, outputBytes) = await InvokeMiddleware(inputBytes, contentEncodings);
// Assert
var logMessage = Assert.Single(logMessages);
AssertLog(logMessage, LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings.");
Assert.Equal(inputBytes, outputBytes);
}
[Fact]
public async Task Request_MiddlewareAddedMultipleTimes_OnlyDecompressedOnce()
{
// Arrange
var uncompressedBytes = GetUncompressedContent();
var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
var contentEncoding = "gzip";
var decompressedBytes = Array.Empty<byte>();
var sink = new TestSink(
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
using var host = new HostBuilder()
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddRequestDecompression();
services.AddSingleton<ILoggerFactory>(loggerFactory);
})
.Configure(app =>
{
app.Use((context, next) =>
{
context.Features.Set<IHttpMaxRequestBodySizeFeature>(
new FakeHttpMaxRequestBodySizeFeature());
return next(context);
});
app.UseRequestDecompression();
app.UseRequestDecompression();
app.Run(async context =>
{
await using var ms = new MemoryStream();
await context.Request.Body.CopyToAsync(ms, context.RequestAborted);
decompressedBytes = ms.ToArray();
});
});
}).Build();
await host.StartAsync();
var server = host.GetTestServer();
var client = server.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "");
request.Content = new ByteArrayContent(compressedBytes);
request.Content.Headers.ContentEncoding.Add(contentEncoding);
// Act
await client.SendAsync(request);
// Assert
var logMessages = sink.Writes.ToList();
Assert.Equal(2, logMessages.Count);
AssertLog(logMessages.First(), LogLevel.Debug, $"The request will be decompressed with '{contentEncoding}'.");
AssertLog(logMessages.Skip(1).First(), LogLevel.Trace, "The Content-Encoding header is empty or not specified. Skipping request decompression.");
Assert.Equal(uncompressedBytes, decompressedBytes);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Request_Decompressed_ContentEncodingHeaderRemoved(bool isDecompressed)
{
// Arrange
var contentEncoding = isDecompressed ? "gzip" : "custom";
var contentEncodingHeader = new StringValues();
var uncompressedBytes = GetUncompressedContent();
var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
var outputBytes = Array.Empty<byte>();
var sink = new TestSink(
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
using var host = new HostBuilder()
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddRequestDecompression();
services.AddSingleton<ILoggerFactory>(loggerFactory);
})
.Configure(app =>
{
app.Use((context, next) =>
{
context.Features.Set<IHttpMaxRequestBodySizeFeature>(
new FakeHttpMaxRequestBodySizeFeature());
return next(context);
});
app.UseRequestDecompression();
app.Run(async context =>
{
contentEncodingHeader = context.Request.Headers.ContentEncoding;
await using var ms = new MemoryStream();
await context.Request.Body.CopyToAsync(ms, context.RequestAborted);
outputBytes = ms.ToArray();
});
});
}).Build();
await host.StartAsync();
var server = host.GetTestServer();
var client = server.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "");
request.Content = new ByteArrayContent(compressedBytes);
request.Content.Headers.ContentEncoding.Add(contentEncoding);
// Act
await client.SendAsync(request);
// Assert
var logMessages = sink.Writes.ToList();
if (isDecompressed)
{
Assert.Empty(contentEncodingHeader);
AssertDecompressedWithLog(logMessages, contentEncoding);
Assert.Equal(uncompressedBytes, outputBytes);
}
else
{
Assert.Equal(contentEncoding, contentEncodingHeader);
AssertNoDecompressionProviderLog(logMessages);
Assert.Equal(compressedBytes, outputBytes);
}
}
[Fact]
public async Task Request_InvalidDataForContentEncoding_ThrowsInvalidOperationException()
{
// Arrange
var uncompressedBytes = GetUncompressedContent();
var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
var contentEncoding = "br";
Exception exception = null;
var sink = new TestSink(
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
using var host = new HostBuilder()
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddRequestDecompression();
services.AddSingleton<ILoggerFactory>(loggerFactory);
})
.Configure(app =>
{
app.Use((context, next) =>
{
context.Features.Set<IHttpMaxRequestBodySizeFeature>(
new FakeHttpMaxRequestBodySizeFeature());
return next(context);
});
app.UseRequestDecompression();
app.Run(async context =>
{
exception = await Record.ExceptionAsync(async () =>
{
using var ms = new MemoryStream();
await context.Request.Body.CopyToAsync(ms, context.RequestAborted);
});
});
});
}).Build();
await host.StartAsync();
var server = host.GetTestServer();
var client = server.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "");
request.Content = new ByteArrayContent(compressedBytes);
request.Content.Headers.ContentEncoding.Add(contentEncoding);
// Act
await client.SendAsync(request);
// Assert
var logMessages = sink.Writes.ToList();
AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant());
Assert.NotNull(exception);
Assert.IsAssignableFrom<InvalidOperationException>(exception);
}
[Fact]
public async Task Options_RegisterCustomDecompressionProvider()
{
// Arrange
var uncompressedBytes = GetUncompressedContent();
var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
var contentEncoding = "custom";
// Act
var (logMessages, decompressedBytes) =
await InvokeMiddleware(
compressedBytes,
new[] { contentEncoding },
configure: (RequestDecompressionOptions options) =>
{
options.DecompressionProviders.Add(contentEncoding, new CustomDecompressionProvider());
});
// Assert
AssertDecompressedWithLog(logMessages, contentEncoding);
Assert.Equal(uncompressedBytes, decompressedBytes);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Endpoint_HasRequestSizeLimit_UsedForRequest(bool exceedsLimit)
{
// Arrange
long attributeSizeLimit = 10;
long featureSizeLimit = 5;
var contentEncoding = "gzip";
var uncompressedBytes = new byte[attributeSizeLimit + (exceedsLimit ? 1 : 0)];
var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
var decompressedBytes = Array.Empty<byte>();
Exception exception = null;
var sink = new TestSink(
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
using var host = new HostBuilder()
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddRequestDecompression();
services.AddSingleton<ILoggerFactory>(loggerFactory);
})
.Configure(app =>
{
app.Use((context, next) =>
{
context.Features.Set<IEndpointFeature>(
GetFakeEndpointFeature(attributeSizeLimit));
context.Features.Set<IHttpMaxRequestBodySizeFeature>(
new FakeHttpMaxRequestBodySizeFeature(featureSizeLimit));
return next(context);
});
app.UseRequestDecompression();
app.Run(async context =>
{
await using var ms = new MemoryStream();
exception = await Record.ExceptionAsync(async () =>
{
await context.Request.Body.CopyToAsync(ms, context.RequestAborted);
decompressedBytes = ms.ToArray();
});
decompressedBytes = ms.ToArray();
});
});
}).Build();
await host.StartAsync();
var server = host.GetTestServer();
var client = server.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "");
request.Content = new ByteArrayContent(compressedBytes);
request.Content.Headers.ContentEncoding.Add(contentEncoding);
// Act
await client.SendAsync(request);
// Assert
var logMessages = sink.Writes.ToList();
AssertDecompressedWithLog(logMessages, contentEncoding);
if (exceedsLimit)
{
Assert.NotNull(exception);
Assert.IsAssignableFrom<InvalidOperationException>(exception);
Assert.Equal("The maximum number of bytes have been read.", exception.Message);
}
else
{
Assert.Null(exception);
Assert.Equal(uncompressedBytes, decompressedBytes);
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Feature_HasRequestSizeLimit_UsedForRequest(bool exceedsLimit)
{
// Arrange
long featureSizeLimit = 10;
var contentEncoding = "gzip";
var uncompressedBytes = new byte[featureSizeLimit + (exceedsLimit ? 1 : 0)];
var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
var decompressedBytes = Array.Empty<byte>();
Exception exception = null;
var sink = new TestSink(
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
using var host = new HostBuilder()
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddRequestDecompression();
services.AddSingleton<ILoggerFactory>(loggerFactory);
})
.Configure(app =>
{
app.Use((context, next) =>
{
context.Features.Set<IHttpMaxRequestBodySizeFeature>(
new FakeHttpMaxRequestBodySizeFeature(featureSizeLimit));
return next(context);
});
app.UseRequestDecompression();
app.Run(async context =>
{
await using var ms = new MemoryStream();
exception = await Record.ExceptionAsync(async () =>
{
await context.Request.Body.CopyToAsync(ms, context.RequestAborted);
decompressedBytes = ms.ToArray();
});
decompressedBytes = ms.ToArray();
});
});
}).Build();
await host.StartAsync();
var server = host.GetTestServer();
var client = server.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "");
request.Content = new ByteArrayContent(compressedBytes);
request.Content.Headers.ContentEncoding.Add(contentEncoding);
// Act
await client.SendAsync(request);
// Assert
var logMessages = sink.Writes.ToList();
AssertDecompressedWithLog(logMessages, contentEncoding);
if (exceedsLimit)
{
Assert.NotNull(exception);
Assert.IsAssignableFrom<InvalidOperationException>(exception);
Assert.Equal("The maximum number of bytes have been read.", exception.Message);
}
else
{
Assert.Null(exception);
Assert.Equal(uncompressedBytes, decompressedBytes);
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Endpoint_DoesNotHaveSizeLimitMetadata(bool isCompressed)
{
// Arrange
var sink = new TestSink();
var logger = new TestLogger<RequestDecompressionMiddleware>(
new TestLoggerFactory(sink, enabled: true));
IRequestDecompressionProvider provider = new FakeRequestDecompressionProvider(isCompressed);
var middleware = new RequestDecompressionMiddleware(
c =>
{
c.Response.StatusCode = StatusCodes.Status200OK;
return Task.CompletedTask;
},
logger,
provider);
var context = new DefaultHttpContext();
IEndpointFeature endpointFeature = new FakeEndpointFeature
{
Endpoint = new Endpoint(
requestDelegate: null,
metadata: new EndpointMetadataCollection(),
displayName: null)
};
context.HttpContext.Features.Set(endpointFeature);
long expectedRequestSizeLimit = 100;
IHttpMaxRequestBodySizeFeature maxRequestBodySizeFeature =
new FakeHttpMaxRequestBodySizeFeature(expectedRequestSizeLimit);
context.HttpContext.Features.Set(maxRequestBodySizeFeature);
// Act
await middleware.Invoke(context);
// Assert
var logMessages = sink.Writes.ToList();
AssertLog(Assert.Single(logMessages), LogLevel.Debug,
$"The endpoint does not specify the {nameof(IRequestSizeLimitMetadata)}.");
var actualRequestSizeLimit = maxRequestBodySizeFeature.MaxRequestBodySize;
Assert.Equal(expectedRequestSizeLimit, actualRequestSizeLimit);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Endpoint_DoesNotHaveBodySizeFeature(bool isCompressed)
{
// Arrange
var sink = new TestSink();
var logger = new TestLogger<RequestDecompressionMiddleware>(
new TestLoggerFactory(sink, enabled: true));
IRequestDecompressionProvider provider = new FakeRequestDecompressionProvider(isCompressed);
var middleware = new RequestDecompressionMiddleware(
c =>
{
c.Response.StatusCode = StatusCodes.Status200OK;
return Task.CompletedTask;
},
logger,
provider);
var context = new DefaultHttpContext();
IEndpointFeature endpointFeature = GetFakeEndpointFeature(100);
context.HttpContext.Features.Set(endpointFeature);
IHttpMaxRequestBodySizeFeature maxRequestBodySizeFeature = null;
context.HttpContext.Features.Set(maxRequestBodySizeFeature);
// Act
await middleware.Invoke(context);
// Assert
var logMessages = sink.Writes.ToList();
AssertLog(Assert.Single(logMessages), LogLevel.Warning,
$"A request body size limit could not be applied. This server does not support the {nameof(IHttpMaxRequestBodySizeFeature)}.");
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Endpoint_BodySizeFeatureIsReadOnly(bool isCompressed)
{
// Arrange
var sink = new TestSink();
var logger = new TestLogger<RequestDecompressionMiddleware>(
new TestLoggerFactory(sink, enabled: true));
IRequestDecompressionProvider provider = new FakeRequestDecompressionProvider(isCompressed);
var middleware = new RequestDecompressionMiddleware(
c =>
{
c.Response.StatusCode = StatusCodes.Status200OK;
return Task.CompletedTask;
},
logger,
provider);
var context = new DefaultHttpContext();
IEndpointFeature endpointFeature = GetFakeEndpointFeature(100);
context.HttpContext.Features.Set(endpointFeature);
long expectedRequestSizeLimit = 50;
IHttpMaxRequestBodySizeFeature maxRequestBodySizeFeature =
new FakeHttpMaxRequestBodySizeFeature(expectedRequestSizeLimit, isReadOnly: true);
context.HttpContext.Features.Set(maxRequestBodySizeFeature);
// Act
await middleware.Invoke(context);
// Assert
var logMessages = sink.Writes.ToList();
AssertLog(Assert.Single(logMessages), LogLevel.Warning,
$"A request body size limit could not be applied. The {nameof(IHttpMaxRequestBodySizeFeature)} for the server is read-only.");
var actualRequestSizeLimit = maxRequestBodySizeFeature.MaxRequestBodySize;
Assert.Equal(expectedRequestSizeLimit, actualRequestSizeLimit);
}
[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, false)]
[InlineData(false, true)]
public async Task Endpoint_HasBodySizeFeature_SetUsingSizeLimitMetadata(bool isCompressed, bool isRequestSizeLimitDisabled)
{
// Arrange
var sink = new TestSink();
var logger = new TestLogger<RequestDecompressionMiddleware>(
new TestLoggerFactory(sink, enabled: true));
IRequestDecompressionProvider provider = new FakeRequestDecompressionProvider(isCompressed);
var middleware = new RequestDecompressionMiddleware(
c =>
{
c.Response.StatusCode = StatusCodes.Status200OK;
return Task.CompletedTask;
},
logger,
provider);
var context = new DefaultHttpContext();
long? expectedRequestSizeLimit = isRequestSizeLimitDisabled ? null : 100;
IEndpointFeature endpointFeature = GetFakeEndpointFeature(expectedRequestSizeLimit);
context.HttpContext.Features.Set(endpointFeature);
IHttpMaxRequestBodySizeFeature maxRequestBodySizeFeature =
new FakeHttpMaxRequestBodySizeFeature(50);
context.HttpContext.Features.Set(maxRequestBodySizeFeature);
// Act
await middleware.Invoke(context);
// Assert
var logMessages = sink.Writes.ToList();
if (isRequestSizeLimitDisabled)
{
AssertLog(Assert.Single(logMessages), LogLevel.Debug,
"The maximum request body size as been disabled.");
}
else
{
AssertLog(Assert.Single(logMessages), LogLevel.Debug,
$"The maximum request body size has been set to {expectedRequestSizeLimit.Value.ToString(CultureInfo.InvariantCulture)}.");
}
var actualRequestSizeLimit = maxRequestBodySizeFeature.MaxRequestBodySize;
Assert.Equal(expectedRequestSizeLimit, actualRequestSizeLimit);
}
[Fact]
public void Ctor_NullRequestDelegate_Throws()
{
// Arrange
RequestDelegate requestDelegate = null;
var logger = new TestLogger<RequestDecompressionMiddleware>(
new TestLoggerFactory(new TestSink(), enabled: true));
var provider = new FakeRequestDecompressionProvider();
// Act + Assert
Assert.Throws<ArgumentNullException>(() =>
{
new RequestDecompressionMiddleware(requestDelegate, logger, provider);
});
}
[Fact]
public void Ctor_NullLogger_Throws()
{
// Arrange
static Task requestDelegate(HttpContext context) => Task.FromResult(context);
ILogger<RequestDecompressionMiddleware> logger = null;
var provider = new FakeRequestDecompressionProvider();
// Act + Assert
Assert.Throws<ArgumentNullException>(() =>
{
new RequestDecompressionMiddleware(requestDelegate, logger, provider);
});
}
[Fact]
public void Ctor_NullRequestDecompressionProvider_Throws()
{
// Arrange
static Task requestDelegate(HttpContext context) => Task.FromResult(context);
var logger = new TestLogger<RequestDecompressionMiddleware>(
new TestLoggerFactory(new TestSink(), enabled: true));
IRequestDecompressionProvider provider = null;
// Act + Assert
Assert.Throws<ArgumentNullException>(() =>
{
new RequestDecompressionMiddleware(requestDelegate, logger, provider);
});
}
private class FakeRequestDecompressionProvider : IRequestDecompressionProvider
{
private readonly bool _isCompressed;
public FakeRequestDecompressionProvider(bool isCompressed = false)
{
_isCompressed = isCompressed;
}
#nullable enable
public Stream? GetDecompressionStream(HttpContext context)
=> _isCompressed
? new MemoryStream()
: null;
#nullable disable
}
private static void AssertLog(WriteContext log, LogLevel level, string message)
{
Assert.Equal(level, log.LogLevel);
Assert.Equal(message, log.State.ToString());
}
private static void AssertDecompressedWithLog(List<WriteContext> logMessages, string encoding)
{
var logMessage = Assert.Single(logMessages);
AssertLog(logMessage, LogLevel.Debug, $"The request will be decompressed with '{encoding}'.");
}
private static void AssertNoDecompressionProviderLog(List<WriteContext> logMessages)
{
var logMessage = Assert.Single(logMessages);
AssertLog(logMessage, LogLevel.Debug, "No matching request decompression provider found.");
}
private static async Task<(List<WriteContext>, byte[])> InvokeMiddleware(
byte[] compressedContent,
string[] contentEncodings = null,
Action<RequestDecompressionOptions> configure = null)
{
var sink = new TestSink(
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var outputContent = Array.Empty<byte>();
using var host = new HostBuilder()
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddRequestDecompression(configure ?? (_ => { }));
services.AddSingleton<ILoggerFactory>(loggerFactory);
})
.Configure(app =>
{
app.Use((context, next) =>
{
context.Features.Set<IHttpMaxRequestBodySizeFeature>(
new FakeHttpMaxRequestBodySizeFeature());
return next(context);
});
app.UseRequestDecompression();
app.Run(async context =>
{
await using var ms = new MemoryStream();
await context.Request.Body.CopyToAsync(ms, context.RequestAborted);
outputContent = ms.ToArray();
});
});
}).Build();
await host.StartAsync();
var server = host.GetTestServer();
var client = server.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "");
request.Content = new ByteArrayContent(compressedContent);
if (contentEncodings != null)
{
foreach (var encoding in contentEncodings)
{
request.Content.Headers.ContentEncoding.Add(encoding);
}
}
await client.SendAsync(request);
return (sink.Writes.ToList(), outputContent);
}
private class CustomDecompressionProvider : IDecompressionProvider
{
public Stream GetDecompressionStream(Stream stream)
{
return new GZipStream(stream, CompressionMode.Decompress);
}
}
private static FakeEndpointFeature GetFakeEndpointFeature(long? requestSizeLimit)
{
var requestSizeLimitMetadata = new FakeRequestSizeLimitMetadata
{
MaxRequestBodySize = requestSizeLimit
};
var endpointMetadata =
new EndpointMetadataCollection(new[] { requestSizeLimitMetadata });
return new FakeEndpointFeature
{
Endpoint = new Endpoint(
requestDelegate: null,
metadata: endpointMetadata,
displayName: null)
};
}
private class FakeEndpointFeature : IEndpointFeature
{
public Endpoint Endpoint { get; set; }
}
private class FakeRequestSizeLimitMetadata : IRequestSizeLimitMetadata
{
public long? MaxRequestBodySize { get; set; }
}
private class FakeHttpMaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature
{
public FakeHttpMaxRequestBodySizeFeature(
long? maxRequestBodySize = null,
bool isReadOnly = false)
{
MaxRequestBodySize = maxRequestBodySize;
IsReadOnly = isReadOnly;
}
public bool IsReadOnly { get; }
public long? MaxRequestBodySize { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.AspNetCore.RequestDecompression.Tests;
public class RequestDecompressionOptionsTests
{
[Fact]
public void Options_InitializedWithDefaultProviders()
{
// Arrange
var defaultProviderCount = 3;
// Act
var options = new RequestDecompressionOptions();
// Assert
var providers = options.DecompressionProviders;
Assert.Equal(defaultProviderCount, providers.Count);
var brotliProvider = Assert.Contains("br", providers);
Assert.IsType<BrotliDecompressionProvider>(brotliProvider);
var deflateProvider = Assert.Contains("deflate", providers);
Assert.IsType<DeflateDecompressionProvider>(deflateProvider);
var gzipProvider = Assert.Contains("gzip", providers);
Assert.IsType<GZipDecompressionProvider>(gzipProvider);
}
}

Просмотреть файл

@ -0,0 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.RequestDecompression.Tests;
public class RequestDecompressionServiceExtensionsTests
{
[Fact]
public void AddRequestDecompression_NullServiceCollection_Throws()
{
// Arrange
IServiceCollection serviceCollection = null;
var configureOptions = (RequestDecompressionOptions options) => { };
// Act + Assert
Assert.Throws<ArgumentNullException>(() =>
{
serviceCollection.AddRequestDecompression(configureOptions);
});
}
[Fact]
public void AddRequestDecompression_NullConfigureOptions_Throws()
{
// Arrange
var serviceCollection = new ServiceCollection();
Action<RequestDecompressionOptions> configureOptions = null;
// Act + Assert
Assert.Throws<ArgumentNullException>(() =>
{
serviceCollection.AddRequestDecompression(configureOptions);
});
}
}

Просмотреть файл

@ -0,0 +1,107 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.AspNetCore.RequestDecompression.Tests;
public class SizeLimitedStreamTests
{
[Fact]
public void Ctor_NullInnerStream_Throws()
{
// Arrange
Stream innerStream = null;
// Act + Assert
Assert.Throws<ArgumentNullException>(() =>
{
using var sizeLimitedStream = new SizeLimitedStream(innerStream, 1);
});
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task ReadAsync_InnerStreamExceedsSizeLimit_Throws(bool exceedsLimit)
{
// Arrange
var sizeLimit = 10;
var bytes = new byte[sizeLimit + (exceedsLimit ? 1 : 0)];
using var innerStream = new MemoryStream(bytes);
using var sizeLimitedStream = new SizeLimitedStream(innerStream, sizeLimit);
var buffer = new byte[bytes.Length];
// Act
var exception = await Record.ExceptionAsync(async () =>
{
while (await sizeLimitedStream.ReadAsync(buffer) > 0) { }
});
// Assert
AssertStreamReadingException(exception, exceedsLimit);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Read_InnerStreamExceedsSizeLimit_Throws(bool exceedsLimit)
{
// Arrange
var sizeLimit = 10;
var bytes = new byte[sizeLimit + (exceedsLimit ? 1 : 0)];
using var innerStream = new MemoryStream(bytes);
using var sizeLimitedStream = new SizeLimitedStream(innerStream, sizeLimit);
var buffer = new byte[bytes.Length];
// Act
var exception = Record.Exception(() =>
{
while (sizeLimitedStream.Read(buffer, 0, buffer.Length) > 0) { }
});
// Assert
AssertStreamReadingException(exception, exceedsLimit);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void BeginRead_InnerStreamExceedsSizeLimit_Throws(bool exceedsLimit)
{
// Arrange
var sizeLimit = 10;
var bytes = new byte[sizeLimit + (exceedsLimit ? 1 : 0)];
using var innerStream = new MemoryStream(bytes);
using var sizeLimitedStream = new SizeLimitedStream(innerStream, sizeLimit);
var buffer = new byte[bytes.Length];
// Act
var exception = Record.Exception(() =>
{
var asyncResult = sizeLimitedStream.BeginRead(buffer, 0, buffer.Length, (o) => { }, null);
sizeLimitedStream.EndRead(asyncResult);
});
// Assert
AssertStreamReadingException(exception, exceedsLimit);
}
private static void AssertStreamReadingException(Exception exception, bool exceedsLimit)
{
if (exceedsLimit)
{
Assert.NotNull(exception);
Assert.IsAssignableFrom<InvalidOperationException>(exception);
Assert.Equal("The maximum number of bytes have been read.", exception.Message);
}
else
{
Assert.Null(exception);
}
}
}

Просмотреть файл

@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
@ -9,8 +10,12 @@ namespace Microsoft.AspNetCore.Mvc;
/// <summary>
/// Disables the request body size limit.
/// </summary>
/// <remarks>
/// Disabling the request body size limit can be a security concern in regards to uncontrolled
/// resource consumption, particularly if the request body is being buffered.
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class DisableRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter
public class DisableRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter, IRequestSizeLimitMetadata
{
/// <summary>
/// Gets the order value for determining the order of execution of filters. Filters execute in
@ -39,4 +44,7 @@ public class DisableRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrde
var filter = serviceProvider.GetRequiredService<DisableRequestSizeLimitFilter>();
return filter;
}
/// <inheritdoc />
long? IRequestSizeLimitMetadata.MaxRequestBodySize => null;
}

Просмотреть файл

@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
@ -10,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc;
/// 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
public class RequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter, IRequestSizeLimitMetadata
{
private readonly long _bytes;
@ -51,4 +52,7 @@ public class RequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilt
filter.Bytes = _bytes;
return filter;
}
/// <inheritdoc />
long? IRequestSizeLimitMetadata.MaxRequestBodySize => _bytes;
}