From 77923d609f43d932d79cf292f3a292944d30ce2b Mon Sep 17 00:00:00 2001 From: David Acker Date: Wed, 16 Feb 2022 20:31:02 -0500 Subject: [PATCH 01/42] Add Request Decompression middleware --- AspNetCore.sln | 92 ++++--- eng/ProjectReferences.props | 1 + eng/SharedFramework.Local.props | 1 + src/Middleware/Middleware.slnf | 5 +- .../sample/CustomDecompressionProvider.cs | 17 ++ .../sample/Properties/launchsettings.json | 27 ++ .../sample/RequestDecompressionSample.csproj | 13 + .../RequestDecompression/sample/Startup.cs | 52 ++++ .../src/BrotliDecompressionProvider.cs | 21 ++ .../src/DecompressionProviderCollection.cs | 46 ++++ .../src/DecompressionProviderFactory.cs | 40 +++ .../src/DeflateDecompressionProvider.cs | 21 ++ .../src/GzipDecompressionProvider.cs | 21 ++ .../src/IDecompressionProvider.cs | 22 ++ .../src/IRequestDecompressionProvider.cs | 33 +++ ...oft.AspNetCore.RequestDecompression.csproj | 24 ++ .../src/Properties/AssemblyInfo.cs | 6 + .../src/PublicAPI.Shipped.txt | 1 + .../src/PublicAPI.Unshipped.txt | 41 +++ .../src/RequestDecompressionBody.cs | 152 +++++++++++ .../RequestDecompressionBuilderExtensions.cs | 26 ++ .../RequestDecompressionLoggingExtensions.cs | 30 +++ .../src/RequestDecompressionMiddleware.cs | 69 +++++ .../src/RequestDecompressionOptions.cs | 15 ++ .../src/RequestDecompressionProvider.cs | 144 ++++++++++ .../RequestDecompressionServiceExtensions.cs | 51 ++++ ...pNetCore.RequestDecompression.Tests.csproj | 14 + .../RequestDecompressionMiddlewareTests.cs | 254 ++++++++++++++++++ 28 files changed, 1206 insertions(+), 33 deletions(-) create mode 100644 src/Middleware/RequestDecompression/sample/CustomDecompressionProvider.cs create mode 100644 src/Middleware/RequestDecompression/sample/Properties/launchsettings.json create mode 100644 src/Middleware/RequestDecompression/sample/RequestDecompressionSample.csproj create mode 100644 src/Middleware/RequestDecompression/sample/Startup.cs create mode 100644 src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs create mode 100644 src/Middleware/RequestDecompression/src/DecompressionProviderCollection.cs create mode 100644 src/Middleware/RequestDecompression/src/DecompressionProviderFactory.cs create mode 100644 src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs create mode 100644 src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs create mode 100644 src/Middleware/RequestDecompression/src/IDecompressionProvider.cs create mode 100644 src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs create mode 100644 src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj create mode 100644 src/Middleware/RequestDecompression/src/Properties/AssemblyInfo.cs create mode 100644 src/Middleware/RequestDecompression/src/PublicAPI.Shipped.txt create mode 100644 src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt create mode 100644 src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs create mode 100644 src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs create mode 100644 src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs create mode 100644 src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs create mode 100644 src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs create mode 100644 src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs create mode 100644 src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs create mode 100644 src/Middleware/RequestDecompression/test/Microsoft.AspNetCore.RequestDecompression.Tests.csproj create mode 100644 src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index 6cccef647f8d..98db4b132213 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1658,6 +1658,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Compon EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -3414,38 +3422,6 @@ Global {8DE6625B-C9E4-4949-A75C-89E3FF556724}.Release|x64.Build.0 = Release|Any CPU {8DE6625B-C9E4-4949-A75C-89E3FF556724}.Release|x86.ActiveCfg = Release|Any CPU {8DE6625B-C9E4-4949-A75C-89E3FF556724}.Release|x86.Build.0 = Release|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Debug|arm64.ActiveCfg = Debug|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Debug|arm64.Build.0 = Debug|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Debug|x64.ActiveCfg = Debug|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Debug|x64.Build.0 = Debug|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Debug|x86.ActiveCfg = Debug|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Debug|x86.Build.0 = Debug|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Release|Any CPU.Build.0 = Release|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Release|arm64.ActiveCfg = Release|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Release|arm64.Build.0 = Release|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Release|x64.ActiveCfg = Release|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Release|x64.Build.0 = Release|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Release|x86.ActiveCfg = Release|Any CPU - {BE5D6903-34B9-4C29-85A2-811A7EA06DAF}.Release|x86.Build.0 = Release|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Debug|arm64.ActiveCfg = Debug|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Debug|arm64.Build.0 = Debug|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Debug|x64.ActiveCfg = Debug|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Debug|x64.Build.0 = Debug|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Debug|x86.ActiveCfg = Debug|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Debug|x86.Build.0 = Debug|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Release|Any CPU.Build.0 = Release|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Release|arm64.ActiveCfg = Release|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Release|arm64.Build.0 = Release|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Release|x64.ActiveCfg = Release|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Release|x64.Build.0 = Release|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Release|x86.ActiveCfg = Release|Any CPU - {F3F89B56-66A9-4EBC-8658-80785827237E}.Release|x86.Build.0 = Release|Any CPU {B81C7FA1-870F-4F21-A928-A5BE18754E6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B81C7FA1-870F-4F21-A928-A5BE18754E6E}.Debug|Any CPU.Build.0 = Debug|Any CPU {B81C7FA1-870F-4F21-A928-A5BE18754E6E}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -9966,6 +9942,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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -10786,6 +10810,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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 1350e70c8e9c..ba37439269da 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -89,6 +89,7 @@ + diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index 04869e61b7df..856cb23bf1d2 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -77,6 +77,7 @@ + diff --git a/src/Middleware/Middleware.slnf b/src/Middleware/Middleware.slnf index ea55360ff484..b7e466571e53 100644 --- a/src/Middleware/Middleware.slnf +++ b/src/Middleware/Middleware.slnf @@ -76,6 +76,9 @@ "src\\Middleware\\MiddlewareAnalysis\\samples\\MiddlewareAnalysisSample\\MiddlewareAnalysisSample.csproj", "src\\Middleware\\MiddlewareAnalysis\\src\\Microsoft.AspNetCore.MiddlewareAnalysis.csproj", "src\\Middleware\\MiddlewareAnalysis\\test\\Microsoft.AspNetCore.MiddlewareAnalysis.Tests.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", @@ -115,4 +118,4 @@ "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj" ] } -} \ No newline at end of file +} diff --git a/src/Middleware/RequestDecompression/sample/CustomDecompressionProvider.cs b/src/Middleware/RequestDecompression/sample/CustomDecompressionProvider.cs new file mode 100644 index 000000000000..2a306e0d9f1c --- /dev/null +++ b/src/Middleware/RequestDecompression/sample/CustomDecompressionProvider.cs @@ -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. + +using Microsoft.AspNetCore.RequestDecompression; + +namespace RequestDecompressionSample; + +public class CustomDecompressionProvider : IDecompressionProvider +{ + public string EncodingName => "custom"; + + public Stream CreateStream(Stream outputStream) + { + // Create a custom decompression stream wrapper here. + return outputStream; + } +} diff --git a/src/Middleware/RequestDecompression/sample/Properties/launchsettings.json b/src/Middleware/RequestDecompression/sample/Properties/launchsettings.json new file mode 100644 index 000000000000..57934a027a5f --- /dev/null +++ b/src/Middleware/RequestDecompression/sample/Properties/launchsettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:6164/", + "sslPort": 0 + } + }, + "profiles": { + "ResponseCompressionSample": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:5000/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } \ No newline at end of file diff --git a/src/Middleware/RequestDecompression/sample/RequestDecompressionSample.csproj b/src/Middleware/RequestDecompression/sample/RequestDecompressionSample.csproj new file mode 100644 index 000000000000..fec9ada30879 --- /dev/null +++ b/src/Middleware/RequestDecompression/sample/RequestDecompressionSample.csproj @@ -0,0 +1,13 @@ + + + + $(DefaultNetCoreTargetFramework) + + + + + + + + + \ No newline at end of file diff --git a/src/Middleware/RequestDecompression/sample/Startup.cs b/src/Middleware/RequestDecompression/sample/Startup.cs new file mode 100644 index 000000000000..aa32ba2dccb3 --- /dev/null +++ b/src/Middleware/RequestDecompression/sample/Startup.cs @@ -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; + +namespace RequestDecompressionSample; + +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddRequestDecompression(options => + { + options.Providers.Add(); + options.Providers.Add(); + }); + } + + 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(); + }).Build(); + + return host.RunAsync(); + } +} diff --git a/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs new file mode 100644 index 000000000000..79a916762bc0 --- /dev/null +++ b/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs @@ -0,0 +1,21 @@ +// 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; + +/// +/// Brotli decompression provider. +/// +public class BrotliDecompressionProvider : IDecompressionProvider +{ + /// + public string EncodingName => "br"; + + /// + public Stream CreateStream(Stream outputStream) + { + return new BrotliStream(outputStream, CompressionMode.Decompress); + } +} diff --git a/src/Middleware/RequestDecompression/src/DecompressionProviderCollection.cs b/src/Middleware/RequestDecompression/src/DecompressionProviderCollection.cs new file mode 100644 index 000000000000..9dc5971f887f --- /dev/null +++ b/src/Middleware/RequestDecompression/src/DecompressionProviderCollection.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.ObjectModel; + +namespace Microsoft.AspNetCore.RequestDecompression; + +/// +/// A collection of 's that also allows them to instantiated from an . +/// +public class DecompressionProviderCollection : Collection +{ + /// + /// Adds a type representing an . + /// + /// + /// Provider instances will be created using an . + /// + public void Add() where TDecompressionProvider : IDecompressionProvider + { + Add(typeof(TDecompressionProvider)); + } + + /// + /// Adds a type representing a . + /// + /// Type representing an . + /// + /// Provider instance will be created using an . + /// + public void Add(Type providerType) + { + if (providerType == null) + { + throw new ArgumentNullException(nameof(providerType)); + } + + if (!typeof(IDecompressionProvider).IsAssignableFrom(providerType)) + { + throw new ArgumentException($"The provider must implement {nameof(IDecompressionProvider)}.", nameof(providerType)); + } + + var factory = new DecompressionProviderFactory(providerType); + Add(factory); + } +} diff --git a/src/Middleware/RequestDecompression/src/DecompressionProviderFactory.cs b/src/Middleware/RequestDecompression/src/DecompressionProviderFactory.cs new file mode 100644 index 000000000000..6b5a93efd039 --- /dev/null +++ b/src/Middleware/RequestDecompression/src/DecompressionProviderFactory.cs @@ -0,0 +1,40 @@ +// 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; + +/// +/// This is a placeholder for the that allows +/// the creation of the given type via an . +/// +internal class DecompressionProviderFactory : IDecompressionProvider +{ + public DecompressionProviderFactory(Type providerType) + { + ProviderType = providerType; + } + + private Type ProviderType { get; } + + public IDecompressionProvider CreateInstance(IServiceProvider serviceProvider) + { + if (serviceProvider == null) + { + throw new ArgumentNullException(nameof(serviceProvider)); + } + + return (IDecompressionProvider)ActivatorUtilities.CreateInstance(serviceProvider, ProviderType, Type.EmptyTypes); + } + + string IDecompressionProvider.EncodingName + { + get { throw new NotSupportedException(); } + } + + Stream IDecompressionProvider.CreateStream(Stream outputStream) + { + throw new NotSupportedException(); + } +} diff --git a/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs new file mode 100644 index 000000000000..dc39bad2cbf7 --- /dev/null +++ b/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs @@ -0,0 +1,21 @@ +// 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; + +/// +/// DEFLATE decompression provider. +/// +public class DeflateDecompressionProvider : IDecompressionProvider +{ + /// + public string EncodingName => "deflate"; + + /// + public Stream CreateStream(Stream outputStream) + { + return new DeflateStream(outputStream, CompressionMode.Decompress); + } +} diff --git a/src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs new file mode 100644 index 000000000000..c50d2aed2578 --- /dev/null +++ b/src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs @@ -0,0 +1,21 @@ +// 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; + +/// +/// GZip decompression provider. +/// +public class GzipDecompressionProvider : IDecompressionProvider +{ + /// + public string EncodingName => "gzip"; + + /// + public Stream CreateStream(Stream outputStream) + { + return new GZipStream(outputStream, CompressionMode.Decompress); + } +} diff --git a/src/Middleware/RequestDecompression/src/IDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/IDecompressionProvider.cs new file mode 100644 index 000000000000..474a02287cfb --- /dev/null +++ b/src/Middleware/RequestDecompression/src/IDecompressionProvider.cs @@ -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. + +namespace Microsoft.AspNetCore.RequestDecompression; + +/// +/// Provides a specific decompression implementation to decompress HTTP requests. +/// +public interface IDecompressionProvider +{ + /// + /// The encoding name used in the 'Content-Encoding' request header. + /// + string EncodingName { get; } + + /// + /// Creates a new decompression stream. + /// + /// The stream where the decompressed data will be written. + /// The decompression stream. + Stream CreateStream(Stream outputStream); +} diff --git a/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs new file mode 100644 index 000000000000..e7f9a24931d6 --- /dev/null +++ b/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs @@ -0,0 +1,33 @@ +// 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; + +/// +/// Used to examine requests to see if decompression should be used. +/// +public interface IRequestDecompressionProvider +{ + /// + /// Examines the request and selects an acceptable decompression provider, if any. + /// + /// The . + /// A decompression provider or null if there are no acceptable providers. + IDecompressionProvider? GetDecompressionProvider(HttpContext context); + + /// + /// Examines the request to see if it should be decompressed. + /// + /// The . + /// if the request should be decompressed, otherwise . + bool ShouldDecompressRequest(HttpContext context); + + /// + /// Examines the request to see if decompression is supported for the specified Content-Type. + /// + /// The . + /// if the Content-Encoding is supported, otherwise . + bool IsContentEncodingSupported(HttpContext context); +} diff --git a/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj new file mode 100644 index 000000000000..88d09fc07d77 --- /dev/null +++ b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj @@ -0,0 +1,24 @@ + + + + ASP.NET Core middleware for HTTP Request decompression. + $(DefaultNetCoreTargetFramework) + true + true + aspnetcore + false + enable + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Middleware/RequestDecompression/src/Properties/AssemblyInfo.cs b/src/Middleware/RequestDecompression/src/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..180f70092df5 --- /dev/null +++ b/src/Middleware/RequestDecompression/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.RequestDecompression.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Middleware/RequestDecompression/src/PublicAPI.Shipped.txt b/src/Middleware/RequestDecompression/src/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..ab058de62d44 --- /dev/null +++ b/src/Middleware/RequestDecompression/src/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..7f1dd869e8f8 --- /dev/null +++ b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt @@ -0,0 +1,41 @@ +Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider +Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider.BrotliDecompressionProvider() -> void +Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! +Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider.EncodingName.get -> string! +Microsoft.AspNetCore.RequestDecompression.DecompressionProviderCollection +Microsoft.AspNetCore.RequestDecompression.DecompressionProviderCollection.Add(System.Type! providerType) -> void +Microsoft.AspNetCore.RequestDecompression.DecompressionProviderCollection.Add() -> void +Microsoft.AspNetCore.RequestDecompression.DecompressionProviderCollection.DecompressionProviderCollection() -> void +Microsoft.AspNetCore.RequestDecompression.DeflateDecompressionProvider +Microsoft.AspNetCore.RequestDecompression.DeflateDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! +Microsoft.AspNetCore.RequestDecompression.DeflateDecompressionProvider.DeflateDecompressionProvider() -> void +Microsoft.AspNetCore.RequestDecompression.DeflateDecompressionProvider.EncodingName.get -> string! +Microsoft.AspNetCore.RequestDecompression.GzipDecompressionProvider +Microsoft.AspNetCore.RequestDecompression.GzipDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! +Microsoft.AspNetCore.RequestDecompression.GzipDecompressionProvider.EncodingName.get -> string! +Microsoft.AspNetCore.RequestDecompression.GzipDecompressionProvider.GzipDecompressionProvider() -> void +Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider +Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! +Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider.EncodingName.get -> string! +Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider +Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.GetDecompressionProvider(Microsoft.AspNetCore.Http.HttpContext! context) -> Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider? +Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.IsContentEncodingSupported(Microsoft.AspNetCore.Http.HttpContext! context) -> bool +Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.ShouldDecompressRequest(Microsoft.AspNetCore.Http.HttpContext! context) -> bool +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionBuilderExtensions +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware.RequestDecompressionMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider! provider) -> void +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.Providers.get -> Microsoft.AspNetCore.RequestDecompression.DecompressionProviderCollection! +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.RequestDecompressionOptions() -> void +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.ReturnUnsupportedMediaType.get -> bool +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionServiceExtensions +static Microsoft.AspNetCore.RequestDecompression.RequestDecompressionBuilderExtensions.UseRequestDecompression(this Microsoft.AspNetCore.Builder.IApplicationBuilder! builder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.AspNetCore.RequestDecompression.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.AspNetCore.RequestDecompression.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.GetAcceptEncodingHeader() -> Microsoft.Extensions.Primitives.StringValues +virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.GetDecompressionProvider(Microsoft.AspNetCore.Http.HttpContext! context) -> Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider? +virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.IsContentEncodingSupported(Microsoft.AspNetCore.Http.HttpContext! context) -> bool +virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.ShouldDecompressRequest(Microsoft.AspNetCore.Http.HttpContext! context) -> bool +~Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.RequestDecompressionProvider(System.IServiceProvider! services, Microsoft.Extensions.Options.IOptions! options) -> void \ No newline at end of file diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs new file mode 100644 index 000000000000..12418b8ee70d --- /dev/null +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs @@ -0,0 +1,152 @@ +// 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; + +/// +/// Stream wrapper that creates a specific decompression stream if necessary. +/// +internal class RequestDecompressionBody : Stream +{ + private readonly HttpContext _context; + private readonly IRequestDecompressionProvider _provider; + private readonly Stream _innerStream; + + private IDecompressionProvider? _decompressionProvider; + private bool _decompressionChecked; + private Stream? _decompressionStream; + private bool _providerCreated; + private bool _complete; + + internal RequestDecompressionBody(HttpContext context, IRequestDecompressionProvider provider) + { + _context = context; + _provider = provider; + _innerStream = context.Request.Body; + } + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => _innerStream.CanWrite; + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public Stream Stream => this; + + public override int Read(byte[] buffer, int offset, int count) + { + OnRead(); + + if (_decompressionStream != null) + { + return _decompressionStream.Read(buffer, offset, count); + } + + return _innerStream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => TaskToApm.Begin(ReadAsync(buffer, offset, count, CancellationToken.None), callback, state); + + public override int EndRead(IAsyncResult asyncResult) + => TaskToApm.End(asyncResult); + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken); + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + OnRead(); + + if (_decompressionStream != null) + { + return await _decompressionStream.ReadAsync(buffer, cancellationToken); + } + + return await _innerStream.ReadAsync(buffer, cancellationToken); + } + + private void OnRead() + { + if (!_decompressionChecked) + { + _decompressionChecked = true; + + var decompressionProvider = ResolveDecompressionProvider(); + + if (decompressionProvider != null) + { + _decompressionStream = decompressionProvider.CreateStream(_innerStream); + } + } + } + + private IDecompressionProvider? ResolveDecompressionProvider() + { + if (!_providerCreated) + { + _providerCreated = true; + _decompressionProvider = _provider.GetDecompressionProvider(_context); + } + + return _decompressionProvider; + } + + public async Task CompleteAsync() + { + if (_complete) + { + return; + } + + await FinishDecompressionAsync(); + } + + internal async Task FinishDecompressionAsync() + { + if (_complete) + { + return; + } + + _complete = true; + + if (_decompressionStream != null) + { + await _decompressionStream.DisposeAsync(); + } + } +} diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs new file mode 100644 index 000000000000..948d4e3c7c3b --- /dev/null +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs @@ -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.Builder; + +namespace Microsoft.AspNetCore.RequestDecompression; + +/// +/// Extension methods for the request decompression middleware. +/// +public static class RequestDecompressionBuilderExtensions +{ + /// + /// Adds middleware for dynamically decompressing HTTP requests. + /// + /// The instance this method extends. + public static IApplicationBuilder UseRequestDecompression(this IApplicationBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.UseMiddleware(); + } +} diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs new file mode 100644 index 000000000000..5707dde382b4 --- /dev/null +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs @@ -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. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.RequestDecompression; + +internal static partial class RequestDecompressionLoggingExtensions +{ + [LoggerMessage(1, LogLevel.Trace, "The Content-Encoding header is missing or empty. Skipping request decompression.", EventName = "NoContentEncoding")] + public static partial void NoContentEncoding(this ILogger logger); + + [LoggerMessage(2, LogLevel.Trace, "The Content-Encoding header is specified. Proceeding with request decompression.", EventName = "ContentEncodingSpecified")] + public static partial void ContentEncodingSpecified(this ILogger logger); + + [LoggerMessage(3, LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings.", EventName = "MultipleContentEncodingsSpecified")] + public static partial void MultipleContentEncodingsSpecified(this ILogger logger); + + [LoggerMessage(4, LogLevel.Trace, "Request decompression is supported for Content-Encoding '{encoding}'.", EventName = "ContentEncodingSupported")] + public static partial void ContentEncodingSupported(this ILogger logger, string? encoding); + + [LoggerMessage(5, LogLevel.Debug, "Request decompression is not supported for Content-Encoding '{encoding}'.", EventName = "ContentEncodingUnsupported")] + public static partial void ContentEncodingUnsupported(this ILogger logger, string? encoding); + + [LoggerMessage(6, LogLevel.Debug, "No matching request decompression provider found.", EventName = "NoDecompressionProvider")] + public static partial void NoDecompressionProvider(this ILogger logger); + + [LoggerMessage(7, LogLevel.Debug, "The request will be decompressed with '{provider}'.", EventName = "DecompressingWith")] + public static partial void DecompressingWith(this ILogger logger, string provider); +} diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs new file mode 100644 index 000000000000..bda0eb16f9b8 --- /dev/null +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -0,0 +1,69 @@ +// 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; + +/// +/// Enables HTTP request decompression. +/// +public class RequestDecompressionMiddleware +{ + private readonly RequestDelegate _next; + private readonly IRequestDecompressionProvider _provider; + + /// + /// Initialize the request decompression middleware. + /// + /// The delegate representing the remaining middleware in the request pipeline. + /// The . + public RequestDecompressionMiddleware( + RequestDelegate next, + IRequestDecompressionProvider provider) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (provider == null) + { + throw new ArgumentNullException(nameof(provider)); + } + + _next = next; + _provider = provider; + } + + /// + /// Invoke the middleware. + /// + /// The . + /// A task that represents the execution of this middleware. + public Task Invoke(HttpContext context) + { + if (!_provider.ShouldDecompressRequest(context)) + { + return _next(context); + } + + if (!_provider.IsContentEncodingSupported(context)) + { + context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; + + return Task.CompletedTask; + } + + return InvokeCore(context); + } + + private async Task InvokeCore(HttpContext context) + { + var decompressionBody = new RequestDecompressionBody(context, _provider); + context.Request.Body = decompressionBody; + + await _next(context); + await decompressionBody.FinishDecompressionAsync(); + } +} diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs new file mode 100644 index 000000000000..2512f41f2cdc --- /dev/null +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs @@ -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.RequestDecompression; + +/// +/// Options for the HTTP request decompression middleware. +/// +public class RequestDecompressionOptions +{ + /// + /// The types to use for request decompression. + /// + public DecompressionProviderCollection Providers { get; } = new DecompressionProviderCollection(); +} diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs new file mode 100644 index 000000000000..55cfed8985d0 --- /dev/null +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.RequestDecompression; + +/// +public class RequestDecompressionProvider : IRequestDecompressionProvider +{ + private readonly IDecompressionProvider[] _providers; + private readonly ILogger _logger; + + /// + /// If no decompression providers are specified then all default providers will be registered. + /// + /// Services to use when instantiating decompression providers. + /// The options for this instance. + public RequestDecompressionProvider(IServiceProvider services, IOptions options) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var requestDecompressionOptions = options.Value; + + _providers = requestDecompressionOptions.Providers.ToArray(); + if (_providers.Length == 0) + { + _providers = new IDecompressionProvider[] + { + new DecompressionProviderFactory(typeof(BrotliDecompressionProvider)), + new DecompressionProviderFactory(typeof(DeflateDecompressionProvider)), + new DecompressionProviderFactory(typeof(GzipDecompressionProvider)) + }; + } + + for (var i = 0; i < _providers.Length; i++) + { + var factory = _providers[i] as DecompressionProviderFactory; + if (factory != null) + { + _providers[i] = factory.CreateInstance(services); + } + } + + _logger = services.GetRequiredService>(); + } + + /// + public virtual IDecompressionProvider? GetDecompressionProvider(HttpContext context) + { + // e.g. Content-Encoding: br, deflate, gzip + var encodings = context.Request.Headers.ContentEncoding; + + if (StringValues.IsNullOrEmpty(encodings)) + { + Debug.Assert(false, "Duplicate check failed."); + _logger.NoContentEncoding(); + return null; + } + + if (encodings.Count > 1) + { + Debug.Assert(false, "Duplicate check failed."); + _logger.MultipleContentEncodingsSpecified(); + return null; + } + + var encodingName = encodings.Single(); + + var selectedProvider = + _providers.FirstOrDefault(x => + StringSegment.Equals(x.EncodingName, encodingName, StringComparison.OrdinalIgnoreCase)); + + if (selectedProvider == null) + { + Debug.Assert(false, "Duplicate check failed."); + _logger.NoDecompressionProvider(); + return null; + } + + _logger.DecompressingWith(selectedProvider.EncodingName); + return selectedProvider; + } + + /// + public virtual bool ShouldDecompressRequest(HttpContext context) + { + var encodings = context.Request.Headers.ContentEncoding; + + if (StringValues.IsNullOrEmpty(encodings)) + { + _logger.NoContentEncoding(); + return false; + } + + _logger.ContentEncodingSpecified(); + return true; + } + + /// + public virtual bool IsContentEncodingSupported(HttpContext context) + { + var encodings = context.Request.Headers.ContentEncoding; + + if (StringValues.IsNullOrEmpty(encodings)) + { + Debug.Assert(false, "Duplicate check failed."); + _logger.NoContentEncoding(); + return false; + } + + if (encodings.Count > 1) + { + _logger.MultipleContentEncodingsSpecified(); + return false; + } + + var encoding = encodings.Single(); + var supportedEncodings = _providers.Select(x => x.EncodingName); + + if (supportedEncodings.Any(x => string.Equals(x, encoding, StringComparison.OrdinalIgnoreCase))) + { + _logger.ContentEncodingSupported(encoding); + return true; + } + + _logger.ContentEncodingUnsupported(encoding); + return false; + } +} diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs new file mode 100644 index 000000000000..812ec8bfd53c --- /dev/null +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs @@ -0,0 +1,51 @@ +// 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; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.RequestDecompression; + +/// +/// Extension methods for the RequestDecompression middleware. +/// +public static class RequestDecompressionServiceExtensions +{ + /// + /// Add request decompression services. + /// + /// The for adding services. + /// The . + public static IServiceCollection AddRequestDecompression(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAddSingleton(); + return services; + } + + /// + /// Add request decompression services and configure the related options. + /// + /// The for adding services. + /// A delegate to configure the . + /// The . + public static IServiceCollection AddRequestDecompression(this IServiceCollection services, Action configureOptions) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + services.Configure(configureOptions); + services.TryAddSingleton(); + return services; + } +} diff --git a/src/Middleware/RequestDecompression/test/Microsoft.AspNetCore.RequestDecompression.Tests.csproj b/src/Middleware/RequestDecompression/test/Microsoft.AspNetCore.RequestDecompression.Tests.csproj new file mode 100644 index 000000000000..21eb7830cc38 --- /dev/null +++ b/src/Middleware/RequestDecompression/test/Microsoft.AspNetCore.RequestDecompression.Tests.csproj @@ -0,0 +1,14 @@ + + + + $(DefaultNetCoreTargetFramework) + + + + + + + + + + \ No newline at end of file diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs new file mode 100644 index 000000000000..469f04dc5955 --- /dev/null +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs @@ -0,0 +1,254 @@ +// 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; +using System.Net; +using System.Net.Http; +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +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 GetCompressedContent( + Func compressorDelegate, + string input = TestRequestBodyData) + { + var bytes = GetUncompressedContent(input); + await using var uncompressedContent = new MemoryStream(bytes); + + await using var compressedContent = new MemoryStream(); + await using (var compressor = compressorDelegate(compressedContent)) + { + uncompressedContent.CopyTo(compressor); + } + + return compressedContent.ToArray(); + } + + private static async Task GetBrotliCompressedContent(string input = TestRequestBodyData) + { + static Stream compressorDelegate(Stream compressedContent) => + new BrotliStream(compressedContent, CompressionMode.Compress); + + return await GetCompressedContent(compressorDelegate, input); + } + + private static async Task GetDeflateCompressedContent(string input = TestRequestBodyData) + { + static Stream compressorDelegate(Stream compressedContent) => + new DeflateStream(compressedContent, CompressionMode.Compress); + + return await GetCompressedContent(compressorDelegate, input); + } + + private static async Task GetGZipCompressedContent(string input = TestRequestBodyData) + { + static Stream compressorDelegate(Stream compressedContent) => + new GZipStream(compressedContent, CompressionMode.Compress); + + return await GetCompressedContent(compressorDelegate, input); + } + + [Fact] + public async Task Request_NoContentEncoding_NotDecompressed() + { + var uncompressedContent = GetUncompressedContent(); + + var (logMessages, outputContent) = await InvokeMiddleware(uncompressedContent); + + AssertLog(logMessages.Single(), LogLevel.Trace, "The Content-Encoding header is missing or empty. Skipping request decompression."); + Assert.Equal(uncompressedContent, outputContent); + } + + [Fact] + public async Task Request_ContentEncodingBrotli_Decompressed() + { + var compressedContent = await GetBrotliCompressedContent(); + var contentEncoding = "br"; + + var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, contentEncoding); + + AssertDecompressedWithLog(logMessages, contentEncoding); + Assert.Equal(GetUncompressedContent(), decompressedContent); + } + + [Fact] + public async Task Request_ContentEncodingDeflate_Decompressed() + { + var compressedContent = await GetDeflateCompressedContent(); + var contentEncoding = "deflate"; + + var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, contentEncoding); + + AssertDecompressedWithLog(logMessages, contentEncoding); + Assert.Equal(GetUncompressedContent(), decompressedContent); + } + + [Fact] + public async Task Request_ContentEncodingGzip_Decompressed() + { + var compressedContent = await GetGZipCompressedContent(); + var contentEncoding = "gzip"; + + var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, contentEncoding); + + AssertDecompressedWithLog(logMessages, contentEncoding); + Assert.Equal(GetUncompressedContent(), decompressedContent); + } + + [Fact] + public async Task Request_UnsupportedContentEncoding_Returns415UnsupportedMediaType() + { + var contentEncoding = "custom"; + + var (logMessages, response) = await InvokeMiddleware(contentEncoding); + + Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode); + AssertLog(logMessages.First(), LogLevel.Trace, "The Content-Encoding header is specified. Proceeding with request decompression."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, $"Request decompression is not supported for Content-Encoding '{contentEncoding}'."); + } + + [Fact] + public async Task Request_MultipleContentEncodings_Returns415UnsupportedMediaType() + { + var contentEncodings = new[] { "br", "gzip" }; + + var (logMessages, response) = await InvokeMiddleware(contentEncodings); + + Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode); + AssertLog(logMessages.First(), LogLevel.Trace, "The Content-Encoding header is specified. Proceeding with request decompression."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings."); + } + + private static async Task<(List, byte[])> InvokeMiddleware( + byte[] compressedContent, + string contentEncoding = null, + Action configure = null) + { + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var decompressedContent = Array.Empty(); + + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .ConfigureServices(services => + { + services.AddRequestDecompression(configure ?? (_ => { })); + services.AddSingleton(loggerFactory); + }) + .Configure(app => + { + app.UseRequestDecompression(); + app.Run(async context => + { + await using var ms = new MemoryStream(); + await context.Request.Body.CopyToAsync(ms, context.RequestAborted); + decompressedContent = 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 (contentEncoding != null) + { + request.Content.Headers.ContentEncoding.Add(contentEncoding); + } + + await client.SendAsync(request); + + return (sink.Writes.ToList(), decompressedContent); + } + + private static async Task<(List, HttpResponseMessage)> InvokeMiddleware( + string contentEncoding, + Action configure = null) + { + return await InvokeMiddleware(new[] { contentEncoding }, configure); + } + + private static async Task<(List, HttpResponseMessage)> InvokeMiddleware( + string[] contentEncodings, + Action configure = null) + { + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .ConfigureServices(services => + { + services.AddRequestDecompression(configure ?? (_ => { })); + services.AddSingleton(loggerFactory); + }) + .Configure(app => + { + app.UseRequestDecompression(); + app.Run(async context => await Task.CompletedTask); + }); + }).Build(); + + await host.StartAsync(); + + var server = host.GetTestServer(); + var client = server.CreateClient(); + + using var request = new HttpRequestMessage(HttpMethod.Post, ""); + request.Content = new ByteArrayContent(new byte[10]); + + foreach (var encoding in contentEncodings) + { + request.Content.Headers.ContentEncoding.Add(encoding); + } + + var response = await client.SendAsync(request); + + return (sink.Writes.ToList(), response); + } + + 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 logMessages, string encoding) + { + Assert.Equal(3, logMessages.Count); + AssertLog(logMessages.First(), LogLevel.Trace, "The Content-Encoding header is specified. Proceeding with request decompression."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Trace, $"Request decompression is supported for Content-Encoding '{encoding}'."); + AssertLog(logMessages.Skip(2).First(), LogLevel.Debug, $"The request will be decompressed with '{encoding}'."); + } +} From d888b86596b7c961e8c7e7482ebedf3ae2b3a9b6 Mon Sep 17 00:00:00 2001 From: David Acker Date: Wed, 16 Feb 2022 22:02:09 -0500 Subject: [PATCH 02/42] Fix API baselines --- .../RequestDecompression/src/PublicAPI.Unshipped.txt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt index 7f1dd869e8f8..8a7b52705cb8 100644 --- a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ -Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider +Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider.BrotliDecompressionProvider() -> void Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider.EncodingName.get -> string! @@ -28,14 +28,12 @@ Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware.Request Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.Providers.get -> Microsoft.AspNetCore.RequestDecompression.DecompressionProviderCollection! Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.RequestDecompressionOptions() -> void -Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.ReturnUnsupportedMediaType.get -> bool Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider Microsoft.AspNetCore.RequestDecompression.RequestDecompressionServiceExtensions static Microsoft.AspNetCore.RequestDecompression.RequestDecompressionBuilderExtensions.UseRequestDecompression(this Microsoft.AspNetCore.Builder.IApplicationBuilder! builder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! static Microsoft.AspNetCore.RequestDecompression.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.RequestDecompression.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.GetAcceptEncodingHeader() -> Microsoft.Extensions.Primitives.StringValues virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.GetDecompressionProvider(Microsoft.AspNetCore.Http.HttpContext! context) -> Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider? virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.IsContentEncodingSupported(Microsoft.AspNetCore.Http.HttpContext! context) -> bool virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.ShouldDecompressRequest(Microsoft.AspNetCore.Http.HttpContext! context) -> bool -~Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.RequestDecompressionProvider(System.IServiceProvider! services, Microsoft.Extensions.Options.IOptions! options) -> void \ No newline at end of file +~Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.RequestDecompressionProvider(System.IServiceProvider! services, Microsoft.Extensions.Options.IOptions! options) -> void From ca2b67517358cabb73c1fbf94bfc8c6622d7d490 Mon Sep 17 00:00:00 2001 From: David Acker Date: Thu, 17 Feb 2022 19:18:04 -0500 Subject: [PATCH 03/42] Minor fixes --- .../sample/Properties/launchsettings.json | 4 ++-- .../RequestDecompression/src/PublicAPI.Unshipped.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Middleware/RequestDecompression/sample/Properties/launchsettings.json b/src/Middleware/RequestDecompression/sample/Properties/launchsettings.json index 57934a027a5f..26a370a87fb6 100644 --- a/src/Middleware/RequestDecompression/sample/Properties/launchsettings.json +++ b/src/Middleware/RequestDecompression/sample/Properties/launchsettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "ResponseCompressionSample": { + "RequestDecompressionSample": { "commandName": "Project", "launchBrowser": true, "launchUrl": "http://localhost:5000/", @@ -24,4 +24,4 @@ } } } - } \ No newline at end of file + } diff --git a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt index 8a7b52705cb8..57c93fcd544d 100644 --- a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +#nullable enable Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider.BrotliDecompressionProvider() -> void Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! From d3d074a9a4944eb4c09aae509d0ff12ee53f7b09 Mon Sep 17 00:00:00 2001 From: David Acker Date: Thu, 17 Feb 2022 19:19:37 -0500 Subject: [PATCH 04/42] Eagerly resolve decompression provider and stream --- .../src/RequestDecompressionBody.cs | 41 +++---------------- 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs index 12418b8ee70d..2f6faded7c2c 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs @@ -10,21 +10,20 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// internal class RequestDecompressionBody : Stream { - private readonly HttpContext _context; private readonly IRequestDecompressionProvider _provider; private readonly Stream _innerStream; - private IDecompressionProvider? _decompressionProvider; - private bool _decompressionChecked; - private Stream? _decompressionStream; - private bool _providerCreated; + private readonly IDecompressionProvider? _decompressionProvider; + private readonly Stream? _decompressionStream; private bool _complete; internal RequestDecompressionBody(HttpContext context, IRequestDecompressionProvider provider) { - _context = context; _provider = provider; _innerStream = context.Request.Body; + + _decompressionProvider = _provider.GetDecompressionProvider(context); + _decompressionStream = _decompressionProvider?.CreateStream(_innerStream); } public override bool CanRead => true; @@ -48,8 +47,6 @@ public override long Position public override int Read(byte[] buffer, int offset, int count) { - OnRead(); - if (_decompressionStream != null) { return _decompressionStream.Read(buffer, offset, count); @@ -89,8 +86,6 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { - OnRead(); - if (_decompressionStream != null) { return await _decompressionStream.ReadAsync(buffer, cancellationToken); @@ -99,32 +94,6 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation return await _innerStream.ReadAsync(buffer, cancellationToken); } - private void OnRead() - { - if (!_decompressionChecked) - { - _decompressionChecked = true; - - var decompressionProvider = ResolveDecompressionProvider(); - - if (decompressionProvider != null) - { - _decompressionStream = decompressionProvider.CreateStream(_innerStream); - } - } - } - - private IDecompressionProvider? ResolveDecompressionProvider() - { - if (!_providerCreated) - { - _providerCreated = true; - _decompressionProvider = _provider.GetDecompressionProvider(_context); - } - - return _decompressionProvider; - } - public async Task CompleteAsync() { if (_complete) From 4a4e90247dcfc6434d32748f578f7b15bc3dc622 Mon Sep 17 00:00:00 2001 From: David Acker Date: Thu, 17 Feb 2022 19:22:03 -0500 Subject: [PATCH 05/42] Pass requests to endpoint instead of returning 415 --- .../src/RequestDecompressionMiddleware.cs | 14 +-- .../RequestDecompressionMiddlewareTests.cs | 87 +++++-------------- 2 files changed, 24 insertions(+), 77 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index bda0eb16f9b8..c5e12f1517c6 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -43,19 +43,13 @@ public RequestDecompressionMiddleware( /// A task that represents the execution of this middleware. public Task Invoke(HttpContext context) { - if (!_provider.ShouldDecompressRequest(context)) + if (_provider.ShouldDecompressRequest(context) + && _provider.IsContentEncodingSupported(context)) { - return _next(context); + return InvokeCore(context); } - if (!_provider.IsContentEncodingSupported(context)) - { - context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; - - return Task.CompletedTask; - } - - return InvokeCore(context); + return _next(context); } private async Task InvokeCore(HttpContext context) diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs index 469f04dc5955..4b8db0698153 100644 --- a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO.Compression; -using System.Net; using System.Net.Http; using System.Text; using Microsoft.AspNetCore.Builder; @@ -81,7 +80,7 @@ public async Task Request_ContentEncodingBrotli_Decompressed() var compressedContent = await GetBrotliCompressedContent(); var contentEncoding = "br"; - var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, contentEncoding); + var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, new[] { contentEncoding }); AssertDecompressedWithLog(logMessages, contentEncoding); Assert.Equal(GetUncompressedContent(), decompressedContent); @@ -93,7 +92,7 @@ public async Task Request_ContentEncodingDeflate_Decompressed() var compressedContent = await GetDeflateCompressedContent(); var contentEncoding = "deflate"; - var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, contentEncoding); + var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, new[] { contentEncoding }); AssertDecompressedWithLog(logMessages, contentEncoding); Assert.Equal(GetUncompressedContent(), decompressedContent); @@ -105,39 +104,41 @@ public async Task Request_ContentEncodingGzip_Decompressed() var compressedContent = await GetGZipCompressedContent(); var contentEncoding = "gzip"; - var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, contentEncoding); + var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, new[] { contentEncoding }); AssertDecompressedWithLog(logMessages, contentEncoding); Assert.Equal(GetUncompressedContent(), decompressedContent); } [Fact] - public async Task Request_UnsupportedContentEncoding_Returns415UnsupportedMediaType() + public async Task Request_UnsupportedContentEncoding_NotDecompressed() { + var inputContent = GetUncompressedContent(); var contentEncoding = "custom"; - var (logMessages, response) = await InvokeMiddleware(contentEncoding); + var (logMessages, outputContent) = await InvokeMiddleware(inputContent, new[] { contentEncoding }); - Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode); AssertLog(logMessages.First(), LogLevel.Trace, "The Content-Encoding header is specified. Proceeding with request decompression."); AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, $"Request decompression is not supported for Content-Encoding '{contentEncoding}'."); + Assert.Equal(GetUncompressedContent(), outputContent); } [Fact] - public async Task Request_MultipleContentEncodings_Returns415UnsupportedMediaType() + public async Task Request_MultipleContentEncodings_NotDecompressed() { + var inputContent = GetUncompressedContent(); var contentEncodings = new[] { "br", "gzip" }; - var (logMessages, response) = await InvokeMiddleware(contentEncodings); + var (logMessages, outputContent) = await InvokeMiddleware(inputContent, contentEncodings); - Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode); AssertLog(logMessages.First(), LogLevel.Trace, "The Content-Encoding header is specified. Proceeding with request decompression."); AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings."); + Assert.Equal(GetUncompressedContent(), outputContent); } private static async Task<(List, byte[])> InvokeMiddleware( byte[] compressedContent, - string contentEncoding = null, + string[] contentEncodings = null, Action configure = null) { var sink = new TestSink( @@ -145,7 +146,7 @@ public async Task Request_MultipleContentEncodings_Returns415UnsupportedMediaTyp TestSink.EnableWithTypeName); var loggerFactory = new TestLoggerFactory(sink, enabled: true); - var decompressedContent = Array.Empty(); + var outputContent = Array.Empty(); using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => @@ -164,7 +165,7 @@ public async Task Request_MultipleContentEncodings_Returns415UnsupportedMediaTyp { await using var ms = new MemoryStream(); await context.Request.Body.CopyToAsync(ms, context.RequestAborted); - decompressedContent = ms.ToArray(); + outputContent = ms.ToArray(); }); }); }).Build(); @@ -177,65 +178,17 @@ public async Task Request_MultipleContentEncodings_Returns415UnsupportedMediaTyp using var request = new HttpRequestMessage(HttpMethod.Post, ""); request.Content = new ByteArrayContent(compressedContent); - if (contentEncoding != null) + if (contentEncodings != null) { - request.Content.Headers.ContentEncoding.Add(contentEncoding); - } - - await client.SendAsync(request); - - return (sink.Writes.ToList(), decompressedContent); - } - - private static async Task<(List, HttpResponseMessage)> InvokeMiddleware( - string contentEncoding, - Action configure = null) - { - return await InvokeMiddleware(new[] { contentEncoding }, configure); - } - - private static async Task<(List, HttpResponseMessage)> InvokeMiddleware( - string[] contentEncodings, - Action configure = null) - { - var sink = new TestSink( - TestSink.EnableWithTypeName, - TestSink.EnableWithTypeName); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); - - using var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => + foreach (var encoding in contentEncodings) { - webHostBuilder - .UseTestServer() - .ConfigureServices(services => - { - services.AddRequestDecompression(configure ?? (_ => { })); - services.AddSingleton(loggerFactory); - }) - .Configure(app => - { - app.UseRequestDecompression(); - app.Run(async context => await Task.CompletedTask); - }); - }).Build(); - - await host.StartAsync(); - - var server = host.GetTestServer(); - var client = server.CreateClient(); - - using var request = new HttpRequestMessage(HttpMethod.Post, ""); - request.Content = new ByteArrayContent(new byte[10]); - - foreach (var encoding in contentEncodings) - { - request.Content.Headers.ContentEncoding.Add(encoding); + request.Content.Headers.ContentEncoding.Add(encoding); + } } - var response = await client.SendAsync(request); + await client.SendAsync(request); - return (sink.Writes.ToList(), response); + return (sink.Writes.ToList(), outputContent); } private static void AssertLog(WriteContext log, LogLevel level, string message) From 8ad1352cc877df4c1a9e48fa64340090e57321c2 Mon Sep 17 00:00:00 2001 From: David Acker Date: Fri, 18 Feb 2022 18:46:38 -0500 Subject: [PATCH 06/42] Revert request body to original value --- .../src/RequestDecompressionMiddleware.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index c5e12f1517c6..de3daffa054f 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -54,10 +54,19 @@ public Task Invoke(HttpContext context) private async Task InvokeCore(HttpContext context) { + var originalBody = context.Request.Body; + var decompressionBody = new RequestDecompressionBody(context, _provider); context.Request.Body = decompressionBody; - await _next(context); - await decompressionBody.FinishDecompressionAsync(); + try + { + await _next(context); + await decompressionBody.FinishDecompressionAsync(); + } + finally + { + context.Request.Body = originalBody; + } } } From f8d2604266f3e5bd6547c8606054be12652615b5 Mon Sep 17 00:00:00 2001 From: David Acker Date: Fri, 18 Feb 2022 18:49:13 -0500 Subject: [PATCH 07/42] Seal decompression providers --- .../RequestDecompression/src/BrotliDecompressionProvider.cs | 2 +- .../RequestDecompression/src/DeflateDecompressionProvider.cs | 2 +- .../RequestDecompression/src/GzipDecompressionProvider.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs index 79a916762bc0..b7de1f9e59d8 100644 --- a/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// /// Brotli decompression provider. /// -public class BrotliDecompressionProvider : IDecompressionProvider +public sealed class BrotliDecompressionProvider : IDecompressionProvider { /// public string EncodingName => "br"; diff --git a/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs index dc39bad2cbf7..68b3179cab38 100644 --- a/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// /// DEFLATE decompression provider. /// -public class DeflateDecompressionProvider : IDecompressionProvider +public sealed class DeflateDecompressionProvider : IDecompressionProvider { /// public string EncodingName => "deflate"; diff --git a/src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs index c50d2aed2578..0e877a6b150f 100644 --- a/src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// /// GZip decompression provider. /// -public class GzipDecompressionProvider : IDecompressionProvider +public sealed class GzipDecompressionProvider : IDecompressionProvider { /// public string EncodingName => "gzip"; From 12c4519725e7f2c6e52942a25b580a6547afaa17 Mon Sep 17 00:00:00 2001 From: David Acker Date: Fri, 18 Feb 2022 19:08:58 -0500 Subject: [PATCH 08/42] Use constructor injection for logger --- .../src/PublicAPI.Unshipped.txt | 2 +- .../src/RequestDecompressionProvider.cs | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt index 57c93fcd544d..72646ac9bb21 100644 --- a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt @@ -37,4 +37,4 @@ static Microsoft.AspNetCore.RequestDecompression.RequestDecompressionServiceExte virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.GetDecompressionProvider(Microsoft.AspNetCore.Http.HttpContext! context) -> Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider? virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.IsContentEncodingSupported(Microsoft.AspNetCore.Http.HttpContext! context) -> bool virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.ShouldDecompressRequest(Microsoft.AspNetCore.Http.HttpContext! context) -> bool -~Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.RequestDecompressionProvider(System.IServiceProvider! services, Microsoft.Extensions.Options.IOptions! options) -> void +~Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.RequestDecompressionProvider(System.IServiceProvider! services, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Options.IOptions! options) -> void diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs index 55cfed8985d0..e432264fbe93 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -21,19 +20,30 @@ public class RequestDecompressionProvider : IRequestDecompressionProvider /// If no decompression providers are specified then all default providers will be registered. /// /// Services to use when instantiating decompression providers. + /// An instance of . /// The options for this instance. - public RequestDecompressionProvider(IServiceProvider services, IOptions options) + public RequestDecompressionProvider( + IServiceProvider services, + ILogger logger, + IOptions options) { if (services == null) { throw new ArgumentNullException(nameof(services)); } + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + if (options == null) { throw new ArgumentNullException(nameof(options)); } + _logger = logger; + var requestDecompressionOptions = options.Value; _providers = requestDecompressionOptions.Providers.ToArray(); @@ -55,8 +65,6 @@ public RequestDecompressionProvider(IServiceProvider services, IOptions>(); } /// From e000add87ad39cd671025c05bfdac2ee1489025a Mon Sep 17 00:00:00 2001 From: David Acker Date: Fri, 18 Feb 2022 19:13:04 -0500 Subject: [PATCH 09/42] Remove unused property --- .../RequestDecompression/src/RequestDecompressionBody.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs index 2f6faded7c2c..2bacd1520223 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs @@ -43,8 +43,6 @@ public override long Position set { throw new NotSupportedException(); } } - public Stream Stream => this; - public override int Read(byte[] buffer, int offset, int count) { if (_decompressionStream != null) From e2a996384bf33ab81dad7a93dc5d43f38d0b6b02 Mon Sep 17 00:00:00 2001 From: David Acker Date: Fri, 18 Feb 2022 19:23:00 -0500 Subject: [PATCH 10/42] Seal more classes --- .../src/DecompressionProviderFactory.cs | 2 +- .../RequestDecompression/src/RequestDecompressionBody.cs | 2 +- .../src/RequestDecompressionProvider.cs | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/DecompressionProviderFactory.cs b/src/Middleware/RequestDecompression/src/DecompressionProviderFactory.cs index 6b5a93efd039..84443f8582c6 100644 --- a/src/Middleware/RequestDecompression/src/DecompressionProviderFactory.cs +++ b/src/Middleware/RequestDecompression/src/DecompressionProviderFactory.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// This is a placeholder for the that allows /// the creation of the given type via an . /// -internal class DecompressionProviderFactory : IDecompressionProvider +internal sealed class DecompressionProviderFactory : IDecompressionProvider { public DecompressionProviderFactory(Type providerType) { diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs index 2bacd1520223..e91ebcb0a4b1 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// /// Stream wrapper that creates a specific decompression stream if necessary. /// -internal class RequestDecompressionBody : Stream +internal sealed class RequestDecompressionBody : Stream { private readonly IRequestDecompressionProvider _provider; private readonly Stream _innerStream; diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs index e432264fbe93..c9b0fdb31ce7 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// -public class RequestDecompressionProvider : IRequestDecompressionProvider +internal sealed class RequestDecompressionProvider : IRequestDecompressionProvider { private readonly IDecompressionProvider[] _providers; private readonly ILogger _logger; @@ -68,7 +68,7 @@ public RequestDecompressionProvider( } /// - public virtual IDecompressionProvider? GetDecompressionProvider(HttpContext context) + public IDecompressionProvider? GetDecompressionProvider(HttpContext context) { // e.g. Content-Encoding: br, deflate, gzip var encodings = context.Request.Headers.ContentEncoding; @@ -105,7 +105,7 @@ public RequestDecompressionProvider( } /// - public virtual bool ShouldDecompressRequest(HttpContext context) + public bool ShouldDecompressRequest(HttpContext context) { var encodings = context.Request.Headers.ContentEncoding; @@ -120,7 +120,7 @@ public virtual bool ShouldDecompressRequest(HttpContext context) } /// - public virtual bool IsContentEncodingSupported(HttpContext context) + public bool IsContentEncodingSupported(HttpContext context) { var encodings = context.Request.Headers.ContentEncoding; From 74622ddc3852e664d00629b4d317da9d3b949eb3 Mon Sep 17 00:00:00 2001 From: David Acker Date: Fri, 18 Feb 2022 19:51:10 -0500 Subject: [PATCH 11/42] Only create RequestDecompressionBody if a decompresison provider exists --- .../src/IRequestDecompressionProvider.cs | 14 ------ .../src/RequestDecompressionBody.cs | 37 +++----------- .../RequestDecompressionLoggingExtensions.cs | 15 ++---- .../src/RequestDecompressionMiddleware.cs | 14 +++--- .../src/RequestDecompressionProvider.cs | 50 ------------------- .../RequestDecompressionMiddlewareTests.cs | 12 ++--- 6 files changed, 22 insertions(+), 120 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs index e7f9a24931d6..c01f2bd0f5f8 100644 --- a/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs @@ -16,18 +16,4 @@ public interface IRequestDecompressionProvider /// The . /// A decompression provider or null if there are no acceptable providers. IDecompressionProvider? GetDecompressionProvider(HttpContext context); - - /// - /// Examines the request to see if it should be decompressed. - /// - /// The . - /// if the request should be decompressed, otherwise . - bool ShouldDecompressRequest(HttpContext context); - - /// - /// Examines the request to see if decompression is supported for the specified Content-Type. - /// - /// The . - /// if the Content-Encoding is supported, otherwise . - bool IsContentEncodingSupported(HttpContext context); } diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs index e91ebcb0a4b1..ea74d746b6d1 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs @@ -1,8 +1,6 @@ // 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; /// @@ -10,27 +8,19 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// internal sealed class RequestDecompressionBody : Stream { - private readonly IRequestDecompressionProvider _provider; - private readonly Stream _innerStream; - - private readonly IDecompressionProvider? _decompressionProvider; - private readonly Stream? _decompressionStream; + private readonly Stream _decompressionStream; private bool _complete; - internal RequestDecompressionBody(HttpContext context, IRequestDecompressionProvider provider) + internal RequestDecompressionBody(Stream decompressionStream) { - _provider = provider; - _innerStream = context.Request.Body; - - _decompressionProvider = _provider.GetDecompressionProvider(context); - _decompressionStream = _decompressionProvider?.CreateStream(_innerStream); + _decompressionStream = decompressionStream; } public override bool CanRead => true; public override bool CanSeek => false; - public override bool CanWrite => _innerStream.CanWrite; + public override bool CanWrite => _decompressionStream.CanWrite; public override long Length { @@ -45,12 +35,7 @@ public override long Position public override int Read(byte[] buffer, int offset, int count) { - if (_decompressionStream != null) - { - return _decompressionStream.Read(buffer, offset, count); - } - - return _innerStream.Read(buffer, offset, count); + return _decompressionStream.Read(buffer, offset, count); } public override long Seek(long offset, SeekOrigin origin) @@ -84,12 +69,7 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { - if (_decompressionStream != null) - { - return await _decompressionStream.ReadAsync(buffer, cancellationToken); - } - - return await _innerStream.ReadAsync(buffer, cancellationToken); + return await _decompressionStream.ReadAsync(buffer, cancellationToken); } public async Task CompleteAsync() @@ -111,9 +91,6 @@ internal async Task FinishDecompressionAsync() _complete = true; - if (_decompressionStream != null) - { - await _decompressionStream.DisposeAsync(); - } + await _decompressionStream.DisposeAsync(); } } diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs index 5707dde382b4..fabfa17eabc6 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs @@ -10,21 +10,12 @@ internal static partial class RequestDecompressionLoggingExtensions [LoggerMessage(1, LogLevel.Trace, "The Content-Encoding header is missing or empty. Skipping request decompression.", EventName = "NoContentEncoding")] public static partial void NoContentEncoding(this ILogger logger); - [LoggerMessage(2, LogLevel.Trace, "The Content-Encoding header is specified. Proceeding with request decompression.", EventName = "ContentEncodingSpecified")] - public static partial void ContentEncodingSpecified(this ILogger logger); - - [LoggerMessage(3, LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings.", EventName = "MultipleContentEncodingsSpecified")] + [LoggerMessage(2, LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings.", EventName = "MultipleContentEncodingsSpecified")] public static partial void MultipleContentEncodingsSpecified(this ILogger logger); - [LoggerMessage(4, LogLevel.Trace, "Request decompression is supported for Content-Encoding '{encoding}'.", EventName = "ContentEncodingSupported")] - public static partial void ContentEncodingSupported(this ILogger logger, string? encoding); - - [LoggerMessage(5, LogLevel.Debug, "Request decompression is not supported for Content-Encoding '{encoding}'.", EventName = "ContentEncodingUnsupported")] - public static partial void ContentEncodingUnsupported(this ILogger logger, string? encoding); - - [LoggerMessage(6, LogLevel.Debug, "No matching request decompression provider found.", EventName = "NoDecompressionProvider")] + [LoggerMessage(3, LogLevel.Debug, "No matching request decompression provider found.", EventName = "NoDecompressionProvider")] public static partial void NoDecompressionProvider(this ILogger logger); - [LoggerMessage(7, LogLevel.Debug, "The request will be decompressed with '{provider}'.", EventName = "DecompressingWith")] + [LoggerMessage(4, LogLevel.Debug, "The request will be decompressed with '{provider}'.", EventName = "DecompressingWith")] public static partial void DecompressingWith(this ILogger logger, string provider); } diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index de3daffa054f..557c8830d55f 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -43,20 +43,22 @@ public RequestDecompressionMiddleware( /// A task that represents the execution of this middleware. public Task Invoke(HttpContext context) { - if (_provider.ShouldDecompressRequest(context) - && _provider.IsContentEncodingSupported(context)) + var decompressionProvider = _provider.GetDecompressionProvider(context); + if (decompressionProvider == null) { - return InvokeCore(context); + return _next(context); } - return _next(context); + return InvokeCore(context, decompressionProvider); } - private async Task InvokeCore(HttpContext context) + private async Task InvokeCore(HttpContext context, IDecompressionProvider decompressionProvider) { var originalBody = context.Request.Body; - var decompressionBody = new RequestDecompressionBody(context, _provider); + var decompressionStream = decompressionProvider.CreateStream(originalBody); + var decompressionBody = new RequestDecompressionBody(decompressionStream); + context.Request.Body = decompressionBody; try diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs index c9b0fdb31ce7..9fce4318aa85 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -75,14 +74,12 @@ public RequestDecompressionProvider( if (StringValues.IsNullOrEmpty(encodings)) { - Debug.Assert(false, "Duplicate check failed."); _logger.NoContentEncoding(); return null; } if (encodings.Count > 1) { - Debug.Assert(false, "Duplicate check failed."); _logger.MultipleContentEncodingsSpecified(); return null; } @@ -95,7 +92,6 @@ public RequestDecompressionProvider( if (selectedProvider == null) { - Debug.Assert(false, "Duplicate check failed."); _logger.NoDecompressionProvider(); return null; } @@ -103,50 +99,4 @@ public RequestDecompressionProvider( _logger.DecompressingWith(selectedProvider.EncodingName); return selectedProvider; } - - /// - public bool ShouldDecompressRequest(HttpContext context) - { - var encodings = context.Request.Headers.ContentEncoding; - - if (StringValues.IsNullOrEmpty(encodings)) - { - _logger.NoContentEncoding(); - return false; - } - - _logger.ContentEncodingSpecified(); - return true; - } - - /// - public bool IsContentEncodingSupported(HttpContext context) - { - var encodings = context.Request.Headers.ContentEncoding; - - if (StringValues.IsNullOrEmpty(encodings)) - { - Debug.Assert(false, "Duplicate check failed."); - _logger.NoContentEncoding(); - return false; - } - - if (encodings.Count > 1) - { - _logger.MultipleContentEncodingsSpecified(); - return false; - } - - var encoding = encodings.Single(); - var supportedEncodings = _providers.Select(x => x.EncodingName); - - if (supportedEncodings.Any(x => string.Equals(x, encoding, StringComparison.OrdinalIgnoreCase))) - { - _logger.ContentEncodingSupported(encoding); - return true; - } - - _logger.ContentEncodingUnsupported(encoding); - return false; - } } diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs index 4b8db0698153..6dfde064a092 100644 --- a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs @@ -118,8 +118,7 @@ public async Task Request_UnsupportedContentEncoding_NotDecompressed() var (logMessages, outputContent) = await InvokeMiddleware(inputContent, new[] { contentEncoding }); - AssertLog(logMessages.First(), LogLevel.Trace, "The Content-Encoding header is specified. Proceeding with request decompression."); - AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, $"Request decompression is not supported for Content-Encoding '{contentEncoding}'."); + AssertLog(logMessages.First(), LogLevel.Debug, $"No matching request decompression provider found."); Assert.Equal(GetUncompressedContent(), outputContent); } @@ -131,8 +130,7 @@ public async Task Request_MultipleContentEncodings_NotDecompressed() var (logMessages, outputContent) = await InvokeMiddleware(inputContent, contentEncodings); - AssertLog(logMessages.First(), LogLevel.Trace, "The Content-Encoding header is specified. Proceeding with request decompression."); - AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings."); + AssertLog(logMessages.First(), LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings."); Assert.Equal(GetUncompressedContent(), outputContent); } @@ -199,9 +197,7 @@ private static void AssertLog(WriteContext log, LogLevel level, string message) private static void AssertDecompressedWithLog(List logMessages, string encoding) { - Assert.Equal(3, logMessages.Count); - AssertLog(logMessages.First(), LogLevel.Trace, "The Content-Encoding header is specified. Proceeding with request decompression."); - AssertLog(logMessages.Skip(1).First(), LogLevel.Trace, $"Request decompression is supported for Content-Encoding '{encoding}'."); - AssertLog(logMessages.Skip(2).First(), LogLevel.Debug, $"The request will be decompressed with '{encoding}'."); + var message = Assert.Single(logMessages); + AssertLog(message, LogLevel.Debug, $"The request will be decompressed with '{encoding}'."); } } From b4ddfd3006a9249de72e0690ad1752255caf553a Mon Sep 17 00:00:00 2001 From: David Acker Date: Fri, 18 Feb 2022 22:20:56 -0500 Subject: [PATCH 12/42] Remove Content-Encoding header if decompressed --- .../src/RequestDecompressionMiddleware.cs | 2 + .../RequestDecompressionMiddlewareTests.cs | 77 ++++++++++++++++--- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index 557c8830d55f..9e7ee9cdb8ad 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.RequestDecompression; @@ -60,6 +61,7 @@ private async Task InvokeCore(HttpContext context, IDecompressionProvider decomp var decompressionBody = new RequestDecompressionBody(decompressionStream); context.Request.Body = decompressionBody; + context.Request.Headers.Remove(HeaderNames.ContentEncoding); try { diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs index 6dfde064a092..973ae5fe53fb 100644 --- a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs @@ -63,17 +63,6 @@ static Stream compressorDelegate(Stream compressedContent) => return await GetCompressedContent(compressorDelegate, input); } - [Fact] - public async Task Request_NoContentEncoding_NotDecompressed() - { - var uncompressedContent = GetUncompressedContent(); - - var (logMessages, outputContent) = await InvokeMiddleware(uncompressedContent); - - AssertLog(logMessages.Single(), LogLevel.Trace, "The Content-Encoding header is missing or empty. Skipping request decompression."); - Assert.Equal(uncompressedContent, outputContent); - } - [Fact] public async Task Request_ContentEncodingBrotli_Decompressed() { @@ -110,6 +99,17 @@ public async Task Request_ContentEncodingGzip_Decompressed() Assert.Equal(GetUncompressedContent(), decompressedContent); } + [Fact] + public async Task Request_NoContentEncoding_NotDecompressed() + { + var uncompressedContent = GetUncompressedContent(); + + var (logMessages, outputContent) = await InvokeMiddleware(uncompressedContent); + + AssertLog(logMessages.Single(), LogLevel.Trace, "The Content-Encoding header is missing or empty. Skipping request decompression."); + Assert.Equal(uncompressedContent, outputContent); + } + [Fact] public async Task Request_UnsupportedContentEncoding_NotDecompressed() { @@ -189,6 +189,61 @@ public async Task Request_MultipleContentEncodings_NotDecompressed() return (sink.Writes.ToList(), outputContent); } + [Fact] + public async Task Request_MiddlewareAddedMultipleTimes_OnlyDecompressedOnce() + { + var compressedContent = await GetGZipCompressedContent(); + var contentEncoding = "gzip"; + + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var outputContent = Array.Empty(); + + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .ConfigureServices(services => + { + services.AddRequestDecompression(); + services.AddSingleton(loggerFactory); + }) + .Configure(app => + { + app.UseRequestDecompression(); + 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); + request.Content.Headers.ContentEncoding.Add(contentEncoding); + + await client.SendAsync(request); + + 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 missing or empty. Skipping request decompression."); + Assert.Equal(GetUncompressedContent(), outputContent); + } + private static void AssertLog(WriteContext log, LogLevel level, string message) { Assert.Equal(level, log.LogLevel); From 1d43fef08546dddd43b978133f50fc7b2378d11d Mon Sep 17 00:00:00 2001 From: David Acker Date: Sat, 19 Feb 2022 13:07:31 -0500 Subject: [PATCH 13/42] Fix extensions namespaces --- .../RequestDecompression/src/PublicAPI.Unshipped.txt | 5 +++++ .../src/RequestDecompressionBuilderExtensions.cs | 4 ++-- .../src/RequestDecompressionServiceExtensions.cs | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt index 72646ac9bb21..587c83778f56 100644 --- a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +Microsoft.AspNetCore.Builder.RequestDecompressionBuilderExtensions +Microsoft.AspNetCore.Builder.RequestDecompressionServiceExtensions Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider.BrotliDecompressionProvider() -> void Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! @@ -31,6 +33,9 @@ Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.Providers. Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.RequestDecompressionOptions() -> void Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider Microsoft.AspNetCore.RequestDecompression.RequestDecompressionServiceExtensions +static Microsoft.AspNetCore.Builder.RequestDecompressionBuilderExtensions.UseRequestDecompression(this Microsoft.AspNetCore.Builder.IApplicationBuilder! builder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.AspNetCore.Builder.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.AspNetCore.Builder.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.RequestDecompression.RequestDecompressionBuilderExtensions.UseRequestDecompression(this Microsoft.AspNetCore.Builder.IApplicationBuilder! builder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! static Microsoft.AspNetCore.RequestDecompression.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.RequestDecompression.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs index 948d4e3c7c3b..144c0bd4eb4e 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs @@ -1,9 +1,9 @@ // 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; +using Microsoft.AspNetCore.RequestDecompression; -namespace Microsoft.AspNetCore.RequestDecompression; +namespace Microsoft.AspNetCore.Builder; /// /// Extension methods for the request decompression middleware. diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs index 812ec8bfd53c..c4a00b098446 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs @@ -1,10 +1,11 @@ // 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; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace Microsoft.AspNetCore.RequestDecompression; +namespace Microsoft.AspNetCore.Builder; /// /// Extension methods for the RequestDecompression middleware. From fa987560d0cccd5b22aa0a029463e90d2017e96d Mon Sep 17 00:00:00 2001 From: David Acker Date: Sat, 19 Feb 2022 17:27:09 -0500 Subject: [PATCH 14/42] Remove RequestDecompressionBody --- .../src/RequestDecompressionBody.cs | 96 ------------------- .../src/RequestDecompressionMiddleware.cs | 6 +- 2 files changed, 2 insertions(+), 100 deletions(-) delete mode 100644 src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs deleted file mode 100644 index ea74d746b6d1..000000000000 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionBody.cs +++ /dev/null @@ -1,96 +0,0 @@ -// 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; - -/// -/// Stream wrapper that creates a specific decompression stream if necessary. -/// -internal sealed class RequestDecompressionBody : Stream -{ - private readonly Stream _decompressionStream; - private bool _complete; - - internal RequestDecompressionBody(Stream decompressionStream) - { - _decompressionStream = decompressionStream; - } - - public override bool CanRead => true; - - public override bool CanSeek => false; - - public override bool CanWrite => _decompressionStream.CanWrite; - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _decompressionStream.Read(buffer, offset, count); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Flush() - { - throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - => TaskToApm.Begin(ReadAsync(buffer, offset, count, CancellationToken.None), callback, state); - - public override int EndRead(IAsyncResult asyncResult) - => TaskToApm.End(asyncResult); - - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken); - - public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - return await _decompressionStream.ReadAsync(buffer, cancellationToken); - } - - public async Task CompleteAsync() - { - if (_complete) - { - return; - } - - await FinishDecompressionAsync(); - } - - internal async Task FinishDecompressionAsync() - { - if (_complete) - { - return; - } - - _complete = true; - - await _decompressionStream.DisposeAsync(); - } -} diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index 9e7ee9cdb8ad..17306f32544a 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -57,16 +57,14 @@ private async Task InvokeCore(HttpContext context, IDecompressionProvider decomp { var originalBody = context.Request.Body; - var decompressionStream = decompressionProvider.CreateStream(originalBody); - var decompressionBody = new RequestDecompressionBody(decompressionStream); + await using var decompressionStream = decompressionProvider.CreateStream(originalBody); - context.Request.Body = decompressionBody; + context.Request.Body = decompressionStream; context.Request.Headers.Remove(HeaderNames.ContentEncoding); try { await _next(context); - await decompressionBody.FinishDecompressionAsync(); } finally { From e055698e4e08d28d27f491d7445149aafc81aeeb Mon Sep 17 00:00:00 2001 From: David Acker Date: Sat, 19 Feb 2022 18:26:19 -0500 Subject: [PATCH 15/42] Fix class name --- .../RequestDecompression/sample/Startup.cs | 2 +- .../src/GzipDecompressionProvider.cs | 2 +- .../src/PublicAPI.Unshipped.txt | 14 ++++---------- .../src/RequestDecompressionProvider.cs | 2 +- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Middleware/RequestDecompression/sample/Startup.cs b/src/Middleware/RequestDecompression/sample/Startup.cs index aa32ba2dccb3..9a52496adfcc 100644 --- a/src/Middleware/RequestDecompression/sample/Startup.cs +++ b/src/Middleware/RequestDecompression/sample/Startup.cs @@ -11,7 +11,7 @@ public void ConfigureServices(IServiceCollection services) { services.AddRequestDecompression(options => { - options.Providers.Add(); + options.Providers.Add(); options.Providers.Add(); }); } diff --git a/src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs index 0e877a6b150f..7da30bc6d6a0 100644 --- a/src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// /// GZip decompression provider. /// -public sealed class GzipDecompressionProvider : IDecompressionProvider +public sealed class GZipDecompressionProvider : IDecompressionProvider { /// public string EncodingName => "gzip"; diff --git a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt index 587c83778f56..94ddcd67c3bc 100644 --- a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt @@ -13,10 +13,10 @@ Microsoft.AspNetCore.RequestDecompression.DeflateDecompressionProvider Microsoft.AspNetCore.RequestDecompression.DeflateDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! Microsoft.AspNetCore.RequestDecompression.DeflateDecompressionProvider.DeflateDecompressionProvider() -> void Microsoft.AspNetCore.RequestDecompression.DeflateDecompressionProvider.EncodingName.get -> string! -Microsoft.AspNetCore.RequestDecompression.GzipDecompressionProvider -Microsoft.AspNetCore.RequestDecompression.GzipDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! -Microsoft.AspNetCore.RequestDecompression.GzipDecompressionProvider.EncodingName.get -> string! -Microsoft.AspNetCore.RequestDecompression.GzipDecompressionProvider.GzipDecompressionProvider() -> void +Microsoft.AspNetCore.RequestDecompression.GZipDecompressionProvider +Microsoft.AspNetCore.RequestDecompression.GZipDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! +Microsoft.AspNetCore.RequestDecompression.GZipDecompressionProvider.EncodingName.get -> string! +Microsoft.AspNetCore.RequestDecompression.GZipDecompressionProvider.GZipDecompressionProvider() -> void Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider.EncodingName.get -> string! @@ -24,21 +24,15 @@ Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.GetDecompressionProvider(Microsoft.AspNetCore.Http.HttpContext! context) -> Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider? Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.IsContentEncodingSupported(Microsoft.AspNetCore.Http.HttpContext! context) -> bool Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.ShouldDecompressRequest(Microsoft.AspNetCore.Http.HttpContext! context) -> bool -Microsoft.AspNetCore.RequestDecompression.RequestDecompressionBuilderExtensions Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware.RequestDecompressionMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider! provider) -> void Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.Providers.get -> Microsoft.AspNetCore.RequestDecompression.DecompressionProviderCollection! Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.RequestDecompressionOptions() -> void -Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider -Microsoft.AspNetCore.RequestDecompression.RequestDecompressionServiceExtensions static Microsoft.AspNetCore.Builder.RequestDecompressionBuilderExtensions.UseRequestDecompression(this Microsoft.AspNetCore.Builder.IApplicationBuilder! builder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! static Microsoft.AspNetCore.Builder.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.Builder.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.AspNetCore.RequestDecompression.RequestDecompressionBuilderExtensions.UseRequestDecompression(this Microsoft.AspNetCore.Builder.IApplicationBuilder! builder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! -static Microsoft.AspNetCore.RequestDecompression.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.AspNetCore.RequestDecompression.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.GetDecompressionProvider(Microsoft.AspNetCore.Http.HttpContext! context) -> Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider? virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.IsContentEncodingSupported(Microsoft.AspNetCore.Http.HttpContext! context) -> bool virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.ShouldDecompressRequest(Microsoft.AspNetCore.Http.HttpContext! context) -> bool diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs index 9fce4318aa85..f320d9b76743 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs @@ -52,7 +52,7 @@ public RequestDecompressionProvider( { new DecompressionProviderFactory(typeof(BrotliDecompressionProvider)), new DecompressionProviderFactory(typeof(DeflateDecompressionProvider)), - new DecompressionProviderFactory(typeof(GzipDecompressionProvider)) + new DecompressionProviderFactory(typeof(GZipDecompressionProvider)) }; } From 03e31c1afd32d00be578c24d61a9c4760a476bf9 Mon Sep 17 00:00:00 2001 From: David Acker Date: Sat, 19 Feb 2022 18:32:17 -0500 Subject: [PATCH 16/42] Directly instantiate default providers --- .../src/RequestDecompressionProvider.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs index f320d9b76743..cb8c9a072983 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs @@ -50,16 +50,15 @@ public RequestDecompressionProvider( { _providers = new IDecompressionProvider[] { - new DecompressionProviderFactory(typeof(BrotliDecompressionProvider)), - new DecompressionProviderFactory(typeof(DeflateDecompressionProvider)), - new DecompressionProviderFactory(typeof(GZipDecompressionProvider)) + new BrotliDecompressionProvider(), + new DeflateDecompressionProvider(), + new GZipDecompressionProvider() }; } for (var i = 0; i < _providers.Length; i++) { - var factory = _providers[i] as DecompressionProviderFactory; - if (factory != null) + if (_providers[i] is DecompressionProviderFactory factory) { _providers[i] = factory.CreateInstance(services); } From dca2b1774e77d10c25a25a6eb8a374bf5b028daf Mon Sep 17 00:00:00 2001 From: David Acker Date: Sat, 19 Feb 2022 18:46:41 -0500 Subject: [PATCH 17/42] Replace Single with cast --- .../RequestDecompression/src/RequestDecompressionProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs index cb8c9a072983..28fe0d16ad77 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs @@ -83,7 +83,7 @@ public RequestDecompressionProvider( return null; } - var encodingName = encodings.Single(); + string encodingName = encodings!; var selectedProvider = _providers.FirstOrDefault(x => From 202cd2d60965fa3b6ee87f37b25ed0fa1ff218ad Mon Sep 17 00:00:00 2001 From: David Acker Date: Sun, 20 Feb 2022 13:25:50 -0500 Subject: [PATCH 18/42] Add tests --- .../RequestDecompressionMiddlewareTests.cs | 133 ++++++++++++++++-- 1 file changed, 119 insertions(+), 14 deletions(-) diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs index 973ae5fe53fb..c176d18c7cbc 100644 --- a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.RequestDecompression.Tests; @@ -63,39 +64,42 @@ static Stream compressorDelegate(Stream compressedContent) => return await GetCompressedContent(compressorDelegate, input); } - [Fact] - public async Task Request_ContentEncodingBrotli_Decompressed() + [Theory] + [InlineData("br")] + [InlineData("BR")] + public async Task Request_ContentEncodingBrotli_Decompressed(string contentEncoding) { var compressedContent = await GetBrotliCompressedContent(); - var contentEncoding = "br"; var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, new[] { contentEncoding }); - AssertDecompressedWithLog(logMessages, contentEncoding); + AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant()); Assert.Equal(GetUncompressedContent(), decompressedContent); } - [Fact] - public async Task Request_ContentEncodingDeflate_Decompressed() + [Theory] + [InlineData("deflate")] + [InlineData("DEFLATE")] + public async Task Request_ContentEncodingDeflate_Decompressed(string contentEncoding) { var compressedContent = await GetDeflateCompressedContent(); - var contentEncoding = "deflate"; var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, new[] { contentEncoding }); - AssertDecompressedWithLog(logMessages, contentEncoding); + AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant()); Assert.Equal(GetUncompressedContent(), decompressedContent); } - [Fact] - public async Task Request_ContentEncodingGzip_Decompressed() + [Theory] + [InlineData("gzip")] + [InlineData("GZIP")] + public async Task Request_ContentEncodingGzip_Decompressed(string contentEncoding) { var compressedContent = await GetGZipCompressedContent(); - var contentEncoding = "gzip"; var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, new[] { contentEncoding }); - AssertDecompressedWithLog(logMessages, contentEncoding); + AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant()); Assert.Equal(GetUncompressedContent(), decompressedContent); } @@ -118,7 +122,7 @@ public async Task Request_UnsupportedContentEncoding_NotDecompressed() var (logMessages, outputContent) = await InvokeMiddleware(inputContent, new[] { contentEncoding }); - AssertLog(logMessages.First(), LogLevel.Debug, $"No matching request decompression provider found."); + AssertNoDecompressionProviderLog(logMessages); Assert.Equal(GetUncompressedContent(), outputContent); } @@ -130,7 +134,8 @@ public async Task Request_MultipleContentEncodings_NotDecompressed() var (logMessages, outputContent) = await InvokeMiddleware(inputContent, contentEncodings); - AssertLog(logMessages.First(), LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings."); + var logMessage = Assert.Single(logMessages); + AssertLog(logMessage, LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings."); Assert.Equal(GetUncompressedContent(), outputContent); } @@ -244,6 +249,100 @@ public async Task Request_MiddlewareAddedMultipleTimes_OnlyDecompressedOnce() Assert.Equal(GetUncompressedContent(), outputContent); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Request_Decompressed_ContentEncodingHeaderRemoved(bool isDecompressed) + { + var compressedContent = await GetGZipCompressedContent(); + var contentEncoding = isDecompressed ? "gzip" : "custom"; + + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var outputContent = Array.Empty(); + var contentEncodingHeader = new StringValues(); + + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .ConfigureServices(services => + { + services.AddRequestDecompression(); + services.AddSingleton(loggerFactory); + }) + .Configure(app => + { + 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); + 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); + request.Content.Headers.ContentEncoding.Add(contentEncoding); + + await client.SendAsync(request); + + var logMessages = sink.Writes.ToList(); + + if (isDecompressed) + { + AssertDecompressedWithLog(logMessages, contentEncoding); + Assert.Empty(contentEncodingHeader); + Assert.Equal(GetUncompressedContent(), outputContent); + } + else + { + AssertNoDecompressionProviderLog(logMessages); + Assert.Equal(compressedContent, outputContent); + } + } + + [Theory] + [InlineData("gzip", true)] + [InlineData("br", false)] + public async Task Options_IncludesProviders_OnlyUsesRegisteredProviders(string contentEncoding, bool explicitlyRegistered) + { + var compressedContent = await GetGZipCompressedContent(); + + var (logMessages, outputContent) = + await InvokeMiddleware( + compressedContent, + new[] { contentEncoding }, + configure: (RequestDecompressionOptions options) => + { + options.Providers.Add(); + }); + + if (explicitlyRegistered) + { + AssertDecompressedWithLog(logMessages, contentEncoding); + Assert.Equal(GetUncompressedContent(), outputContent); + } + else + { + AssertNoDecompressionProviderLog(logMessages); + Assert.Equal(compressedContent, outputContent); + } + } + private static void AssertLog(WriteContext log, LogLevel level, string message) { Assert.Equal(level, log.LogLevel); @@ -255,4 +354,10 @@ private static void AssertDecompressedWithLog(List logMessages, st var message = Assert.Single(logMessages); AssertLog(message, LogLevel.Debug, $"The request will be decompressed with '{encoding}'."); } + + private static void AssertNoDecompressionProviderLog(List logMessages) + { + var logMessage = Assert.Single(logMessages); + AssertLog(logMessage, LogLevel.Debug, "No matching request decompression provider found."); + } } From 087cf21b2d81c824228da09de3e496b9e44b0b1c Mon Sep 17 00:00:00 2001 From: David Acker Date: Sun, 20 Feb 2022 14:45:18 -0500 Subject: [PATCH 19/42] Store providers in dictionary --- .../src/RequestDecompressionProvider.cs | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs index 28fe0d16ad77..309de4cc40c5 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// internal sealed class RequestDecompressionProvider : IRequestDecompressionProvider { - private readonly IDecompressionProvider[] _providers; + private readonly IReadOnlyDictionary _providers; private readonly ILogger _logger; /// @@ -45,10 +45,10 @@ public RequestDecompressionProvider( var requestDecompressionOptions = options.Value; - _providers = requestDecompressionOptions.Providers.ToArray(); - if (_providers.Length == 0) + var registeredProviders = requestDecompressionOptions.Providers.ToArray(); + if (registeredProviders.Length == 0) { - _providers = new IDecompressionProvider[] + registeredProviders = new IDecompressionProvider[] { new BrotliDecompressionProvider(), new DeflateDecompressionProvider(), @@ -56,13 +56,22 @@ public RequestDecompressionProvider( }; } - for (var i = 0; i < _providers.Length; i++) + var providers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var provider in registeredProviders) { - if (_providers[i] is DecompressionProviderFactory factory) + if (provider is DecompressionProviderFactory factory) + { + var providerInstance = factory.CreateInstance(services); + providers[providerInstance.EncodingName] = providerInstance; + } + else { - _providers[i] = factory.CreateInstance(services); + providers[provider.EncodingName] = provider; } } + + _providers = providers; } /// @@ -85,17 +94,13 @@ public RequestDecompressionProvider( string encodingName = encodings!; - var selectedProvider = - _providers.FirstOrDefault(x => - StringSegment.Equals(x.EncodingName, encodingName, StringComparison.OrdinalIgnoreCase)); - - if (selectedProvider == null) + if (_providers.TryGetValue(encodingName, out var matchingProvider)) { - _logger.NoDecompressionProvider(); - return null; + _logger.DecompressingWith(matchingProvider.EncodingName); + return matchingProvider; } - _logger.DecompressingWith(selectedProvider.EncodingName); - return selectedProvider; + _logger.NoDecompressionProvider(); + return null; } } From 8857a6323c2986b07df9eb2cb46c6dfd50a52e65 Mon Sep 17 00:00:00 2001 From: David Acker Date: Sun, 20 Feb 2022 15:00:54 -0500 Subject: [PATCH 20/42] Fix API baselines --- .../RequestDecompression/src/PublicAPI.Unshipped.txt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt index 94ddcd67c3bc..a7bae12682fc 100644 --- a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable Microsoft.AspNetCore.Builder.RequestDecompressionBuilderExtensions Microsoft.AspNetCore.Builder.RequestDecompressionServiceExtensions Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider @@ -22,8 +22,6 @@ Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider.CreateStream(Sy Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider.EncodingName.get -> string! Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.GetDecompressionProvider(Microsoft.AspNetCore.Http.HttpContext! context) -> Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider? -Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.IsContentEncodingSupported(Microsoft.AspNetCore.Http.HttpContext! context) -> bool -Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.ShouldDecompressRequest(Microsoft.AspNetCore.Http.HttpContext! context) -> bool Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware.RequestDecompressionMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider! provider) -> void @@ -33,7 +31,3 @@ Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.RequestDec static Microsoft.AspNetCore.Builder.RequestDecompressionBuilderExtensions.UseRequestDecompression(this Microsoft.AspNetCore.Builder.IApplicationBuilder! builder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! static Microsoft.AspNetCore.Builder.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.Builder.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.GetDecompressionProvider(Microsoft.AspNetCore.Http.HttpContext! context) -> Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider? -virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.IsContentEncodingSupported(Microsoft.AspNetCore.Http.HttpContext! context) -> bool -virtual Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.ShouldDecompressRequest(Microsoft.AspNetCore.Http.HttpContext! context) -> bool -~Microsoft.AspNetCore.RequestDecompression.RequestDecompressionProvider.RequestDecompressionProvider(System.IServiceProvider! services, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Options.IOptions! options) -> void From c4537394f232f42ee80f65e4d24235db5f8be9b6 Mon Sep 17 00:00:00 2001 From: David Acker Date: Sun, 20 Feb 2022 22:27:48 -0500 Subject: [PATCH 21/42] Add MaxRequestBodySize to RequestDecompressionOptions --- .../src/IRequestDecompressionProvider.cs | 7 ++++ .../src/PublicAPI.Unshipped.txt | 2 ++ .../RequestDecompressionLoggingExtensions.cs | 12 ++++++- .../src/RequestDecompressionMiddleware.cs | 3 ++ .../src/RequestDecompressionOptions.cs | 5 +++ .../src/RequestDecompressionProvider.cs | 32 ++++++++++++++++--- .../RequestDecompressionMiddlewareTests.cs | 10 +++--- 7 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs index c01f2bd0f5f8..9ccead793668 100644 --- a/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.RequestDecompression; @@ -16,4 +17,10 @@ public interface IRequestDecompressionProvider /// The . /// A decompression provider or null if there are no acceptable providers. IDecompressionProvider? GetDecompressionProvider(HttpContext context); + + /// + /// Sets , if the feature exists. + /// + /// The . + void SetRequestSizeLimit(HttpContext context); } diff --git a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt index a7bae12682fc..8d1386ddb228 100644 --- a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt @@ -26,6 +26,8 @@ Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware.RequestDecompressionMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider! provider) -> void Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.MaxRequestBodySize.get -> long +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.MaxRequestBodySize.set -> void Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.Providers.get -> Microsoft.AspNetCore.RequestDecompression.DecompressionProviderCollection! Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.RequestDecompressionOptions() -> void static Microsoft.AspNetCore.Builder.RequestDecompressionBuilderExtensions.UseRequestDecompression(this Microsoft.AspNetCore.Builder.IApplicationBuilder! builder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs index fabfa17eabc6..57a828689be4 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs @@ -1,6 +1,7 @@ // 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.Features; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.RequestDecompression; @@ -16,6 +17,15 @@ internal static partial class RequestDecompressionLoggingExtensions [LoggerMessage(3, LogLevel.Debug, "No matching request decompression provider found.", EventName = "NoDecompressionProvider")] public static partial void NoDecompressionProvider(this ILogger logger); - [LoggerMessage(4, LogLevel.Debug, "The request will be decompressed with '{provider}'.", EventName = "DecompressingWith")] + [LoggerMessage(4, LogLevel.Debug, "The request will be decompressed with '{Provider}'.", EventName = "DecompressingWith")] public static partial void DecompressingWith(this ILogger logger, string provider); + + [LoggerMessage(5, 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(this ILogger logger); + + [LoggerMessage(6, 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(this ILogger logger); + + [LoggerMessage(7, LogLevel.Debug, "The maximum request body size has been set to {RequestSize}.", EventName = "MaxRequestBodySizeSet")] + public static partial void MaxRequestBodySizeSet(this ILogger logger, long? requestSize); } diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index 17306f32544a..bb76782719f6 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.RequestDecompression; @@ -62,6 +63,8 @@ private async Task InvokeCore(HttpContext context, IDecompressionProvider decomp context.Request.Body = decompressionStream; context.Request.Headers.Remove(HeaderNames.ContentEncoding); + _provider.SetRequestSizeLimit(context); + try { await _next(context); diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs index 2512f41f2cdc..9ccb659fdadf 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs @@ -12,4 +12,9 @@ public class RequestDecompressionOptions /// The types to use for request decompression. /// public DecompressionProviderCollection Providers { get; } = new DecompressionProviderCollection(); + + /// + /// The maximum allowed size of the decompressed request body in bytes. + /// + public long MaxRequestBodySize { get; set; } } diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs index 309de4cc40c5..8e98ddc71681 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -12,9 +13,11 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// internal sealed class RequestDecompressionProvider : IRequestDecompressionProvider { - private readonly IReadOnlyDictionary _providers; + private readonly RequestDecompressionOptions _options; private readonly ILogger _logger; + private readonly IReadOnlyDictionary _providers; + /// /// If no decompression providers are specified then all default providers will be registered. /// @@ -41,11 +44,10 @@ public RequestDecompressionProvider( throw new ArgumentNullException(nameof(options)); } - _logger = logger; - - var requestDecompressionOptions = options.Value; + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - var registeredProviders = requestDecompressionOptions.Providers.ToArray(); + var registeredProviders = _options.Providers.ToArray(); if (registeredProviders.Length == 0) { registeredProviders = new IDecompressionProvider[] @@ -103,4 +105,24 @@ public RequestDecompressionProvider( _logger.NoDecompressionProvider(); return null; } + + /// + public void SetRequestSizeLimit(HttpContext context) + { + var maxRequestBodySizeFeature = context.Features.Get(); + + if (maxRequestBodySizeFeature == null) + { + _logger.FeatureNotFound(); + } + else if (maxRequestBodySizeFeature.IsReadOnly) + { + _logger.FeatureIsReadOnly(); + } + else + { + maxRequestBodySizeFeature.MaxRequestBodySize = _options.MaxRequestBodySize; + _logger.MaxRequestBodySizeSet(_options.MaxRequestBodySize); + } + } } diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs index c176d18c7cbc..90b535faa6ee 100644 --- a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs @@ -243,9 +243,10 @@ public async Task Request_MiddlewareAddedMultipleTimes_OnlyDecompressedOnce() var logMessages = sink.Writes.ToList(); - Assert.Equal(2, logMessages.Count); + Assert.Equal(3, 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 missing or empty. Skipping request decompression."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Warning, "A request body size limit could not be applied. This server does not support the IHttpMaxRequestBodySizeFeature."); + AssertLog(logMessages.Skip(2).First(), LogLevel.Trace, "The Content-Encoding header is missing or empty. Skipping request decompression."); Assert.Equal(GetUncompressedContent(), outputContent); } @@ -351,8 +352,9 @@ private static void AssertLog(WriteContext log, LogLevel level, string message) private static void AssertDecompressedWithLog(List logMessages, string encoding) { - var message = Assert.Single(logMessages); - AssertLog(message, LogLevel.Debug, $"The request will be decompressed with '{encoding}'."); + Assert.Equal(2, logMessages.Count); + AssertLog(logMessages.First(), LogLevel.Debug, $"The request will be decompressed with '{encoding}'."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Warning, "A request body size limit could not be applied. This server does not support the IHttpMaxRequestBodySizeFeature."); } private static void AssertNoDecompressionProviderLog(List logMessages) From 2d674b1beed65944d0bb30fdccfcdc3cb599a0db Mon Sep 17 00:00:00 2001 From: David Acker Date: Mon, 21 Feb 2022 20:55:22 -0500 Subject: [PATCH 22/42] Clean up --- .../src/Microsoft.AspNetCore.RequestDecompression.csproj | 4 ---- .../RequestDecompression/src/PublicAPI.Unshipped.txt | 1 + .../src/RequestDecompressionLoggingExtensions.cs | 2 +- .../src/RequestDecompressionMiddleware.cs | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj index 88d09fc07d77..b77ac11563ad 100644 --- a/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj +++ b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj @@ -17,8 +17,4 @@ - - - - \ No newline at end of file diff --git a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt index 8d1386ddb228..e0dc10e2ebea 100644 --- a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt @@ -22,6 +22,7 @@ Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider.CreateStream(Sy Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider.EncodingName.get -> string! Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.GetDecompressionProvider(Microsoft.AspNetCore.Http.HttpContext! context) -> Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider? +Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.SetRequestSizeLimit(Microsoft.AspNetCore.Http.HttpContext! context) -> void Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware.RequestDecompressionMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider! provider) -> void diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs index 57a828689be4..482c69044c32 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs @@ -27,5 +27,5 @@ internal static partial class RequestDecompressionLoggingExtensions public static partial void FeatureIsReadOnly(this ILogger logger); [LoggerMessage(7, LogLevel.Debug, "The maximum request body size has been set to {RequestSize}.", EventName = "MaxRequestBodySizeSet")] - public static partial void MaxRequestBodySizeSet(this ILogger logger, long? requestSize); + public static partial void MaxRequestBodySizeSet(this ILogger logger, long requestSize); } diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index bb76782719f6..defd4ca6c949 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.RequestDecompression; From 76fe411ebda30844f06939b669944d5b93176f1a Mon Sep 17 00:00:00 2001 From: David Acker Date: Mon, 21 Feb 2022 20:55:47 -0500 Subject: [PATCH 23/42] Add, clean up tests --- .../RequestDecompressionMiddlewareTests.cs | 285 +++++++++++++----- 1 file changed, 210 insertions(+), 75 deletions(-) diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs index 90b535faa6ee..5cabaf88f9c1 100644 --- a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs @@ -6,6 +6,7 @@ using System.Text; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -110,7 +111,8 @@ public async Task Request_NoContentEncoding_NotDecompressed() var (logMessages, outputContent) = await InvokeMiddleware(uncompressedContent); - AssertLog(logMessages.Single(), LogLevel.Trace, "The Content-Encoding header is missing or empty. Skipping request decompression."); + var logMessage = Assert.Single(logMessages); + AssertLog(logMessage, LogLevel.Trace, "The Content-Encoding header is missing or empty. Skipping request decompression."); Assert.Equal(uncompressedContent, outputContent); } @@ -139,74 +141,19 @@ public async Task Request_MultipleContentEncodings_NotDecompressed() Assert.Equal(GetUncompressedContent(), outputContent); } - private static async Task<(List, byte[])> InvokeMiddleware( - byte[] compressedContent, - string[] contentEncodings = null, - Action configure = null) - { - var sink = new TestSink( - TestSink.EnableWithTypeName, - TestSink.EnableWithTypeName); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); - - var outputContent = Array.Empty(); - - using var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .UseTestServer() - .ConfigureServices(services => - { - services.AddRequestDecompression(configure ?? (_ => { })); - services.AddSingleton(loggerFactory); - }) - .Configure(app => - { - 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); - } - [Fact] public async Task Request_MiddlewareAddedMultipleTimes_OnlyDecompressedOnce() { var compressedContent = await GetGZipCompressedContent(); var contentEncoding = "gzip"; + var outputContent = Array.Empty(); + var sink = new TestSink( TestSink.EnableWithTypeName, TestSink.EnableWithTypeName); var loggerFactory = new TestLoggerFactory(sink, enabled: true); - var outputContent = Array.Empty(); - using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { @@ -245,8 +192,9 @@ public async Task Request_MiddlewareAddedMultipleTimes_OnlyDecompressedOnce() Assert.Equal(3, logMessages.Count); AssertLog(logMessages.First(), LogLevel.Debug, $"The request will be decompressed with '{contentEncoding}'."); - AssertLog(logMessages.Skip(1).First(), LogLevel.Warning, "A request body size limit could not be applied. This server does not support the IHttpMaxRequestBodySizeFeature."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Warning, $"A request body size limit could not be applied. This server does not support the {nameof(IHttpMaxRequestBodySizeFeature)}."); AssertLog(logMessages.Skip(2).First(), LogLevel.Trace, "The Content-Encoding header is missing or empty. Skipping request decompression."); + Assert.Equal(GetUncompressedContent(), outputContent); } @@ -255,17 +203,14 @@ public async Task Request_MiddlewareAddedMultipleTimes_OnlyDecompressedOnce() [InlineData(false)] public async Task Request_Decompressed_ContentEncodingHeaderRemoved(bool isDecompressed) { - var compressedContent = await GetGZipCompressedContent(); var contentEncoding = isDecompressed ? "gzip" : "custom"; + var contentEncodingHeader = new StringValues(); var sink = new TestSink( TestSink.EnableWithTypeName, TestSink.EnableWithTypeName); var loggerFactory = new TestLoggerFactory(sink, enabled: true); - var outputContent = Array.Empty(); - var contentEncodingHeader = new StringValues(); - using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { @@ -279,13 +224,10 @@ public async Task Request_Decompressed_ContentEncodingHeaderRemoved(bool isDecom .Configure(app => { app.UseRequestDecompression(); - app.Run(async context => + app.Run(context => { contentEncodingHeader = context.Request.Headers.ContentEncoding; - - await using var ms = new MemoryStream(); - await context.Request.Body.CopyToAsync(ms, context.RequestAborted); - outputContent = ms.ToArray(); + return Task.CompletedTask; }); }); }).Build(); @@ -296,7 +238,7 @@ public async Task Request_Decompressed_ContentEncodingHeaderRemoved(bool isDecom var client = server.CreateClient(); using var request = new HttpRequestMessage(HttpMethod.Post, ""); - request.Content = new ByteArrayContent(compressedContent); + request.Content = new ByteArrayContent(new byte[1]); request.Content.Headers.ContentEncoding.Add(contentEncoding); await client.SendAsync(request); @@ -307,12 +249,11 @@ public async Task Request_Decompressed_ContentEncodingHeaderRemoved(bool isDecom { AssertDecompressedWithLog(logMessages, contentEncoding); Assert.Empty(contentEncodingHeader); - Assert.Equal(GetUncompressedContent(), outputContent); } else { AssertNoDecompressionProviderLog(logMessages); - Assert.Equal(compressedContent, outputContent); + Assert.Equal(contentEncoding, contentEncodingHeader); } } @@ -323,7 +264,7 @@ public async Task Options_IncludesProviders_OnlyUsesRegisteredProviders(string c { var compressedContent = await GetGZipCompressedContent(); - var (logMessages, outputContent) = + var (logMessages, _) = await InvokeMiddleware( compressedContent, new[] { contentEncoding }, @@ -335,15 +276,141 @@ await InvokeMiddleware( if (explicitlyRegistered) { AssertDecompressedWithLog(logMessages, contentEncoding); - Assert.Equal(GetUncompressedContent(), outputContent); } else { AssertNoDecompressionProviderLog(logMessages); - Assert.Equal(compressedContent, outputContent); } } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Request_ServerSupportsMaxBodySizeFeature_RequestSizeLimitSet(bool supportsFeature) + { + var contentEncoding = "gzip"; + long maxRequestBodySize = 100; + + var fakeMaxRequestBodySize = supportsFeature + ? new FakeHttpMaxRequestBodySizeFeature() + : null; + + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .ConfigureServices(services => + { + services.AddRequestDecompression(options => + { + options.MaxRequestBodySize = maxRequestBodySize; + }); + services.AddSingleton(loggerFactory); + }) + .Configure(app => + { + app.Use((context, next) => + { + context.Features.Set(fakeMaxRequestBodySize); + return next(context); + }); + app.UseRequestDecompression(); + app.Run(context => Task.CompletedTask); + }); + }).Build(); + + await host.StartAsync(); + + var server = host.GetTestServer(); + var client = server.CreateClient(); + + using var request = new HttpRequestMessage(HttpMethod.Post, ""); + request.Content = new ByteArrayContent(new byte[1]); + request.Content.Headers.ContentEncoding.Add(contentEncoding); + + await client.SendAsync(request); + + var logMessages = sink.Writes.ToList(); + + Assert.Equal(2, logMessages.Count); + AssertLog(logMessages.First(), LogLevel.Debug, $"The request will be decompressed with '{contentEncoding}'."); + + if (supportsFeature) + { + AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, $"The maximum request body size has been set to {maxRequestBodySize}."); + Assert.Equal(maxRequestBodySize, fakeMaxRequestBodySize.MaxRequestBodySize); + } + else + { + AssertLog(logMessages.Skip(1).First(), LogLevel.Warning, $"A request body size limit could not be applied. This server does not support the {nameof(IHttpMaxRequestBodySizeFeature)}."); + } + } + + [Fact] + public async Task Request_ReadOnlyMaxBodySizeFeature_RequestSizeLimitNotSet() + { + var contentEncoding = "gzip"; + long maxRequestBodySize = 100; + + FakeHttpMaxRequestBodySizeFeature fakeMaxRequestBodySize = null; + + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .ConfigureServices(services => + { + services.AddRequestDecompression(options => + { + options.MaxRequestBodySize = maxRequestBodySize; + }); + services.AddSingleton(loggerFactory); + }) + .Configure(app => + { + app.Use((context, next) => + { + fakeMaxRequestBodySize = new FakeHttpMaxRequestBodySizeFeature(isReadOnly: true); + context.Features.Set(fakeMaxRequestBodySize); + return next(context); + }); + app.UseRequestDecompression(); + app.Run(context => Task.CompletedTask); + }); + }).Build(); + + await host.StartAsync(); + + var server = host.GetTestServer(); + var client = server.CreateClient(); + + using var request = new HttpRequestMessage(HttpMethod.Post, ""); + request.Content = new ByteArrayContent(new byte[1]); + request.Content.Headers.ContentEncoding.Add(contentEncoding); + + await client.SendAsync(request); + + 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.Warning, $"A request body size limit could not be applied. The {nameof(IHttpMaxRequestBodySizeFeature)} for the server is read-only."); + + Assert.NotEqual(maxRequestBodySize, fakeMaxRequestBodySize.MaxRequestBodySize); + } + private static void AssertLog(WriteContext log, LogLevel level, string message) { Assert.Equal(level, log.LogLevel); @@ -354,7 +421,7 @@ private static void AssertDecompressedWithLog(List logMessages, st { Assert.Equal(2, logMessages.Count); AssertLog(logMessages.First(), LogLevel.Debug, $"The request will be decompressed with '{encoding}'."); - AssertLog(logMessages.Skip(1).First(), LogLevel.Warning, "A request body size limit could not be applied. This server does not support the IHttpMaxRequestBodySizeFeature."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Warning, $"A request body size limit could not be applied. This server does not support the {nameof(IHttpMaxRequestBodySizeFeature)}."); } private static void AssertNoDecompressionProviderLog(List logMessages) @@ -362,4 +429,72 @@ private static void AssertNoDecompressionProviderLog(List logMessa var logMessage = Assert.Single(logMessages); AssertLog(logMessage, LogLevel.Debug, "No matching request decompression provider found."); } + + private static async Task<(List, byte[])> InvokeMiddleware( + byte[] compressedContent, + string[] contentEncodings = null, + Action configure = null) + { + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var outputContent = Array.Empty(); + + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .ConfigureServices(services => + { + services.AddRequestDecompression(configure ?? (_ => { })); + services.AddSingleton(loggerFactory); + }) + .Configure(app => + { + 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 FakeHttpMaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature + { + public FakeHttpMaxRequestBodySizeFeature(long? maxRequestBodySize = null, bool isReadOnly = false) + { + MaxRequestBodySize = maxRequestBodySize; + IsReadOnly = isReadOnly; + } + + public bool IsReadOnly { get; } + + public long? MaxRequestBodySize { get; set; } + } } From 7b297ca19431ce53958dad0a19b6a2d19c50fdb1 Mon Sep 17 00:00:00 2001 From: David Acker Date: Sun, 27 Feb 2022 19:24:23 -0500 Subject: [PATCH 24/42] Add IRequestSizeLimitMetadata --- .../src/Metadata/IRequestSizeLimitMetadata.cs | 15 +++++++++++++++ .../Http.Abstractions/src/PublicAPI.Unshipped.txt | 2 ++ .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 1 + src/Mvc/Mvc.Core/src/RequestSizeLimitAttribute.cs | 8 ++++++-- 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 src/Http/Http.Abstractions/src/Metadata/IRequestSizeLimitMetadata.cs diff --git a/src/Http/Http.Abstractions/src/Metadata/IRequestSizeLimitMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IRequestSizeLimitMetadata.cs new file mode 100644 index 000000000000..bb26d8e73b3e --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IRequestSizeLimitMetadata.cs @@ -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; + +/// +/// Interface marking attributes that specify the maximum allowed size of the request body. +/// +public interface IRequestSizeLimitMetadata +{ + /// + /// The maximum allowed size of the current request body in bytes. + /// + long MaxRequestBodySize { get; } +} diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 92c400c0bac6..115c0b97ed0f 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -2,5 +2,7 @@ *REMOVED*abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string! 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 abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string? Microsoft.AspNetCore.Http.Metadata.ISkipStatusCodePagesMetadata diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index a0b45ab9d83a..cfd34b2818e6 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -46,6 +46,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + diff --git a/src/Mvc/Mvc.Core/src/RequestSizeLimitAttribute.cs b/src/Mvc/Mvc.Core/src/RequestSizeLimitAttribute.cs index 15c69a613172..e73ab21d4bed 100644 --- a/src/Mvc/Mvc.Core/src/RequestSizeLimitAttribute.cs +++ b/src/Mvc/Mvc.Core/src/RequestSizeLimitAttribute.cs @@ -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. /// [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 IFilterMetadata CreateInstance(IServiceProvider serviceProvider) filter.Bytes = _bytes; return filter; } + + /// + long IRequestSizeLimitMetadata.MaxRequestBodySize => _bytes; } From ef9f9cc33c0f5b3149bb05fd24e03795ff0ae3f8 Mon Sep 17 00:00:00 2001 From: David Acker Date: Sun, 27 Feb 2022 21:08:45 -0500 Subject: [PATCH 25/42] Use updated middleware design --- src/Framework/test/TestData.cs | 2 + .../sample/CustomDecompressionProvider.cs | 6 +- .../src/BrotliDecompressionProvider.cs | 9 +- .../src/DecompressionProviderCollection.cs | 46 -- .../src/DecompressionProviderFactory.cs | 40 -- .../DefaultRequestDecompressionProvider.cs | 81 ++++ .../src/DeflateDecompressionProvider.cs | 9 +- ...ovider.cs => GZipDecompressionProvider.cs} | 9 +- .../src/IDecompressionProvider.cs | 9 +- .../src/IRequestDecompressionProvider.cs | 7 - ...oft.AspNetCore.RequestDecompression.csproj | 4 + .../src/PublicAPI.Unshipped.txt | 34 -- .../RequestDecompressionBuilderExtensions.cs | 2 +- .../RequestDecompressionLoggingExtensions.cs | 31 -- .../src/RequestDecompressionMiddleware.cs | 34 +- .../src/RequestDecompressionOptions.cs | 14 +- .../src/RequestDecompressionProvider.cs | 128 ------ .../RequestDecompressionServiceExtensions.cs | 11 +- .../src/SizeLimitedStream.cs | 101 +++++ ...efaultRequestDecompressionProviderTests.cs | 171 ++++++++ ...uestDecompressionBuilderExtensionsTests.cs | 22 + .../RequestDecompressionMiddlewareTests.cs | 414 ++++++++++++------ .../test/RequestDecompressionOptionsTests.cs | 30 ++ ...uestDecompressionServiceExtensionsTests.cs | 38 ++ .../test/SizeLimitedStreamTests.cs | 107 +++++ 25 files changed, 890 insertions(+), 469 deletions(-) delete mode 100644 src/Middleware/RequestDecompression/src/DecompressionProviderCollection.cs delete mode 100644 src/Middleware/RequestDecompression/src/DecompressionProviderFactory.cs create mode 100644 src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs rename src/Middleware/RequestDecompression/src/{GzipDecompressionProvider.cs => GZipDecompressionProvider.cs} (55%) delete mode 100644 src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs delete mode 100644 src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs create mode 100644 src/Middleware/RequestDecompression/src/SizeLimitedStream.cs create mode 100644 src/Middleware/RequestDecompression/test/DefaultRequestDecompressionProviderTests.cs create mode 100644 src/Middleware/RequestDecompression/test/RequestDecompressionBuilderExtensionsTests.cs create mode 100644 src/Middleware/RequestDecompression/test/RequestDecompressionOptionsTests.cs create mode 100644 src/Middleware/RequestDecompression/test/RequestDecompressionServiceExtensionsTests.cs create mode 100644 src/Middleware/RequestDecompression/test/SizeLimitedStreamTests.cs diff --git a/src/Framework/test/TestData.cs b/src/Framework/test/TestData.cs index 81744043c605..0a79cfb19273 100644 --- a/src/Framework/test/TestData.cs +++ b/src/Framework/test/TestData.cs @@ -75,6 +75,7 @@ static 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 @@ static 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" }, diff --git a/src/Middleware/RequestDecompression/sample/CustomDecompressionProvider.cs b/src/Middleware/RequestDecompression/sample/CustomDecompressionProvider.cs index 2a306e0d9f1c..9bfa11acf207 100644 --- a/src/Middleware/RequestDecompression/sample/CustomDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/sample/CustomDecompressionProvider.cs @@ -7,11 +7,9 @@ namespace RequestDecompressionSample; public class CustomDecompressionProvider : IDecompressionProvider { - public string EncodingName => "custom"; - - public Stream CreateStream(Stream outputStream) + public Stream GetDecompressionStream(Stream stream) { // Create a custom decompression stream wrapper here. - return outputStream; + return stream; } } diff --git a/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs index b7de1f9e59d8..d398d0567d7f 100644 --- a/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs @@ -8,14 +8,11 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// /// Brotli decompression provider. /// -public sealed class BrotliDecompressionProvider : IDecompressionProvider +internal sealed class BrotliDecompressionProvider : IDecompressionProvider { /// - public string EncodingName => "br"; - - /// - public Stream CreateStream(Stream outputStream) + public Stream GetDecompressionStream(Stream stream) { - return new BrotliStream(outputStream, CompressionMode.Decompress); + return new BrotliStream(stream, CompressionMode.Decompress); } } diff --git a/src/Middleware/RequestDecompression/src/DecompressionProviderCollection.cs b/src/Middleware/RequestDecompression/src/DecompressionProviderCollection.cs deleted file mode 100644 index 9dc5971f887f..000000000000 --- a/src/Middleware/RequestDecompression/src/DecompressionProviderCollection.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.ObjectModel; - -namespace Microsoft.AspNetCore.RequestDecompression; - -/// -/// A collection of 's that also allows them to instantiated from an . -/// -public class DecompressionProviderCollection : Collection -{ - /// - /// Adds a type representing an . - /// - /// - /// Provider instances will be created using an . - /// - public void Add() where TDecompressionProvider : IDecompressionProvider - { - Add(typeof(TDecompressionProvider)); - } - - /// - /// Adds a type representing a . - /// - /// Type representing an . - /// - /// Provider instance will be created using an . - /// - public void Add(Type providerType) - { - if (providerType == null) - { - throw new ArgumentNullException(nameof(providerType)); - } - - if (!typeof(IDecompressionProvider).IsAssignableFrom(providerType)) - { - throw new ArgumentException($"The provider must implement {nameof(IDecompressionProvider)}.", nameof(providerType)); - } - - var factory = new DecompressionProviderFactory(providerType); - Add(factory); - } -} diff --git a/src/Middleware/RequestDecompression/src/DecompressionProviderFactory.cs b/src/Middleware/RequestDecompression/src/DecompressionProviderFactory.cs deleted file mode 100644 index 84443f8582c6..000000000000 --- a/src/Middleware/RequestDecompression/src/DecompressionProviderFactory.cs +++ /dev/null @@ -1,40 +0,0 @@ -// 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; - -/// -/// This is a placeholder for the that allows -/// the creation of the given type via an . -/// -internal sealed class DecompressionProviderFactory : IDecompressionProvider -{ - public DecompressionProviderFactory(Type providerType) - { - ProviderType = providerType; - } - - private Type ProviderType { get; } - - public IDecompressionProvider CreateInstance(IServiceProvider serviceProvider) - { - if (serviceProvider == null) - { - throw new ArgumentNullException(nameof(serviceProvider)); - } - - return (IDecompressionProvider)ActivatorUtilities.CreateInstance(serviceProvider, ProviderType, Type.EmptyTypes); - } - - string IDecompressionProvider.EncodingName - { - get { throw new NotSupportedException(); } - } - - Stream IDecompressionProvider.CreateStream(Stream outputStream) - { - throw new NotSupportedException(); - } -} diff --git a/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs new file mode 100644 index 000000000000..8be702447316 --- /dev/null +++ b/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs @@ -0,0 +1,81 @@ +// 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; + +/// +internal sealed partial class DefaultRequestDecompressionProvider : IRequestDecompressionProvider +{ + private readonly ILogger _logger; + private readonly IDictionary _providers; + + public DefaultRequestDecompressionProvider( + ILogger logger, + IOptions 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; + } + + /// + public IDecompressionProvider? GetDecompressionProvider(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)) + { + context.Request.Headers.Remove(HeaderNames.ContentEncoding); + + Log.DecompressingWith(_logger, encodingName.ToLowerInvariant()); + return matchingProvider; + } + + 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); + + [LoggerMessage(4, LogLevel.Debug, "The request will be decompressed with '{ContentEncoding}'.", EventName = "DecompressingWith")] + public static partial void DecompressingWith(ILogger logger, string contentEncoding); + } +} diff --git a/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs index 68b3179cab38..7ddf85478516 100644 --- a/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs @@ -8,14 +8,11 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// /// DEFLATE decompression provider. /// -public sealed class DeflateDecompressionProvider : IDecompressionProvider +internal sealed class DeflateDecompressionProvider : IDecompressionProvider { /// - public string EncodingName => "deflate"; - - /// - public Stream CreateStream(Stream outputStream) + public Stream GetDecompressionStream(Stream stream) { - return new DeflateStream(outputStream, CompressionMode.Decompress); + return new DeflateStream(stream, CompressionMode.Decompress); } } diff --git a/src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/GZipDecompressionProvider.cs similarity index 55% rename from src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs rename to src/Middleware/RequestDecompression/src/GZipDecompressionProvider.cs index 7da30bc6d6a0..687c4046f444 100644 --- a/src/Middleware/RequestDecompression/src/GzipDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/GZipDecompressionProvider.cs @@ -8,14 +8,11 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// /// GZip decompression provider. /// -public sealed class GZipDecompressionProvider : IDecompressionProvider +internal sealed class GZipDecompressionProvider : IDecompressionProvider { /// - public string EncodingName => "gzip"; - - /// - public Stream CreateStream(Stream outputStream) + public Stream GetDecompressionStream(Stream stream) { - return new GZipStream(outputStream, CompressionMode.Decompress); + return new GZipStream(stream, CompressionMode.Decompress); } } diff --git a/src/Middleware/RequestDecompression/src/IDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/IDecompressionProvider.cs index 474a02287cfb..ee795001c710 100644 --- a/src/Middleware/RequestDecompression/src/IDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/IDecompressionProvider.cs @@ -8,15 +8,10 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// public interface IDecompressionProvider { - /// - /// The encoding name used in the 'Content-Encoding' request header. - /// - string EncodingName { get; } - /// /// Creates a new decompression stream. /// - /// The stream where the decompressed data will be written. + /// The compressed data stream. /// The decompression stream. - Stream CreateStream(Stream outputStream); + Stream GetDecompressionStream(Stream stream); } diff --git a/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs index 9ccead793668..c01f2bd0f5f8 100644 --- a/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.RequestDecompression; @@ -17,10 +16,4 @@ public interface IRequestDecompressionProvider /// The . /// A decompression provider or null if there are no acceptable providers. IDecompressionProvider? GetDecompressionProvider(HttpContext context); - - /// - /// Sets , if the feature exists. - /// - /// The . - void SetRequestSizeLimit(HttpContext context); } diff --git a/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj index b77ac11563ad..638de20476cf 100644 --- a/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj +++ b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj @@ -17,4 +17,8 @@ + + + + \ No newline at end of file diff --git a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt index e0dc10e2ebea..aef39aad09f6 100644 --- a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt @@ -1,36 +1,2 @@ #nullable enable -Microsoft.AspNetCore.Builder.RequestDecompressionBuilderExtensions -Microsoft.AspNetCore.Builder.RequestDecompressionServiceExtensions -Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider -Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider.BrotliDecompressionProvider() -> void -Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! -Microsoft.AspNetCore.RequestDecompression.BrotliDecompressionProvider.EncodingName.get -> string! -Microsoft.AspNetCore.RequestDecompression.DecompressionProviderCollection -Microsoft.AspNetCore.RequestDecompression.DecompressionProviderCollection.Add(System.Type! providerType) -> void -Microsoft.AspNetCore.RequestDecompression.DecompressionProviderCollection.Add() -> void -Microsoft.AspNetCore.RequestDecompression.DecompressionProviderCollection.DecompressionProviderCollection() -> void -Microsoft.AspNetCore.RequestDecompression.DeflateDecompressionProvider -Microsoft.AspNetCore.RequestDecompression.DeflateDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! -Microsoft.AspNetCore.RequestDecompression.DeflateDecompressionProvider.DeflateDecompressionProvider() -> void -Microsoft.AspNetCore.RequestDecompression.DeflateDecompressionProvider.EncodingName.get -> string! -Microsoft.AspNetCore.RequestDecompression.GZipDecompressionProvider -Microsoft.AspNetCore.RequestDecompression.GZipDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! -Microsoft.AspNetCore.RequestDecompression.GZipDecompressionProvider.EncodingName.get -> string! -Microsoft.AspNetCore.RequestDecompression.GZipDecompressionProvider.GZipDecompressionProvider() -> void -Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider -Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider.CreateStream(System.IO.Stream! outputStream) -> System.IO.Stream! -Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider.EncodingName.get -> string! -Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider -Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.GetDecompressionProvider(Microsoft.AspNetCore.Http.HttpContext! context) -> Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider? -Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.SetRequestSizeLimit(Microsoft.AspNetCore.Http.HttpContext! context) -> void Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware -Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware.RequestDecompressionMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider! provider) -> void -Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions -Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.MaxRequestBodySize.get -> long -Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.MaxRequestBodySize.set -> void -Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.Providers.get -> Microsoft.AspNetCore.RequestDecompression.DecompressionProviderCollection! -Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.RequestDecompressionOptions() -> void -static Microsoft.AspNetCore.Builder.RequestDecompressionBuilderExtensions.UseRequestDecompression(this Microsoft.AspNetCore.Builder.IApplicationBuilder! builder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! -static Microsoft.AspNetCore.Builder.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.AspNetCore.Builder.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs index 144c0bd4eb4e..9a9d919ea0a3 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs @@ -16,7 +16,7 @@ public static class RequestDecompressionBuilderExtensions /// The instance this method extends. public static IApplicationBuilder UseRequestDecompression(this IApplicationBuilder builder) { - if (builder == null) + if (builder is null) { throw new ArgumentNullException(nameof(builder)); } diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs deleted file mode 100644 index 482c69044c32..000000000000 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionLoggingExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -// 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.Features; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.RequestDecompression; - -internal static partial class RequestDecompressionLoggingExtensions -{ - [LoggerMessage(1, LogLevel.Trace, "The Content-Encoding header is missing or empty. Skipping request decompression.", EventName = "NoContentEncoding")] - public static partial void NoContentEncoding(this ILogger logger); - - [LoggerMessage(2, LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings.", EventName = "MultipleContentEncodingsSpecified")] - public static partial void MultipleContentEncodingsSpecified(this ILogger logger); - - [LoggerMessage(3, LogLevel.Debug, "No matching request decompression provider found.", EventName = "NoDecompressionProvider")] - public static partial void NoDecompressionProvider(this ILogger logger); - - [LoggerMessage(4, LogLevel.Debug, "The request will be decompressed with '{Provider}'.", EventName = "DecompressingWith")] - public static partial void DecompressingWith(this ILogger logger, string provider); - - [LoggerMessage(5, 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(this ILogger logger); - - [LoggerMessage(6, 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(this ILogger logger); - - [LoggerMessage(7, LogLevel.Debug, "The maximum request body size has been set to {RequestSize}.", EventName = "MaxRequestBodySizeSet")] - public static partial void MaxRequestBodySizeSet(this ILogger logger, long requestSize); -} diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index defd4ca6c949..232dac9ea538 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -2,7 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; -using Microsoft.Net.Http.Headers; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Metadata; namespace Microsoft.AspNetCore.RequestDecompression; @@ -23,12 +24,12 @@ public RequestDecompressionMiddleware( RequestDelegate next, IRequestDecompressionProvider provider) { - if (next == null) + if (next is null) { throw new ArgumentNullException(nameof(next)); } - if (provider == null) + if (provider is null) { throw new ArgumentNullException(nameof(provider)); } @@ -44,33 +45,32 @@ public RequestDecompressionMiddleware( /// A task that represents the execution of this middleware. public Task Invoke(HttpContext context) { - var decompressionProvider = _provider.GetDecompressionProvider(context); - if (decompressionProvider == null) + var provider = _provider.GetDecompressionProvider(context); + if (provider is null) { return _next(context); } - return InvokeCore(context, decompressionProvider); + return InvokeCore(context, provider); } - private async Task InvokeCore(HttpContext context, IDecompressionProvider decompressionProvider) + private async Task InvokeCore(HttpContext context, IDecompressionProvider provider) { - var originalBody = context.Request.Body; - - await using var decompressionStream = decompressionProvider.CreateStream(originalBody); - - context.Request.Body = decompressionStream; - context.Request.Headers.Remove(HeaderNames.ContentEncoding); - - _provider.SetRequestSizeLimit(context); - + var request = context.Request.Body; try { + await using var stream = provider.GetDecompressionStream(request); + + var sizeLimit = + context.GetEndpoint()?.Metadata?.GetMetadata()?.MaxRequestBodySize + ?? context.Features.GetRequiredFeature().MaxRequestBodySize; + + context.Request.Body = new SizeLimitedStream(stream, sizeLimit); await _next(context); } finally { - context.Request.Body = originalBody; + context.Request.Body = request; } } } diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs index 9ccb659fdadf..b6821e90469b 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs @@ -6,15 +6,15 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// /// Options for the HTTP request decompression middleware. /// -public class RequestDecompressionOptions +public sealed class RequestDecompressionOptions { /// /// The types to use for request decompression. /// - public DecompressionProviderCollection Providers { get; } = new DecompressionProviderCollection(); - - /// - /// The maximum allowed size of the decompressed request body in bytes. - /// - public long MaxRequestBodySize { get; set; } + public IDictionary DecompressionProviders { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["br"] = new BrotliDecompressionProvider(), + ["deflate"] = new DeflateDecompressionProvider(), + ["gzip"] = new GZipDecompressionProvider() + }; } diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs deleted file mode 100644 index 8e98ddc71681..000000000000 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionProvider.cs +++ /dev/null @@ -1,128 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Linq; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.RequestDecompression; - -/// -internal sealed class RequestDecompressionProvider : IRequestDecompressionProvider -{ - private readonly RequestDecompressionOptions _options; - private readonly ILogger _logger; - - private readonly IReadOnlyDictionary _providers; - - /// - /// If no decompression providers are specified then all default providers will be registered. - /// - /// Services to use when instantiating decompression providers. - /// An instance of . - /// The options for this instance. - public RequestDecompressionProvider( - IServiceProvider services, - ILogger logger, - IOptions options) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - var registeredProviders = _options.Providers.ToArray(); - if (registeredProviders.Length == 0) - { - registeredProviders = new IDecompressionProvider[] - { - new BrotliDecompressionProvider(), - new DeflateDecompressionProvider(), - new GZipDecompressionProvider() - }; - } - - var providers = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var provider in registeredProviders) - { - if (provider is DecompressionProviderFactory factory) - { - var providerInstance = factory.CreateInstance(services); - providers[providerInstance.EncodingName] = providerInstance; - } - else - { - providers[provider.EncodingName] = provider; - } - } - - _providers = providers; - } - - /// - public IDecompressionProvider? GetDecompressionProvider(HttpContext context) - { - // e.g. Content-Encoding: br, deflate, gzip - var encodings = context.Request.Headers.ContentEncoding; - - if (StringValues.IsNullOrEmpty(encodings)) - { - _logger.NoContentEncoding(); - return null; - } - - if (encodings.Count > 1) - { - _logger.MultipleContentEncodingsSpecified(); - return null; - } - - string encodingName = encodings!; - - if (_providers.TryGetValue(encodingName, out var matchingProvider)) - { - _logger.DecompressingWith(matchingProvider.EncodingName); - return matchingProvider; - } - - _logger.NoDecompressionProvider(); - return null; - } - - /// - public void SetRequestSizeLimit(HttpContext context) - { - var maxRequestBodySizeFeature = context.Features.Get(); - - if (maxRequestBodySizeFeature == null) - { - _logger.FeatureNotFound(); - } - else if (maxRequestBodySizeFeature.IsReadOnly) - { - _logger.FeatureIsReadOnly(); - } - else - { - maxRequestBodySizeFeature.MaxRequestBodySize = _options.MaxRequestBodySize; - _logger.MaxRequestBodySizeSet(_options.MaxRequestBodySize); - } - } -} diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs index c4a00b098446..7b314c657170 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs @@ -19,12 +19,12 @@ public static class RequestDecompressionServiceExtensions /// The . public static IServiceCollection AddRequestDecompression(this IServiceCollection services) { - if (services == null) + if (services is null) { throw new ArgumentNullException(nameof(services)); } - services.TryAddSingleton(); + services.TryAddSingleton(); return services; } @@ -36,17 +36,18 @@ public static IServiceCollection AddRequestDecompression(this IServiceCollection /// The . public static IServiceCollection AddRequestDecompression(this IServiceCollection services, Action configureOptions) { - if (services == null) + if (services is null) { throw new ArgumentNullException(nameof(services)); } - if (configureOptions == null) + + if (configureOptions is null) { throw new ArgumentNullException(nameof(configureOptions)); } services.Configure(configureOptions); - services.TryAddSingleton(); + services.TryAddSingleton(); return services; } } diff --git a/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs b/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs new file mode 100644 index 000000000000..d2dd978b060e --- /dev/null +++ b/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs @@ -0,0 +1,101 @@ +// 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 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 => false; + + public override bool CanWrite => false; + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + 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) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + return TaskToApm.Begin(ReadAsync(buffer, offset, count, CancellationToken.None), callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + return TaskToApm.End(asyncResult); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return await ReadAsync(buffer.AsMemory(offset, count), cancellationToken); + } + + public override async ValueTask ReadAsync(Memory 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; + } +} diff --git a/src/Middleware/RequestDecompression/test/DefaultRequestDecompressionProviderTests.cs b/src/Middleware/RequestDecompression/test/DefaultRequestDecompressionProviderTests.cs new file mode 100644 index 000000000000..a3430af76d29 --- /dev/null +++ b/src/Middleware/RequestDecompression/test/DefaultRequestDecompressionProviderTests.cs @@ -0,0 +1,171 @@ +// 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; + +namespace Microsoft.AspNetCore.RequestDecompression.Tests; + +public class DefaultRequestDecompressionProviderTests +{ + [Theory] + [InlineData("br", typeof(BrotliDecompressionProvider))] + [InlineData("BR", typeof(BrotliDecompressionProvider))] + 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.GetDecompressionProvider(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.GetDecompressionProvider(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.GetDecompressionProvider(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.GetDecompressionProvider(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 options = null; + + // Act + Assert + Assert.Throws(() => + { + new DefaultRequestDecompressionProvider(logger, options); + }); + } + + [Fact] + public void Ctor_NullOptions_Throws() + { + // Arrange + ILogger logger = null; + var options = Options.Create(new RequestDecompressionOptions()); + + // Act + Assert + Assert.Throws(() => + { + new DefaultRequestDecompressionProvider(logger, options); + }); + } + + private static (ILogger, TestSink) GetTestLogger() + { + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var logger = loggerFactory.CreateLogger(); + + return (logger, sink); + } + + private static void AssertLog(WriteContext log, LogLevel level, string message) + { + Assert.Equal(level, log.LogLevel); + Assert.Equal(message, log.State.ToString()); + } +} diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionBuilderExtensionsTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionBuilderExtensionsTests.cs new file mode 100644 index 000000000000..b30aaf0122e4 --- /dev/null +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionBuilderExtensionsTests.cs @@ -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(() => + { + builder.UseRequestDecompression(); + }); + } +} diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs index 5cabaf88f9c1..d95bb7f89eb0 100644 --- a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs @@ -6,7 +6,9 @@ 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; @@ -27,131 +29,152 @@ private static byte[] GetUncompressedContent(string input = TestRequestBodyData) private static async Task GetCompressedContent( Func compressorDelegate, - string input = TestRequestBodyData) + byte[] uncompressedBytes) { - var bytes = GetUncompressedContent(input); - await using var uncompressedContent = new MemoryStream(bytes); + await using var uncompressedStream = new MemoryStream(uncompressedBytes); - await using var compressedContent = new MemoryStream(); - await using (var compressor = compressorDelegate(compressedContent)) + await using var compressedStream = new MemoryStream(); + await using (var compressor = compressorDelegate(compressedStream)) { - uncompressedContent.CopyTo(compressor); + await uncompressedStream.CopyToAsync(compressor); } - return compressedContent.ToArray(); + return compressedStream.ToArray(); } - private static async Task GetBrotliCompressedContent(string input = TestRequestBodyData) + private static async Task GetBrotliCompressedContent(byte[] uncompressedBytes) { static Stream compressorDelegate(Stream compressedContent) => new BrotliStream(compressedContent, CompressionMode.Compress); - return await GetCompressedContent(compressorDelegate, input); + return await GetCompressedContent(compressorDelegate, uncompressedBytes); } - private static async Task GetDeflateCompressedContent(string input = TestRequestBodyData) + private static async Task GetDeflateCompressedContent(byte[] uncompressedBytes) { static Stream compressorDelegate(Stream compressedContent) => new DeflateStream(compressedContent, CompressionMode.Compress); - return await GetCompressedContent(compressorDelegate, input); + return await GetCompressedContent(compressorDelegate, uncompressedBytes); } - private static async Task GetGZipCompressedContent(string input = TestRequestBodyData) + private static async Task GetGZipCompressedContent(byte[] uncompressedBytes) { static Stream compressorDelegate(Stream compressedContent) => new GZipStream(compressedContent, CompressionMode.Compress); - return await GetCompressedContent(compressorDelegate, input); + return await GetCompressedContent(compressorDelegate, uncompressedBytes); } - [Theory] - [InlineData("br")] - [InlineData("BR")] - public async Task Request_ContentEncodingBrotli_Decompressed(string contentEncoding) + [Fact] + public async Task Request_ContentEncodingBrotli_Decompressed() { - var compressedContent = await GetBrotliCompressedContent(); + // Arrange + var contentEncoding = "br"; + var uncompressedBytes = GetUncompressedContent(); + var compressedBytes = await GetBrotliCompressedContent(uncompressedBytes); - var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, new[] { contentEncoding }); + // Act + var (logMessages, decompressedBytes) = await InvokeMiddleware(compressedBytes, new[] { contentEncoding }); + // Assert AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant()); - Assert.Equal(GetUncompressedContent(), decompressedContent); + Assert.Equal(uncompressedBytes, decompressedBytes); } - [Theory] - [InlineData("deflate")] - [InlineData("DEFLATE")] - public async Task Request_ContentEncodingDeflate_Decompressed(string contentEncoding) + [Fact] + public async Task Request_ContentEncodingDeflate_Decompressed() { - var compressedContent = await GetDeflateCompressedContent(); + // Arrange + var contentEncoding = "deflate"; + var uncompressedBytes = GetUncompressedContent(); + var compressedBytes = await GetDeflateCompressedContent(uncompressedBytes); - var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, new[] { contentEncoding }); + // Act + var (logMessages, decompressedBytes) = await InvokeMiddleware(compressedBytes, new[] { contentEncoding }); + // Assert AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant()); - Assert.Equal(GetUncompressedContent(), decompressedContent); + Assert.Equal(uncompressedBytes, decompressedBytes); } - [Theory] - [InlineData("gzip")] - [InlineData("GZIP")] - public async Task Request_ContentEncodingGzip_Decompressed(string contentEncoding) + [Fact] + public async Task Request_ContentEncodingGzip_Decompressed() { - var compressedContent = await GetGZipCompressedContent(); + // Arrange + var contentEncoding = "gzip"; + var uncompressedBytes = GetUncompressedContent(); + var compressedBytes = await GetGZipCompressedContent(uncompressedBytes); - var (logMessages, decompressedContent) = await InvokeMiddleware(compressedContent, new[] { contentEncoding }); + // Act + var (logMessages, decompressedBytes) = await InvokeMiddleware(compressedBytes, new[] { contentEncoding }); + // Assert AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant()); - Assert.Equal(GetUncompressedContent(), decompressedContent); + Assert.Equal(uncompressedBytes, decompressedBytes); } [Fact] public async Task Request_NoContentEncoding_NotDecompressed() { - var uncompressedContent = GetUncompressedContent(); + // Arrange + var uncompressedBytes = GetUncompressedContent(); - var (logMessages, outputContent) = await InvokeMiddleware(uncompressedContent); + // Act + var (logMessages, outputBytes) = await InvokeMiddleware(uncompressedBytes); + // Assert var logMessage = Assert.Single(logMessages); - AssertLog(logMessage, LogLevel.Trace, "The Content-Encoding header is missing or empty. Skipping request decompression."); - Assert.Equal(uncompressedContent, outputContent); + 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() { - var inputContent = GetUncompressedContent(); + // Arrange + var uncompressedBytes = GetUncompressedContent(); + var compressedBytes = await GetGZipCompressedContent(uncompressedBytes); var contentEncoding = "custom"; - var (logMessages, outputContent) = await InvokeMiddleware(inputContent, new[] { contentEncoding }); + // Act + var (logMessages, outputBytes) = await InvokeMiddleware(compressedBytes, new[] { contentEncoding }); + // Assert AssertNoDecompressionProviderLog(logMessages); - Assert.Equal(GetUncompressedContent(), outputContent); + Assert.Equal(compressedBytes, outputBytes); } [Fact] public async Task Request_MultipleContentEncodings_NotDecompressed() { - var inputContent = GetUncompressedContent(); + // Arrange + var uncompressedBytes = GetUncompressedContent(); + var inputBytes = await GetGZipCompressedContent(uncompressedBytes); var contentEncodings = new[] { "br", "gzip" }; - var (logMessages, outputContent) = await InvokeMiddleware(inputContent, contentEncodings); + // 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(GetUncompressedContent(), outputContent); + Assert.Equal(inputBytes, outputBytes); } [Fact] public async Task Request_MiddlewareAddedMultipleTimes_OnlyDecompressedOnce() { - var compressedContent = await GetGZipCompressedContent(); + // Arrange + var uncompressedBytes = GetUncompressedContent(); + var compressedBytes = await GetGZipCompressedContent(uncompressedBytes); var contentEncoding = "gzip"; - var outputContent = Array.Empty(); + var decompressedBytes = Array.Empty(); var sink = new TestSink( - TestSink.EnableWithTypeName, - TestSink.EnableWithTypeName); + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); var loggerFactory = new TestLoggerFactory(sink, enabled: true); using var host = new HostBuilder() @@ -166,13 +189,19 @@ public async Task Request_MiddlewareAddedMultipleTimes_OnlyDecompressedOnce() }) .Configure(app => { + app.Use((context, next) => + { + context.Features.Set( + 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); - outputContent = ms.ToArray(); + decompressedBytes = ms.ToArray(); }); }); }).Build(); @@ -183,19 +212,20 @@ public async Task Request_MiddlewareAddedMultipleTimes_OnlyDecompressedOnce() var client = server.CreateClient(); using var request = new HttpRequestMessage(HttpMethod.Post, ""); - request.Content = new ByteArrayContent(compressedContent); + request.Content = new ByteArrayContent(compressedBytes); request.Content.Headers.ContentEncoding.Add(contentEncoding); + // Act await client.SendAsync(request); + // Assert var logMessages = sink.Writes.ToList(); - Assert.Equal(3, logMessages.Count); + Assert.Equal(2, logMessages.Count); AssertLog(logMessages.First(), LogLevel.Debug, $"The request will be decompressed with '{contentEncoding}'."); - AssertLog(logMessages.Skip(1).First(), LogLevel.Warning, $"A request body size limit could not be applied. This server does not support the {nameof(IHttpMaxRequestBodySizeFeature)}."); - AssertLog(logMessages.Skip(2).First(), LogLevel.Trace, "The Content-Encoding header is missing or empty. Skipping request decompression."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Trace, "The Content-Encoding header is empty or not specified. Skipping request decompression."); - Assert.Equal(GetUncompressedContent(), outputContent); + Assert.Equal(uncompressedBytes, decompressedBytes); } [Theory] @@ -203,12 +233,18 @@ public async Task Request_MiddlewareAddedMultipleTimes_OnlyDecompressedOnce() [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(); + var sink = new TestSink( - TestSink.EnableWithTypeName, - TestSink.EnableWithTypeName); + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); var loggerFactory = new TestLoggerFactory(sink, enabled: true); using var host = new HostBuilder() @@ -223,11 +259,20 @@ public async Task Request_Decompressed_ContentEncodingHeaderRemoved(bool isDecom }) .Configure(app => { + app.Use((context, next) => + { + context.Features.Set( + new FakeHttpMaxRequestBodySizeFeature()); + return next(context); + }); app.UseRequestDecompression(); - app.Run(context => + app.Run(async context => { contentEncodingHeader = context.Request.Headers.ContentEncoding; - return Task.CompletedTask; + + await using var ms = new MemoryStream(); + await context.Request.Body.CopyToAsync(ms, context.RequestAborted); + outputBytes = ms.ToArray(); }); }); }).Build(); @@ -238,66 +283,73 @@ public async Task Request_Decompressed_ContentEncodingHeaderRemoved(bool isDecom var client = server.CreateClient(); using var request = new HttpRequestMessage(HttpMethod.Post, ""); - request.Content = new ByteArrayContent(new byte[1]); + request.Content = new ByteArrayContent(compressedBytes); request.Content.Headers.ContentEncoding.Add(contentEncoding); + // Act await client.SendAsync(request); + // Assert var logMessages = sink.Writes.ToList(); if (isDecompressed) { - AssertDecompressedWithLog(logMessages, contentEncoding); Assert.Empty(contentEncodingHeader); + + AssertDecompressedWithLog(logMessages, contentEncoding); + Assert.Equal(uncompressedBytes, outputBytes); } else { - AssertNoDecompressionProviderLog(logMessages); Assert.Equal(contentEncoding, contentEncodingHeader); + + AssertNoDecompressionProviderLog(logMessages); + Assert.Equal(compressedBytes, outputBytes); } } - [Theory] - [InlineData("gzip", true)] - [InlineData("br", false)] - public async Task Options_IncludesProviders_OnlyUsesRegisteredProviders(string contentEncoding, bool explicitlyRegistered) + [Fact] + public async Task Options_RegisterCustomDecompressionProvider() { - var compressedContent = await GetGZipCompressedContent(); + // Arrange + var uncompressedBytes = GetUncompressedContent(); + var compressedBytes = await GetGZipCompressedContent(uncompressedBytes); + var contentEncoding = "custom"; - var (logMessages, _) = + // Act + var (logMessages, decompressedBytes) = await InvokeMiddleware( - compressedContent, + compressedBytes, new[] { contentEncoding }, configure: (RequestDecompressionOptions options) => { - options.Providers.Add(); + options.DecompressionProviders.Add(contentEncoding, new CustomDecompressionProvider()); }); - if (explicitlyRegistered) - { - AssertDecompressedWithLog(logMessages, contentEncoding); - } - else - { - AssertNoDecompressionProviderLog(logMessages); - } + // Assert + AssertDecompressedWithLog(logMessages, contentEncoding); + Assert.Equal(uncompressedBytes, decompressedBytes); } [Theory] [InlineData(true)] [InlineData(false)] - public async Task Request_ServerSupportsMaxBodySizeFeature_RequestSizeLimitSet(bool supportsFeature) + public async Task Endpoint_HasRequestSizeLimit_UsedForRequest(bool exceedsLimit) { + // Arrange + long attributeSizeLimit = 10; + long featureSizeLimit = 5; + var contentEncoding = "gzip"; - long maxRequestBodySize = 100; + var uncompressedBytes = new byte[attributeSizeLimit + (exceedsLimit ? 1 : 0)]; + var compressedBytes = await GetGZipCompressedContent(uncompressedBytes); - var fakeMaxRequestBodySize = supportsFeature - ? new FakeHttpMaxRequestBodySizeFeature() - : null; + var decompressedBytes = Array.Empty(); + Exception exception = null; var sink = new TestSink( - TestSink.EnableWithTypeName, - TestSink.EnableWithTypeName); + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); var loggerFactory = new TestLoggerFactory(sink, enabled: true); using var host = new HostBuilder() @@ -307,21 +359,33 @@ public async Task Request_ServerSupportsMaxBodySizeFeature_RequestSizeLimitSet(b .UseTestServer() .ConfigureServices(services => { - services.AddRequestDecompression(options => - { - options.MaxRequestBodySize = maxRequestBodySize; - }); + services.AddRequestDecompression(); services.AddSingleton(loggerFactory); }) .Configure(app => { app.Use((context, next) => { - context.Features.Set(fakeMaxRequestBodySize); + context.Features.Set( + GetFakeEndpointFeature(attributeSizeLimit)); + context.Features.Set( + new FakeHttpMaxRequestBodySizeFeature(featureSizeLimit)); + return next(context); }); app.UseRequestDecompression(); - app.Run(context => Task.CompletedTask); + 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(); @@ -331,38 +395,47 @@ public async Task Request_ServerSupportsMaxBodySizeFeature_RequestSizeLimitSet(b var client = server.CreateClient(); using var request = new HttpRequestMessage(HttpMethod.Post, ""); - request.Content = new ByteArrayContent(new byte[1]); + 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); - Assert.Equal(2, logMessages.Count); - AssertLog(logMessages.First(), LogLevel.Debug, $"The request will be decompressed with '{contentEncoding}'."); - - if (supportsFeature) + if (exceedsLimit) { - AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, $"The maximum request body size has been set to {maxRequestBodySize}."); - Assert.Equal(maxRequestBodySize, fakeMaxRequestBodySize.MaxRequestBodySize); + Assert.NotNull(exception); + Assert.IsAssignableFrom(exception); + Assert.Equal("The maximum number of bytes have been read.", exception.Message); } else { - AssertLog(logMessages.Skip(1).First(), LogLevel.Warning, $"A request body size limit could not be applied. This server does not support the {nameof(IHttpMaxRequestBodySizeFeature)}."); + Assert.Null(exception); + Assert.Equal(uncompressedBytes, decompressedBytes); } } - [Fact] - public async Task Request_ReadOnlyMaxBodySizeFeature_RequestSizeLimitNotSet() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Feature_HasRequestSizeLimit_UsedForRequest(bool exceedsLimit) { + // Arrange + long featureSizeLimit = 10; + var contentEncoding = "gzip"; - long maxRequestBodySize = 100; + var uncompressedBytes = new byte[featureSizeLimit + (exceedsLimit ? 1 : 0)]; + var compressedBytes = await GetGZipCompressedContent(uncompressedBytes); - FakeHttpMaxRequestBodySizeFeature fakeMaxRequestBodySize = null; + var decompressedBytes = Array.Empty(); + Exception exception = null; var sink = new TestSink( - TestSink.EnableWithTypeName, - TestSink.EnableWithTypeName); + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); var loggerFactory = new TestLoggerFactory(sink, enabled: true); using var host = new HostBuilder() @@ -372,22 +445,31 @@ public async Task Request_ReadOnlyMaxBodySizeFeature_RequestSizeLimitNotSet() .UseTestServer() .ConfigureServices(services => { - services.AddRequestDecompression(options => - { - options.MaxRequestBodySize = maxRequestBodySize; - }); + services.AddRequestDecompression(); services.AddSingleton(loggerFactory); }) .Configure(app => { app.Use((context, next) => { - fakeMaxRequestBodySize = new FakeHttpMaxRequestBodySizeFeature(isReadOnly: true); - context.Features.Set(fakeMaxRequestBodySize); + context.Features.Set( + new FakeHttpMaxRequestBodySizeFeature(featureSizeLimit)); + return next(context); }); app.UseRequestDecompression(); - app.Run(context => Task.CompletedTask); + 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(); @@ -397,18 +479,62 @@ public async Task Request_ReadOnlyMaxBodySizeFeature_RequestSizeLimitNotSet() var client = server.CreateClient(); using var request = new HttpRequestMessage(HttpMethod.Post, ""); - request.Content = new ByteArrayContent(new byte[1]); + 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); - Assert.Equal(2, logMessages.Count); - AssertLog(logMessages.First(), LogLevel.Debug, $"The request will be decompressed with '{contentEncoding}'."); - AssertLog(logMessages.Skip(1).First(), LogLevel.Warning, $"A request body size limit could not be applied. The {nameof(IHttpMaxRequestBodySizeFeature)} for the server is read-only."); + if (exceedsLimit) + { + Assert.NotNull(exception); + Assert.IsAssignableFrom(exception); + Assert.Equal("The maximum number of bytes have been read.", exception.Message); + } + else + { + Assert.Null(exception); + Assert.Equal(uncompressedBytes, decompressedBytes); + } + } + + [Fact] + public void Ctor_NullRequestDelegate_Throws() + { + // Arrange + RequestDelegate requestDelegate = null; + var provider = new FakeRequestDecompressionProvider(); - Assert.NotEqual(maxRequestBodySize, fakeMaxRequestBodySize.MaxRequestBodySize); + // Act + Assert + Assert.Throws(() => + { + new RequestDecompressionMiddleware(requestDelegate, provider); + }); + } + + private class FakeRequestDecompressionProvider : IRequestDecompressionProvider + { +#nullable enable + public IDecompressionProvider? GetDecompressionProvider(HttpContext context) => null; +#nullable disable + } + + [Fact] + public void Ctor_NullRequestDecompressionProvider_Throws() + { + // Arrange + static Task requestDelegate(HttpContext context) => Task.FromResult(context); + IRequestDecompressionProvider provider = null; + + // Act + Assert + Assert.Throws(() => + { + new RequestDecompressionMiddleware(requestDelegate, provider); + }); } private static void AssertLog(WriteContext log, LogLevel level, string message) @@ -419,9 +545,8 @@ private static void AssertLog(WriteContext log, LogLevel level, string message) private static void AssertDecompressedWithLog(List logMessages, string encoding) { - Assert.Equal(2, logMessages.Count); - AssertLog(logMessages.First(), LogLevel.Debug, $"The request will be decompressed with '{encoding}'."); - AssertLog(logMessages.Skip(1).First(), LogLevel.Warning, $"A request body size limit could not be applied. This server does not support the {nameof(IHttpMaxRequestBodySizeFeature)}."); + var logMessage = Assert.Single(logMessages); + AssertLog(logMessage, LogLevel.Debug, $"The request will be decompressed with '{encoding}'."); } private static void AssertNoDecompressionProviderLog(List logMessages) @@ -436,8 +561,8 @@ private static void AssertNoDecompressionProviderLog(List logMessa Action configure = null) { var sink = new TestSink( - TestSink.EnableWithTypeName, - TestSink.EnableWithTypeName); + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); var loggerFactory = new TestLoggerFactory(sink, enabled: true); var outputContent = Array.Empty(); @@ -454,6 +579,12 @@ private static void AssertNoDecompressionProviderLog(List logMessa }) .Configure(app => { + app.Use((context, next) => + { + context.Features.Set( + new FakeHttpMaxRequestBodySizeFeature()); + return next(context); + }); app.UseRequestDecompression(); app.Run(async context => { @@ -484,16 +615,51 @@ private static void AssertNoDecompressionProviderLog(List logMessa 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) + public FakeHttpMaxRequestBodySizeFeature(long? maxRequestBodySize = null) { MaxRequestBodySize = maxRequestBodySize; - IsReadOnly = isReadOnly; } - public bool IsReadOnly { get; } + public bool IsReadOnly => false; public long? MaxRequestBodySize { get; set; } } diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionOptionsTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionOptionsTests.cs new file mode 100644 index 000000000000..eb05050b2dee --- /dev/null +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionOptionsTests.cs @@ -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(brotliProvider); + + var deflateProvider = Assert.Contains("deflate", providers); + Assert.IsType(deflateProvider); + + var gzipProvider = Assert.Contains("gzip", providers); + Assert.IsType(gzipProvider); + } +} diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionServiceExtensionsTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionServiceExtensionsTests.cs new file mode 100644 index 000000000000..4ca1dfd35f22 --- /dev/null +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionServiceExtensionsTests.cs @@ -0,0 +1,38 @@ +// 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; +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(() => + { + serviceCollection.AddRequestDecompression(configureOptions); + }); + } + + [Fact] + public void AddRequestDecompression_NullConfigureOptions_Throws() + { + // Arrange + var serviceCollection = new ServiceCollection(); + Action configureOptions = null; + + // Act + Assert + Assert.Throws(() => + { + serviceCollection.AddRequestDecompression(configureOptions); + }); + } +} diff --git a/src/Middleware/RequestDecompression/test/SizeLimitedStreamTests.cs b/src/Middleware/RequestDecompression/test/SizeLimitedStreamTests.cs new file mode 100644 index 000000000000..a4e086abfbb9 --- /dev/null +++ b/src/Middleware/RequestDecompression/test/SizeLimitedStreamTests.cs @@ -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(() => + { + 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(exception); + Assert.Equal("The maximum number of bytes have been read.", exception.Message); + } + else + { + Assert.Null(exception); + } + } +} From d0e3ea51c7c4e2b5173df5430e1b64f4d156f08a Mon Sep 17 00:00:00 2001 From: David Acker Date: Mon, 28 Feb 2022 19:53:44 -0500 Subject: [PATCH 26/42] Address PR feedback --- .../RequestDecompression/sample/Startup.cs | 6 +---- .../src/BrotliDecompressionProvider.cs | 2 +- .../src/DeflateDecompressionProvider.cs | 2 +- .../src/GZipDecompressionProvider.cs | 2 +- ...oft.AspNetCore.RequestDecompression.csproj | 4 ++++ .../src/Properties/AssemblyInfo.cs | 6 ----- .../src/PublicAPI.Unshipped.txt | 13 ++++++++++- .../RequestDecompressionBuilderExtensions.cs | 2 +- .../src/RequestDecompressionMiddleware.cs | 4 ++-- .../RequestDecompressionServiceExtensions.cs | 5 ++-- .../src/SizeLimitedStream.cs | 23 +++++++++++-------- .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 1 - 12 files changed, 38 insertions(+), 32 deletions(-) delete mode 100644 src/Middleware/RequestDecompression/src/Properties/AssemblyInfo.cs diff --git a/src/Middleware/RequestDecompression/sample/Startup.cs b/src/Middleware/RequestDecompression/sample/Startup.cs index 9a52496adfcc..55d99ab8ec2f 100644 --- a/src/Middleware/RequestDecompression/sample/Startup.cs +++ b/src/Middleware/RequestDecompression/sample/Startup.cs @@ -1,8 +1,6 @@ // 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 Startup @@ -11,15 +9,13 @@ public void ConfigureServices(IServiceCollection services) { services.AddRequestDecompression(options => { - options.Providers.Add(); - options.Providers.Add(); + options.DecompressionProviders.Add("custom", new CustomDecompressionProvider()); }); } public void Configure(IApplicationBuilder app) { app.UseRequestDecompression(); - app.Map("/test", testApp => { testApp.Run(async context => diff --git a/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs index d398d0567d7f..7b3b28837fc2 100644 --- a/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs @@ -13,6 +13,6 @@ internal sealed class BrotliDecompressionProvider : IDecompressionProvider /// public Stream GetDecompressionStream(Stream stream) { - return new BrotliStream(stream, CompressionMode.Decompress); + return new BrotliStream(stream, CompressionMode.Decompress, leaveOpen: true); } } diff --git a/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs index 7ddf85478516..6291f15b2d71 100644 --- a/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs @@ -13,6 +13,6 @@ internal sealed class DeflateDecompressionProvider : IDecompressionProvider /// public Stream GetDecompressionStream(Stream stream) { - return new DeflateStream(stream, CompressionMode.Decompress); + return new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true); } } diff --git a/src/Middleware/RequestDecompression/src/GZipDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/GZipDecompressionProvider.cs index 687c4046f444..74ac6835f81c 100644 --- a/src/Middleware/RequestDecompression/src/GZipDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/GZipDecompressionProvider.cs @@ -13,6 +13,6 @@ internal sealed class GZipDecompressionProvider : IDecompressionProvider /// public Stream GetDecompressionStream(Stream stream) { - return new GZipStream(stream, CompressionMode.Decompress); + return new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true); } } diff --git a/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj index 638de20476cf..d3cf7ef9da12 100644 --- a/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj +++ b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj @@ -17,6 +17,10 @@ + + + + diff --git a/src/Middleware/RequestDecompression/src/Properties/AssemblyInfo.cs b/src/Middleware/RequestDecompression/src/Properties/AssemblyInfo.cs deleted file mode 100644 index 180f70092df5..000000000000 --- a/src/Middleware/RequestDecompression/src/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.AspNetCore.RequestDecompression.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt index aef39aad09f6..be2a1f7958d7 100644 --- a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt @@ -1,2 +1,13 @@ #nullable enable -Microsoft.AspNetCore.RequestDecompression.RequestDecompressionMiddleware +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.GetDecompressionProvider(Microsoft.AspNetCore.Http.HttpContext! context) -> Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider? +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions +Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.DecompressionProviders.get -> System.Collections.Generic.IDictionary! +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! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs index 9a9d919ea0a3..576e6e9eb188 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Builder; public static class RequestDecompressionBuilderExtensions { /// - /// Adds middleware for dynamically decompressing HTTP requests. + /// Adds middleware for dynamically decompressing HTTP request bodies. /// /// The instance this method extends. public static IApplicationBuilder UseRequestDecompression(this IApplicationBuilder builder) diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index 232dac9ea538..c4fe1842271f 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// /// Enables HTTP request decompression. /// -public class RequestDecompressionMiddleware +internal sealed class RequestDecompressionMiddleware { private readonly RequestDelegate _next; private readonly IRequestDecompressionProvider _provider; @@ -63,7 +63,7 @@ private async Task InvokeCore(HttpContext context, IDecompressionProvider provid var sizeLimit = context.GetEndpoint()?.Metadata?.GetMetadata()?.MaxRequestBodySize - ?? context.Features.GetRequiredFeature().MaxRequestBodySize; + ?? context.Features.Get()?.MaxRequestBodySize; context.Request.Body = new SizeLimitedStream(stream, sizeLimit); await _next(context); diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs index 7b314c657170..0690c7d457d9 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs @@ -2,13 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.RequestDecompression; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace Microsoft.AspNetCore.Builder; +namespace Microsoft.Extensions.DependencyInjection; /// -/// Extension methods for the RequestDecompression middleware. +/// Extension methods for the request decompression middleware. /// public static class RequestDecompressionServiceExtensions { diff --git a/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs b/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs index d2dd978b060e..46f718a298b3 100644 --- a/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs +++ b/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.RequestDecompression; -internal class SizeLimitedStream : Stream +internal sealed class SizeLimitedStream : Stream { private readonly Stream _innerStream; private readonly long? _sizeLimit; @@ -23,19 +23,22 @@ public SizeLimitedStream(Stream innerStream, long? sizeLimit) public override bool CanRead => _innerStream.CanRead; - public override bool CanSeek => false; + public override bool CanSeek => _innerStream.CanSeek; public override bool CanWrite => false; - public override long Length - { - get { throw new NotSupportedException(); } - } + public override long Length => _innerStream.Length; public override long Position { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } + get + { + return _innerStream.Position; + } + set + { + _innerStream.Position = value; + } } public override void Flush() @@ -58,12 +61,12 @@ public override int Read(byte[] buffer, int offset, int count) public override long Seek(long offset, SeekOrigin origin) { - throw new NotSupportedException(); + return _innerStream.Seek(offset, origin); } public override void SetLength(long value) { - throw new NotSupportedException(); + _innerStream.SetLength(value); } public override void Write(byte[] buffer, int offset, int count) diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index cfd34b2818e6..a0b45ab9d83a 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -46,7 +46,6 @@ Microsoft.AspNetCore.Mvc.RouteAttribute - From 1302dac7b607f50871aa9b00d33767faa5f05a93 Mon Sep 17 00:00:00 2001 From: David Acker Date: Tue, 1 Mar 2022 20:57:28 -0500 Subject: [PATCH 27/42] Address PR feedback --- .../src/DefaultRequestDecompressionProvider.cs | 7 ++++--- .../src/IDecompressionProvider.cs | 4 ++-- .../src/IRequestDecompressionProvider.cs | 4 ++-- ...osoft.AspNetCore.RequestDecompression.csproj | 5 ++--- .../src/PublicAPI.Unshipped.txt | 2 +- .../RequestDecompressionBuilderExtensions.cs | 2 +- .../src/RequestDecompressionMiddleware.cs | 12 +++++------- .../RequestDecompressionServiceExtensions.cs | 2 +- .../DefaultRequestDecompressionProviderTests.cs | 17 +++++++++++------ .../test/RequestDecompressionMiddlewareTests.cs | 2 +- ...equestDecompressionServiceExtensionsTests.cs | 1 - 11 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs index 8be702447316..7a973d1409e5 100644 --- a/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs @@ -34,7 +34,7 @@ public DefaultRequestDecompressionProvider( } /// - public IDecompressionProvider? GetDecompressionProvider(HttpContext context) + public Stream? GetDecompressionStream(HttpContext context) { var encodings = context.Request.Headers.ContentEncoding; @@ -54,10 +54,11 @@ public DefaultRequestDecompressionProvider( if (_providers.TryGetValue(encodingName, out var matchingProvider)) { + Log.DecompressingWith(_logger, encodingName.ToLowerInvariant()); + context.Request.Headers.Remove(HeaderNames.ContentEncoding); - Log.DecompressingWith(_logger, encodingName.ToLowerInvariant()); - return matchingProvider; + return matchingProvider.GetDecompressionStream(context.Request.Body); } Log.NoDecompressionProvider(_logger); diff --git a/src/Middleware/RequestDecompression/src/IDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/IDecompressionProvider.cs index ee795001c710..f59be0fe87b1 100644 --- a/src/Middleware/RequestDecompression/src/IDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/IDecompressionProvider.cs @@ -4,14 +4,14 @@ namespace Microsoft.AspNetCore.RequestDecompression; /// -/// Provides a specific decompression implementation to decompress HTTP requests. +/// Provides a specific decompression implementation to decompress HTTP request bodies. /// public interface IDecompressionProvider { /// /// Creates a new decompression stream. /// - /// The compressed data stream. + /// The compressed request body stream. /// The decompression stream. Stream GetDecompressionStream(Stream stream); } diff --git a/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs index c01f2bd0f5f8..6387b91a309a 100644 --- a/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs @@ -14,6 +14,6 @@ public interface IRequestDecompressionProvider /// Examines the request and selects an acceptable decompression provider, if any. /// /// The . - /// A decompression provider or null if there are no acceptable providers. - IDecompressionProvider? GetDecompressionProvider(HttpContext context); + /// The decompression stream when the provider is capable of decompressing the HTTP request body, otherwise . + Stream? GetDecompressionStream(HttpContext context); } diff --git a/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj index d3cf7ef9da12..7ea538fe1539 100644 --- a/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj +++ b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj @@ -7,7 +7,6 @@ true aspnetcore false - enable @@ -18,11 +17,11 @@ - + - + \ No newline at end of file diff --git a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt index be2a1f7958d7..14f9b5dc496a 100644 --- a/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt @@ -3,7 +3,7 @@ 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.GetDecompressionProvider(Microsoft.AspNetCore.Http.HttpContext! context) -> Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider? +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! Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.RequestDecompressionOptions() -> void diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs index 576e6e9eb188..c67d2bdfd1d0 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Builder; /// -/// Extension methods for the request decompression middleware. +/// Extension methods for the HTTP request decompression middleware. /// public static class RequestDecompressionBuilderExtensions { diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index c4fe1842271f..e0d60ff356a5 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -45,27 +45,25 @@ public RequestDecompressionMiddleware( /// A task that represents the execution of this middleware. public Task Invoke(HttpContext context) { - var provider = _provider.GetDecompressionProvider(context); - if (provider is null) + using var decompressionStream = _provider.GetDecompressionStream(context); + if (decompressionStream is null) { return _next(context); } - return InvokeCore(context, provider); + return InvokeCore(context, decompressionStream); } - private async Task InvokeCore(HttpContext context, IDecompressionProvider provider) + private async Task InvokeCore(HttpContext context, Stream decompressionStream) { var request = context.Request.Body; try { - await using var stream = provider.GetDecompressionStream(request); - var sizeLimit = context.GetEndpoint()?.Metadata?.GetMetadata()?.MaxRequestBodySize ?? context.Features.Get()?.MaxRequestBodySize; - context.Request.Body = new SizeLimitedStream(stream, sizeLimit); + context.Request.Body = new SizeLimitedStream(decompressionStream, sizeLimit); await _next(context); } finally diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs index 0690c7d457d9..7996ef88a93a 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.DependencyInjection; /// -/// Extension methods for the request decompression middleware. +/// Extension methods for the HTTP request decompression middleware. /// public static class RequestDecompressionServiceExtensions { diff --git a/src/Middleware/RequestDecompression/test/DefaultRequestDecompressionProviderTests.cs b/src/Middleware/RequestDecompression/test/DefaultRequestDecompressionProviderTests.cs index a3430af76d29..1f39cfef3826 100644 --- a/src/Middleware/RequestDecompression/test/DefaultRequestDecompressionProviderTests.cs +++ b/src/Middleware/RequestDecompression/test/DefaultRequestDecompressionProviderTests.cs @@ -7,14 +7,19 @@ 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(BrotliDecompressionProvider))] - [InlineData("BR", typeof(BrotliDecompressionProvider))] + [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) @@ -29,7 +34,7 @@ public void GetDecompressionProvider_SupportedContentEncoding_ReturnsProvider( var provider = new DefaultRequestDecompressionProvider(logger, options); // Act - var matchingProvider = provider.GetDecompressionProvider(httpContext); + var matchingProvider = provider.GetDecompressionStream(httpContext); // Assert Assert.NotNull(matchingProvider); @@ -55,7 +60,7 @@ public void GetDecompressionProvider_NoContentEncoding_ReturnsNull() var provider = new DefaultRequestDecompressionProvider(logger, options); // Act - var matchingProvider = provider.GetDecompressionProvider(httpContext); + var matchingProvider = provider.GetDecompressionStream(httpContext); // Assert Assert.Null(matchingProvider); @@ -82,7 +87,7 @@ public void GetDecompressionProvider_UnsupportedContentEncoding_ReturnsNull() var provider = new DefaultRequestDecompressionProvider(logger, options); // Act - var matchingProvider = provider.GetDecompressionProvider(httpContext); + var matchingProvider = provider.GetDecompressionStream(httpContext); // Assert Assert.Null(matchingProvider); @@ -110,7 +115,7 @@ public void GetDecompressionProvider_MultipleContentEncodings_ReturnsNull() var provider = new DefaultRequestDecompressionProvider(logger, options); // Act - var matchingProvider = provider.GetDecompressionProvider(httpContext); + var matchingProvider = provider.GetDecompressionStream(httpContext); // Assert Assert.Null(matchingProvider); diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs index d95bb7f89eb0..f1b933a2a88c 100644 --- a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs @@ -519,7 +519,7 @@ public void Ctor_NullRequestDelegate_Throws() private class FakeRequestDecompressionProvider : IRequestDecompressionProvider { #nullable enable - public IDecompressionProvider? GetDecompressionProvider(HttpContext context) => null; + public Stream? GetDecompressionStream(HttpContext context) => null; #nullable disable } diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionServiceExtensionsTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionServiceExtensionsTests.cs index 4ca1dfd35f22..00c4f9885f54 100644 --- a/src/Middleware/RequestDecompression/test/RequestDecompressionServiceExtensionsTests.cs +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionServiceExtensionsTests.cs @@ -1,7 +1,6 @@ // 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; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.RequestDecompression.Tests; From 7acf3071ef24b730e14e03b4ef2b9ec8587ed78f Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 2 Mar 2022 06:14:26 -0800 Subject: [PATCH 28/42] Update DefaultRequestDecompressionProvider.cs --- .../src/DefaultRequestDecompressionProvider.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs index 7a973d1409e5..58baf197101e 100644 --- a/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs @@ -54,7 +54,7 @@ public DefaultRequestDecompressionProvider( if (_providers.TryGetValue(encodingName, out var matchingProvider)) { - Log.DecompressingWith(_logger, encodingName.ToLowerInvariant()); + Log.DecompressingWith(_logger, encodingName); context.Request.Headers.Remove(HeaderNames.ContentEncoding); @@ -76,7 +76,15 @@ private static partial class Log [LoggerMessage(3, LogLevel.Debug, "No matching request decompression provider found.", EventName = "NoDecompressionProvider")] public static partial void NoDecompressionProvider(ILogger logger); - [LoggerMessage(4, LogLevel.Debug, "The request will be decompressed with '{ContentEncoding}'.", EventName = "DecompressingWith")] - public static partial void DecompressingWith(ILogger logger, string contentEncoding); + 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); } } From f6304d068c20352da896919418924a3b07ba85bf Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 2 Mar 2022 11:12:34 -0800 Subject: [PATCH 29/42] Update DefaultRequestDecompressionProvider.cs --- .../src/DefaultRequestDecompressionProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs b/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs index 58baf197101e..3178fcc5d288 100644 --- a/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs +++ b/src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs @@ -78,7 +78,7 @@ private static partial class Log public static void DecompressingWith(ILogger logger, string contentEncoding) { - if (logger.IsEnabled(LogLevel.Debug) + if (logger.IsEnabled(LogLevel.Debug)) { DecompressingWithCore(logger, contentEncoding.ToLowerInvariant()); } From 71c3cb4287b709d287e70768d3b59e63b59f9619 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 2 Mar 2022 11:16:57 -0800 Subject: [PATCH 30/42] Update RequestDecompressionMiddleware.cs --- .../src/RequestDecompressionMiddleware.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index e0d60ff356a5..1b9e893465e3 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -45,7 +45,18 @@ public RequestDecompressionMiddleware( /// A task that represents the execution of this middleware. public Task Invoke(HttpContext context) { - using var decompressionStream = _provider.GetDecompressionStream(context); + var decompressionStream = _provider.GetDecompressionStream(context); + if (decompressionStream is null) + { + return _next(context); + } + + return InvokeCore(context, decompressionStream); + } + + public Task Invoke(HttpContext context) + { + var decompressionStream = _provider.GetDecompressionStream(context); if (decompressionStream is null) { return _next(context); @@ -69,6 +80,7 @@ private async Task InvokeCore(HttpContext context, Stream decompressionStream) finally { context.Request.Body = request; + await decompressionStream.DisposeAsync(); } } } From 245ac78e10d399918ee43ed055c9205f886cefb1 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 2 Mar 2022 11:54:49 -0800 Subject: [PATCH 31/42] Update RequestDecompressionMiddleware.cs --- .../src/RequestDecompressionMiddleware.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index 1b9e893465e3..0d2d47fd2120 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -54,17 +54,6 @@ public Task Invoke(HttpContext context) return InvokeCore(context, decompressionStream); } - public Task Invoke(HttpContext 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; From 9a2268e1d92dc09868b49b868e42e3ef86bb97a7 Mon Sep 17 00:00:00 2001 From: David Acker Date: Wed, 2 Mar 2022 20:30:28 -0500 Subject: [PATCH 32/42] Use default implementations for BeginRead, EndRead --- .../Microsoft.AspNetCore.RequestDecompression.csproj | 4 ---- .../RequestDecompression/src/SizeLimitedStream.cs | 10 ---------- 2 files changed, 14 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj index 7ea538fe1539..1a2c50e5205e 100644 --- a/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj +++ b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj @@ -16,10 +16,6 @@ - - - - diff --git a/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs b/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs index 46f718a298b3..f8824c95ad6c 100644 --- a/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs +++ b/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs @@ -74,16 +74,6 @@ public override void Write(byte[] buffer, int offset, int count) throw new NotSupportedException(); } - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - { - return TaskToApm.Begin(ReadAsync(buffer, offset, count, CancellationToken.None), callback, state); - } - - public override int EndRead(IAsyncResult asyncResult) - { - return TaskToApm.End(asyncResult); - } - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { return await ReadAsync(buffer.AsMemory(offset, count), cancellationToken); From 38eb172da8afe69c65d24722da0c16cc36ca5981 Mon Sep 17 00:00:00 2001 From: David Acker Date: Mon, 16 May 2022 10:22:47 -0400 Subject: [PATCH 33/42] Make MaxRequestBodySize nullable --- .../Http.Abstractions/src/Metadata/IRequestSizeLimitMetadata.cs | 2 +- src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt | 2 +- .../test/RequestDecompressionMiddlewareTests.cs | 2 +- src/Mvc/Mvc.Core/src/RequestSizeLimitAttribute.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/IRequestSizeLimitMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IRequestSizeLimitMetadata.cs index bb26d8e73b3e..b90750ee6314 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IRequestSizeLimitMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IRequestSizeLimitMetadata.cs @@ -11,5 +11,5 @@ public interface IRequestSizeLimitMetadata /// /// The maximum allowed size of the current request body in bytes. /// - long MaxRequestBodySize { get; } + long? MaxRequestBodySize { get; } } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 02af708bd748..123900e9cf2b 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -11,7 +11,7 @@ Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.H 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.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! diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs index f1b933a2a88c..02388854ca45 100644 --- a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs @@ -649,7 +649,7 @@ private class FakeEndpointFeature : IEndpointFeature private class FakeRequestSizeLimitMetadata : IRequestSizeLimitMetadata { - public long MaxRequestBodySize { get; set; } + public long? MaxRequestBodySize { get; set; } } private class FakeHttpMaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature diff --git a/src/Mvc/Mvc.Core/src/RequestSizeLimitAttribute.cs b/src/Mvc/Mvc.Core/src/RequestSizeLimitAttribute.cs index e73ab21d4bed..63b79fb23c70 100644 --- a/src/Mvc/Mvc.Core/src/RequestSizeLimitAttribute.cs +++ b/src/Mvc/Mvc.Core/src/RequestSizeLimitAttribute.cs @@ -54,5 +54,5 @@ public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) } /// - long IRequestSizeLimitMetadata.MaxRequestBodySize => _bytes; + long? IRequestSizeLimitMetadata.MaxRequestBodySize => _bytes; } From ea3a334b82e58320fbbb90407e3ae188d400306e Mon Sep 17 00:00:00 2001 From: David Acker Date: Mon, 16 May 2022 10:23:31 -0400 Subject: [PATCH 34/42] Implement IRequestSizeLimitMetadata --- src/Mvc/Mvc.Core/src/DisableRequestSizeLimitAttribute.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Mvc/Mvc.Core/src/DisableRequestSizeLimitAttribute.cs b/src/Mvc/Mvc.Core/src/DisableRequestSizeLimitAttribute.cs index 456ccaf3fc30..52ca95ca643e 100644 --- a/src/Mvc/Mvc.Core/src/DisableRequestSizeLimitAttribute.cs +++ b/src/Mvc/Mvc.Core/src/DisableRequestSizeLimitAttribute.cs @@ -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; /// Disables the request body size limit. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] -public class DisableRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter +public class DisableRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter, IRequestSizeLimitMetadata { /// /// Gets the order value for determining the order of execution of filters. Filters execute in @@ -39,4 +40,7 @@ public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) var filter = serviceProvider.GetRequiredService(); return filter; } + + /// + long? IRequestSizeLimitMetadata.MaxRequestBodySize => null; } From c293ade357f45f25ed2e027328253c7c60f98fc0 Mon Sep 17 00:00:00 2001 From: David Acker Date: Mon, 16 May 2022 16:47:58 -0400 Subject: [PATCH 35/42] Add security concern to remarks --- src/Mvc/Mvc.Core/src/DisableRequestSizeLimitAttribute.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Mvc/Mvc.Core/src/DisableRequestSizeLimitAttribute.cs b/src/Mvc/Mvc.Core/src/DisableRequestSizeLimitAttribute.cs index 52ca95ca643e..112850901681 100644 --- a/src/Mvc/Mvc.Core/src/DisableRequestSizeLimitAttribute.cs +++ b/src/Mvc/Mvc.Core/src/DisableRequestSizeLimitAttribute.cs @@ -10,6 +10,10 @@ namespace Microsoft.AspNetCore.Mvc; /// /// Disables the request body size limit. /// +/// +/// 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. +/// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class DisableRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter, IRequestSizeLimitMetadata { From 7beceb9934dd2404e1e2e43208287eadff7e43f6 Mon Sep 17 00:00:00 2001 From: David Acker Date: Mon, 16 May 2022 16:58:59 -0400 Subject: [PATCH 36/42] Add test for invalid compressed data --- .../RequestDecompressionMiddlewareTests.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs index 02388854ca45..f3418602131c 100644 --- a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs @@ -308,6 +308,72 @@ public async Task Request_Decompressed_ContentEncodingHeaderRemoved(bool isDecom } } + [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, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .ConfigureServices(services => + { + services.AddRequestDecompression(); + services.AddSingleton(loggerFactory); + }) + .Configure(app => + { + app.Use((context, next) => + { + context.Features.Set( + 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(exception); + } + [Fact] public async Task Options_RegisterCustomDecompressionProvider() { From cd3212478199b58a5112b967b3dbc3b9339f75d7 Mon Sep 17 00:00:00 2001 From: David Acker Date: Mon, 16 May 2022 20:12:09 -0400 Subject: [PATCH 37/42] Set IHttpMaxRequestBodySizeFeature using IRequestSizeLimitMetadata --- .../src/RequestDecompressionMiddleware.cs | 68 +++++- .../RequestDecompressionMiddlewareTests.cs | 231 +++++++++++++++++- 2 files changed, 289 insertions(+), 10 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index 0d2d47fd2120..9ee898e88cde 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -1,27 +1,32 @@ // 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; /// /// Enables HTTP request decompression. /// -internal sealed class RequestDecompressionMiddleware +internal sealed partial class RequestDecompressionMiddleware { private readonly RequestDelegate _next; + private readonly ILogger _logger; private readonly IRequestDecompressionProvider _provider; /// /// Initialize the request decompression middleware. /// /// The delegate representing the remaining middleware in the request pipeline. + /// The logger. /// The . public RequestDecompressionMiddleware( RequestDelegate next, + ILogger logger, IRequestDecompressionProvider provider) { if (next is null) @@ -29,12 +34,18 @@ public RequestDecompressionMiddleware( 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; } @@ -45,6 +56,8 @@ public RequestDecompressionMiddleware( /// A task that represents the execution of this middleware. public Task Invoke(HttpContext context) { + SetMaxRequestBodySize(context); + var decompressionStream = _provider.GetDecompressionStream(context); if (decompressionStream is null) { @@ -72,4 +85,57 @@ private async Task InvokeCore(HttpContext context, Stream decompressionStream) await decompressionStream.DisposeAsync(); } } + + private void SetMaxRequestBodySize(HttpContext context) + { + var sizeLimitMetadata = context.GetEndpoint()?.Metadata?.GetMetadata(); + if (sizeLimitMetadata == null) + { + Log.MetadataNotFound(_logger); + return; + } + + var maxRequestSizeBodyFeature = context.Features.Get(); + if (maxRequestSizeBodyFeature == null) + { + Log.FeatureNotFound(_logger); + } + else if (maxRequestSizeBodyFeature.IsReadOnly) + { + Log.FeatureIsReadOnly(_logger); + } + else + { + var maxRequestBodySize = sizeLimitMetadata.MaxRequestBodySize; + maxRequestSizeBodyFeature.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); + } } diff --git a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs index f3418602131c..e28e0b6c6b5c 100644 --- a/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs +++ b/src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs @@ -1,6 +1,7 @@ // 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; @@ -568,25 +569,215 @@ public async Task Feature_HasRequestSizeLimit_UsedForRequest(bool exceedsLimit) } } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Endpoint_DoesNotHaveSizeLimitMetadata(bool isCompressed) + { + // Arrange + var sink = new TestSink(); + var logger = new TestLogger( + 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( + 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( + 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( + 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( + new TestLoggerFactory(new TestSink(), enabled: true)); var provider = new FakeRequestDecompressionProvider(); // Act + Assert Assert.Throws(() => { - new RequestDecompressionMiddleware(requestDelegate, provider); + new RequestDecompressionMiddleware(requestDelegate, logger, provider); }); } - private class FakeRequestDecompressionProvider : IRequestDecompressionProvider + [Fact] + public void Ctor_NullLogger_Throws() { -#nullable enable - public Stream? GetDecompressionStream(HttpContext context) => null; -#nullable disable + // Arrange + static Task requestDelegate(HttpContext context) => Task.FromResult(context); + ILogger logger = null; + var provider = new FakeRequestDecompressionProvider(); + + // Act + Assert + Assert.Throws(() => + { + new RequestDecompressionMiddleware(requestDelegate, logger, provider); + }); } [Fact] @@ -594,15 +785,34 @@ public void Ctor_NullRequestDecompressionProvider_Throws() { // Arrange static Task requestDelegate(HttpContext context) => Task.FromResult(context); + var logger = new TestLogger( + new TestLoggerFactory(new TestSink(), enabled: true)); IRequestDecompressionProvider provider = null; // Act + Assert Assert.Throws(() => { - new RequestDecompressionMiddleware(requestDelegate, provider); + 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); @@ -689,7 +899,7 @@ public Stream GetDecompressionStream(Stream stream) } } - private static FakeEndpointFeature GetFakeEndpointFeature(long requestSizeLimit) + private static FakeEndpointFeature GetFakeEndpointFeature(long? requestSizeLimit) { var requestSizeLimitMetadata = new FakeRequestSizeLimitMetadata { @@ -720,12 +930,15 @@ private class FakeRequestSizeLimitMetadata : IRequestSizeLimitMetadata private class FakeHttpMaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature { - public FakeHttpMaxRequestBodySizeFeature(long? maxRequestBodySize = null) + public FakeHttpMaxRequestBodySizeFeature( + long? maxRequestBodySize = null, + bool isReadOnly = false) { MaxRequestBodySize = maxRequestBodySize; + IsReadOnly = isReadOnly; } - public bool IsReadOnly => false; + public bool IsReadOnly { get; } public long? MaxRequestBodySize { get; set; } } From 519310c81a283340e1d6c6c1a4b3719c4051fddf Mon Sep 17 00:00:00 2001 From: David Acker Date: Thu, 19 May 2022 21:20:08 -0400 Subject: [PATCH 38/42] Add benchmarks --- AspNetCore.sln | 20 ++++ src/Middleware/Middleware.slnf | 3 +- .../perf/Microbenchmarks/AssemblyInfo.cs | 4 + ...equestDecompression.Microbenchmarks.csproj | 18 ++++ ...RequestDecompressionMiddlewareBenchmark.cs | 92 +++++++++++++++++++ ...oft.AspNetCore.RequestDecompression.csproj | 1 + 6 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 src/Middleware/RequestDecompression/perf/Microbenchmarks/AssemblyInfo.cs create mode 100644 src/Middleware/RequestDecompression/perf/Microbenchmarks/Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj create mode 100644 src/Middleware/RequestDecompression/perf/Microbenchmarks/RequestDecompressionMiddlewareBenchmark.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index 04669c3c3ad8..5a7a7c2fa107 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1661,6 +1661,7 @@ 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}" @@ -1717,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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10302,6 +10305,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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11153,6 +11172,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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/Middleware/Middleware.slnf b/src/Middleware/Middleware.slnf index 18c7a72cdc77..6033a2555e0d 100644 --- a/src/Middleware/Middleware.slnf +++ b/src/Middleware/Middleware.slnf @@ -78,6 +78,7 @@ "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", @@ -120,4 +121,4 @@ "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj" ] } -} +} \ No newline at end of file diff --git a/src/Middleware/RequestDecompression/perf/Microbenchmarks/AssemblyInfo.cs b/src/Middleware/RequestDecompression/perf/Microbenchmarks/AssemblyInfo.cs new file mode 100644 index 000000000000..09f49228e9e6 --- /dev/null +++ b/src/Middleware/RequestDecompression/perf/Microbenchmarks/AssemblyInfo.cs @@ -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] diff --git a/src/Middleware/RequestDecompression/perf/Microbenchmarks/Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj b/src/Middleware/RequestDecompression/perf/Microbenchmarks/Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj new file mode 100644 index 000000000000..ed3229dbbc83 --- /dev/null +++ b/src/Middleware/RequestDecompression/perf/Microbenchmarks/Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj @@ -0,0 +1,18 @@ + + + + Exe + $(DefaultNetCoreTargetFramework) + + + + + + + + + + + + + diff --git a/src/Middleware/RequestDecompression/perf/Microbenchmarks/RequestDecompressionMiddlewareBenchmark.cs b/src/Middleware/RequestDecompression/perf/Microbenchmarks/RequestDecompressionMiddlewareBenchmark.cs new file mode 100644 index 000000000000..b78838985917 --- /dev/null +++ b/src/Middleware/RequestDecompression/perf/Microbenchmarks/RequestDecompressionMiddlewareBenchmark.cs @@ -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.Instance, + Options.Create(new RequestDecompressionOptions()) + ); + + _middleware = new RequestDecompressionMiddleware( + context => Task.CompletedTask, + NullLogger.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(new HttpRequestFeature()); + features.Set(new HttpResponseFeature()); + features.Set(new MaxRequestBodySizeFeature()); + features.Set(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; + } +} diff --git a/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj index 1a2c50e5205e..3089a5ca1622 100644 --- a/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj +++ b/src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj @@ -18,6 +18,7 @@ + \ No newline at end of file From e31ef589fed8a1581fb4c7ac7616206537a820d4 Mon Sep 17 00:00:00 2001 From: David Acker Date: Thu, 19 May 2022 21:46:53 -0400 Subject: [PATCH 39/42] Remove unneeded project references --- ...soft.AspNetCore.RequestDecompression.Microbenchmarks.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Middleware/RequestDecompression/perf/Microbenchmarks/Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj b/src/Middleware/RequestDecompression/perf/Microbenchmarks/Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj index ed3229dbbc83..f40a5187122e 100644 --- a/src/Middleware/RequestDecompression/perf/Microbenchmarks/Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj +++ b/src/Middleware/RequestDecompression/perf/Microbenchmarks/Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj @@ -7,10 +7,7 @@ - - - From 18632ef4212de1eb57b287352ab0f3e2fba91480 Mon Sep 17 00:00:00 2001 From: David Acker Date: Fri, 20 May 2022 18:27:09 -0400 Subject: [PATCH 40/42] Pass all write operations to inner stream --- .../RequestDecompression/src/SizeLimitedStream.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs b/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs index f8824c95ad6c..b793fc9df455 100644 --- a/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs +++ b/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs @@ -25,7 +25,7 @@ public SizeLimitedStream(Stream innerStream, long? sizeLimit) public override bool CanSeek => _innerStream.CanSeek; - public override bool CanWrite => false; + public override bool CanWrite => _innerStream.CanWrite; public override long Length => _innerStream.Length; @@ -43,7 +43,7 @@ public override long Position public override void Flush() { - throw new NotSupportedException(); + _innerStream.Flush(); } public override int Read(byte[] buffer, int offset, int count) @@ -71,7 +71,7 @@ public override void SetLength(long value) public override void Write(byte[] buffer, int offset, int count) { - throw new NotSupportedException(); + _innerStream.Write(buffer, offset, count); } public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) From 3f39a4ddee693f3d2b1b3c84b04a0ffbd99a8e91 Mon Sep 17 00:00:00 2001 From: David Acker Date: Fri, 20 May 2022 18:30:21 -0400 Subject: [PATCH 41/42] Replace async/await with AsTask --- src/Middleware/RequestDecompression/src/SizeLimitedStream.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs b/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs index b793fc9df455..2c53bd73077d 100644 --- a/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs +++ b/src/Middleware/RequestDecompression/src/SizeLimitedStream.cs @@ -74,9 +74,9 @@ public override void Write(byte[] buffer, int offset, int count) _innerStream.Write(buffer, offset, count); } - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - return await ReadAsync(buffer.AsMemory(offset, count), cancellationToken); + return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); } public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) From b67035605347fa94434f7f5fa1fbda3a39b33ad1 Mon Sep 17 00:00:00 2001 From: David Acker Date: Wed, 25 May 2022 17:36:50 -0400 Subject: [PATCH 42/42] Fix variable name --- .../src/RequestDecompressionMiddleware.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs index 9ee898e88cde..253b1d238884 100644 --- a/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs +++ b/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs @@ -95,19 +95,19 @@ private void SetMaxRequestBodySize(HttpContext context) return; } - var maxRequestSizeBodyFeature = context.Features.Get(); - if (maxRequestSizeBodyFeature == null) + var maxRequestBodySizeFeature = context.Features.Get(); + if (maxRequestBodySizeFeature == null) { Log.FeatureNotFound(_logger); } - else if (maxRequestSizeBodyFeature.IsReadOnly) + else if (maxRequestBodySizeFeature.IsReadOnly) { Log.FeatureIsReadOnly(_logger); } else { var maxRequestBodySize = sizeLimitMetadata.MaxRequestBodySize; - maxRequestSizeBodyFeature.MaxRequestBodySize = maxRequestBodySize; + maxRequestBodySizeFeature.MaxRequestBodySize = maxRequestBodySize; if (maxRequestBodySize.HasValue) {