зеркало из https://github.com/dotnet/aspnetcore.git
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:
Родитель
d96a100bdd
Коммит
d5a539f19b
|
@ -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;
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче