diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml index e356ab3d42..065ee40bdd 100644 --- a/.azure/pipelines/ci.yml +++ b/.azure/pipelines/ci.yml @@ -71,6 +71,7 @@ variables: # The following extra properties are not set when testing. Use with final build.[cmd,sh] of asset-producing jobs. - name: _PublishArgs value: /p:Publish=true + /p:GenerateChecksums=true /p:DotNetPublishBlobFeedKey=$(dotnetfeed-storage-access-key-1) /p:DotNetPublishBlobFeedUrl=https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json /p:DotNetPublishToBlobFeed=$(_DotNetPublishToBlobFeed) @@ -594,7 +595,7 @@ stages: parameters: condition: ne(variables['SkipTests'], 'true') jobName: MacOS_Test - jobDisplayName: "Test: macOS 10.13" + jobDisplayName: "Test: macOS 10.14" agentOs: macOS isTestingJob: true buildArgs: --all --test "/p:RunTemplateTests=false /p:SkipHelixReadyTests=true" $(_InternalRuntimeDownloadArgs) diff --git a/.vsconfig b/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md new file mode 100644 index 0000000000..775f221c98 --- /dev/null +++ b/CODE-OF-CONDUCT.md @@ -0,0 +1,6 @@ +# Code of Conduct + +This project has adopted the code of conduct defined by the Contributor Covenant +to clarify expected behavior in our community. + +For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). diff --git a/Directory.Build.props b/Directory.Build.props index c40d203441..8be130b168 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -179,7 +179,6 @@ .tar.gz .zip - .sha512 diff --git a/Directory.Build.targets b/Directory.Build.targets index ccef1aa25c..5c0c27b516 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -105,6 +105,14 @@ true + + $(RepoRoot)THIRD-PARTY-NOTICES.TXT + + + + + + diff --git a/README.md b/README.md index 4c2e975c42..327c3e7208 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Follow the [Getting Started](https://docs.microsoft.com/aspnet/core/getting-star Also check out the [.NET Homepage](https://www.microsoft.com/net) for released versions of .NET, getting started guides, and learning resources. -See the [Issue Management Policies](https://github.com/dotnet/aspnetcore/blob/anurse/issue-policies/docs/IssueManagementPolicies.md) document for more information on how we handle incoming issues. +See the [Issue Management Policies](https://github.com/dotnet/aspnetcore/blob/master/docs/IssueManagementPolicies.md) document for more information on how we handle incoming issues. ## How to Engage, Contribute, and Give Feedback @@ -37,4 +37,4 @@ These are some other repos for related projects: ## Code of conduct -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +See [CODE-OF-CONDUCT](./CODE-OF-CONDUCT.md) diff --git a/eng/AfterSigning.targets b/eng/AfterSigning.targets new file mode 100644 index 0000000000..4bbb0dcf03 --- /dev/null +++ b/eng/AfterSigning.targets @@ -0,0 +1,33 @@ + + + + + + + $(ArtifactsDir.Substring(0, $([MSBuild]::Subtract($(ArtifactsDir.Length), 1)))) + + $(ArtifactsDir)\installers\ + + + + + + + + + + + + + %(FullPath).sha512 + + + + + + + + diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props index 057d2662c0..44b6452388 100644 --- a/eng/Baseline.Designer.props +++ b/eng/Baseline.Designer.props @@ -6,12 +6,12 @@ - 3.0.2 + 3.0.3 - 3.0.2 + 3.0.3 diff --git a/eng/Baseline.xml b/eng/Baseline.xml index b4192011c4..df8a80748c 100644 --- a/eng/Baseline.xml +++ b/eng/Baseline.xml @@ -5,8 +5,8 @@ Update this list when preparing for a new patch. --> - - + + diff --git a/eng/Build.props b/eng/Build.props index 6f1b3f1908..ef89408b47 100644 --- a/eng/Build.props +++ b/eng/Build.props @@ -152,6 +152,7 @@ $(RepoRoot)src\SiteExtensions\LoggingAggregate\test\**\*.csproj; $(RepoRoot)src\Shared\**\*.*proj; $(RepoRoot)src\Tools\**\*.*proj; + $(RepoRoot)src\Logging.AzureAppServices\**\src\*.csproj; $(RepoRoot)src\Middleware\**\*.csproj; $(RepoRoot)src\Razor\**\*.*proj; $(RepoRoot)src\Mvc\**\*.*proj; @@ -191,6 +192,7 @@ $(RepoRoot)src\Security\**\src\*.csproj; $(RepoRoot)src\SiteExtensions\**\src\*.csproj; $(RepoRoot)src\Tools\**\src\*.csproj; + $(RepoRoot)src\Logging.AzureAppServices\**\src\*.csproj; $(RepoRoot)src\Middleware\**\src\*.csproj; $(RepoRoot)src\Razor\**\src\*.csproj; $(RepoRoot)src\Mvc\**\src\*.csproj; diff --git a/eng/Dependencies.props b/eng/Dependencies.props index d02e5158ae..6013eeca3d 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -29,7 +29,6 @@ and are generated based on the last package release. - @@ -120,8 +119,6 @@ and are generated based on the last package release. - - diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index dda9b06ce8..a7d42c3782 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -32,6 +32,7 @@ + diff --git a/eng/Publishing.props b/eng/Publishing.props index ab7456c178..2c13cb29bf 100644 --- a/eng/Publishing.props +++ b/eng/Publishing.props @@ -1,7 +1,7 @@ - + - $(ArtifactsDir.Substring(0, $([MSBuild]::Subtract($(ArtifactsDir.Length), 1)))) + $(ArtifactsDir.Substring(0, $([MSBuild]::Subtract($(ArtifactsDir.Length), 1)))) $(PublishDependsOnTargets);_PublishInstallersAndChecksums @@ -50,12 +50,10 @@ - true diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index e63ae9d242..5ab63a957a 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -13,320 +13,312 @@ https://github.com/dotnet/blazor dd7fb4d3931d556458f62642c2edfc59f6295bfb - + https://github.com/dotnet/aspnetcore-tooling - 46133a42cbf5af76e4f98a0b5221d55d342067a0 + 4ec71cb57e45db101bbd4ffcf64dafa1711de0af - + https://github.com/dotnet/aspnetcore-tooling - 46133a42cbf5af76e4f98a0b5221d55d342067a0 + 4ec71cb57e45db101bbd4ffcf64dafa1711de0af - + https://github.com/dotnet/aspnetcore-tooling - 46133a42cbf5af76e4f98a0b5221d55d342067a0 + 4ec71cb57e45db101bbd4ffcf64dafa1711de0af - + https://github.com/dotnet/aspnetcore-tooling - 46133a42cbf5af76e4f98a0b5221d55d342067a0 + 4ec71cb57e45db101bbd4ffcf64dafa1711de0af - + https://github.com/dotnet/efcore - 7a6aa0a4f513c28b5a0501a2db8880885def2236 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - + https://github.com/dotnet/efcore - 7a6aa0a4f513c28b5a0501a2db8880885def2236 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - + https://github.com/dotnet/efcore - 7a6aa0a4f513c28b5a0501a2db8880885def2236 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - + https://github.com/dotnet/efcore - 7a6aa0a4f513c28b5a0501a2db8880885def2236 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - + https://github.com/dotnet/efcore - 7a6aa0a4f513c28b5a0501a2db8880885def2236 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - + https://github.com/dotnet/efcore - 7a6aa0a4f513c28b5a0501a2db8880885def2236 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - + https://github.com/dotnet/efcore - 7a6aa0a4f513c28b5a0501a2db8880885def2236 + b0636ed8050797d0a9c16da8b98c2eea7d7e1f16 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 - - https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a - - - https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a - - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/extensions - ec073cff7d938e94cea1b239e9d8934627239a8a + 03c40031d618f923aa88da125cb078aabde9ebb1 https://github.com/dotnet/arcade @@ -340,9 +332,9 @@ https://github.com/dotnet/arcade 09bb9d929120b402348c9a0e9c8c951e824059aa - + https://github.com/dotnet/roslyn - c9f2423cb5a2ab1ee8de0ef10e536d7672b1a2ea + 8167e4880190407325d6cf7282f6bb62267abc56 diff --git a/eng/Versions.props b/eng/Versions.props index f17da370ff..737b0be4fb 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -9,7 +9,7 @@ 5 0 0 - 3 + 4 @@ -64,92 +64,90 @@ 5.0.0-beta.20180.5 - 3.6.0-3.20177.6 + 3.6.0-3.20201.6 - 5.0.0-preview.3-runtime.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 - 5.0.0-preview.3.20202.4 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4.20201.1 - 5.0.0-preview.3.20202.4 + 5.0.0-preview.4.20201.1 3.2.0-preview1.20067.1 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 - 5.0.0-preview.3.20202.8 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 + 5.0.0-preview.4.20201.2 - 5.0.0-preview.3.20181.2 - 5.0.0-preview.3.20181.2 - 5.0.0-preview.3.20181.2 - 5.0.0-preview.3.20181.2 - 5.0.0-preview.3.20181.2 - 5.0.0-preview.3.20181.2 - 5.0.0-preview.3.20181.2 + 5.0.0-preview.4.20203.1 + 5.0.0-preview.4.20203.1 + 5.0.0-preview.4.20203.1 + 5.0.0-preview.4.20203.1 + 5.0.0-preview.4.20203.1 + 5.0.0-preview.4.20203.1 + 5.0.0-preview.4.20203.1 - 5.0.0-preview.3.20202.16 - 5.0.0-preview.3.20202.16 - 5.0.0-preview.3.20202.16 - 5.0.0-preview.3.20202.16 + 5.0.0-preview.4.20201.4 + 5.0.0-preview.4.20201.4 + 5.0.0-preview.4.20201.4 + 5.0.0-preview.4.20201.4 diff --git a/eng/tools/.vsconfig b/eng/tools/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/eng/tools/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Analyzers/.vsconfig b/src/Analyzers/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Analyzers/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Antiforgery/.vsconfig b/src/Antiforgery/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Antiforgery/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Azure/.vsconfig b/src/Azure/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Azure/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Components/.vsconfig b/src/Components/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Components/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj b/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj index 867dec8215..d53f7d01fe 100644 --- a/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj +++ b/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj @@ -10,6 +10,12 @@ + + + diff --git a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj index 57b1e5b4cd..7bc610f168 100644 --- a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj +++ b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj @@ -24,6 +24,7 @@ + diff --git a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.nuspec b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.nuspec index a3e099dee6..459fed97d0 100644 --- a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.nuspec +++ b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.nuspec @@ -8,7 +8,7 @@ $CommonFileElements$ - + diff --git a/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj b/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj index 366a1bce5e..dc403233a8 100644 --- a/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj +++ b/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.nuspec b/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.nuspec index 2f0f6b8479..77df4b791b 100644 --- a/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.nuspec +++ b/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.nuspec @@ -7,6 +7,6 @@ $CommonFileElements$ - + diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index fefadced49..2f1841b906 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -58,6 +58,7 @@ + diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.multitarget.nuspec b/src/Components/Components/src/Microsoft.AspNetCore.Components.multitarget.nuspec index 7017ce828e..e2190e2be5 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.multitarget.nuspec +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.multitarget.nuspec @@ -21,6 +21,6 @@ - + diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.netcoreapp.nuspec b/src/Components/Components/src/Microsoft.AspNetCore.Components.netcoreapp.nuspec index 9ea33e53dc..69d234bc08 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.netcoreapp.nuspec +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.netcoreapp.nuspec @@ -15,6 +15,6 @@ - + diff --git a/src/Components/Directory.Build.props b/src/Components/Directory.Build.props index b614476c4c..65e48e29f5 100644 --- a/src/Components/Directory.Build.props +++ b/src/Components/Directory.Build.props @@ -20,6 +20,8 @@ $(MSBuildThisFileDirectory)Blazor\Build\src\bin\$(Configuration)\$(DefaultNetCoreTargetFramework)\ + + $(MSBuildThisFileDirectory)THIRD-PARTY-NOTICES.txt diff --git a/src/Components/Directory.Build.targets b/src/Components/Directory.Build.targets index b6b1f773d9..d6569c4088 100644 --- a/src/Components/Directory.Build.targets +++ b/src/Components/Directory.Build.targets @@ -24,8 +24,6 @@ - - + - diff --git a/src/DataProtection/.vsconfig b/src/DataProtection/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/DataProtection/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/DataProtection/Extensions/test/DataProtectionProviderTests.cs b/src/DataProtection/Extensions/test/DataProtectionProviderTests.cs index 5caee24b12..951b003063 100644 --- a/src/DataProtection/Extensions/test/DataProtectionProviderTests.cs +++ b/src/DataProtection/Extensions/test/DataProtectionProviderTests.cs @@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.DataProtection [ConditionalFact] [X509StoreIsAvailable(StoreName.My, StoreLocation.CurrentUser)] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public void System_UsesProvidedDirectoryAndCertificate() { var filePath = Path.Combine(GetTestFilesPath(), "TestCert.pfx"); @@ -165,7 +165,6 @@ namespace Microsoft.AspNetCore.DataProtection [ConditionalFact] [X509StoreIsAvailable(StoreName.My, StoreLocation.CurrentUser)] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")] public void System_UsesProvidedCertificateNotFromStore() { using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) diff --git a/src/DefaultBuilder/.vsconfig b/src/DefaultBuilder/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/DefaultBuilder/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Features/JsonPatch/.vsconfig b/src/Features/JsonPatch/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Features/JsonPatch/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj b/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj index e7b3fd2014..7397e945bf 100644 --- a/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj +++ b/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj @@ -67,7 +67,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant $(IntermediateOutputPath)ignoreme.dev.runtimeconfig.json - $(IntermediateOutputPath).version + $(IntermediateOutputPath)$(SharedFxName).versions.txt none @@ -156,12 +156,6 @@ This package is an internal implementation of the .NET Core SDK and is not meant $(InstallersOutputPath)$(RedistArchiveOutputFileName) - - - $(RedistArchiveOutputPath)$(ChecksumExtension) - - - @@ -501,4 +495,13 @@ This package is an internal implementation of the .NET Core SDK and is not meant + + + + + + + diff --git a/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj b/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj index 9abf901de4..8d9b26e7d0 100644 --- a/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj +++ b/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj @@ -36,6 +36,10 @@ <_Parameter1>TargetingPackLayoutRoot <_Parameter2>$(TargetingPackLayoutRoot) + + <_Parameter1>IsTargetingPackBuilding + <_Parameter2>$(IsTargetingPackBuilding) + <_Parameter1>VerifyAncmBinary <_Parameter2>$(VerifyAncmBinary) diff --git a/src/Framework/test/SharedFxTests.cs b/src/Framework/test/SharedFxTests.cs index 2fc09b96b0..558fc34439 100644 --- a/src/Framework/test/SharedFxTests.cs +++ b/src/Framework/test/SharedFxTests.cs @@ -131,7 +131,7 @@ namespace Microsoft.AspNetCore [Fact] public void ItContainsVersionFile() { - var versionFile = Path.Combine(_sharedFxRoot, ".version"); + var versionFile = Path.Combine(_sharedFxRoot, "Microsoft.AspNetCore.App.versions.txt"); AssertEx.FileExists(versionFile); var lines = File.ReadAllLines(versionFile); Assert.Equal(2, lines.Length); diff --git a/src/Framework/test/TargetingPackTests.cs b/src/Framework/test/TargetingPackTests.cs index 82b43cb831..54da276513 100644 --- a/src/Framework/test/TargetingPackTests.cs +++ b/src/Framework/test/TargetingPackTests.cs @@ -20,17 +20,24 @@ namespace Microsoft.AspNetCore private readonly string _expectedRid; private readonly string _targetingPackRoot; private readonly ITestOutputHelper _output; + private readonly bool _isTargetingPackBuilding; public TargetingPackTests(ITestOutputHelper output) { _output = output; _expectedRid = TestData.GetSharedFxRuntimeIdentifier(); _targetingPackRoot = Path.Combine(TestData.GetTestDataValue("TargetingPackLayoutRoot"), "packs", "Microsoft.AspNetCore.App.Ref", TestData.GetTestDataValue("TargetingPackVersion")); + _isTargetingPackBuilding = bool.Parse(TestData.GetTestDataValue("IsTargetingPackBuilding")); } [Fact] public void AssembliesAreReferenceAssemblies() { + if (!_isTargetingPackBuilding) + { + return; + } + IEnumerable dlls = Directory.GetFiles(_targetingPackRoot, "*.dll", SearchOption.AllDirectories); Assert.NotEmpty(dlls); @@ -58,6 +65,11 @@ namespace Microsoft.AspNetCore [Fact] public void PlatformManifestListsAllFiles() { + if (!_isTargetingPackBuilding) + { + return; + } + var platformManifestPath = Path.Combine(_targetingPackRoot, "data", "PlatformManifest.txt"); var expectedAssemblies = TestData.GetSharedFxDependencies() .Split(';', StringSplitOptions.RemoveEmptyEntries) diff --git a/src/Grpc/.vsconfig b/src/Grpc/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Grpc/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs b/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs index d1d6374cba..f2dcc4f5b7 100644 --- a/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs +++ b/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs @@ -125,7 +125,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks description: "A timeout occurred while running check.", duration: duration, exception: ex, - data: null); + data: null, + tags: registration.Tags); Log.HealthCheckError(_logger, registration, ex, duration); } @@ -139,7 +140,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks description: ex.Message, duration: duration, exception: ex, - data: null); + data: null, + tags: registration.Tags); Log.HealthCheckError(_logger, registration, ex, duration); } diff --git a/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs b/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs index 50cf7ebeae..1cb5b7420b 100644 --- a/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs +++ b/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs @@ -113,6 +113,47 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks }); } + [Fact] + public async Task CheckAsync_TagsArePresentInHealthReportEntryIfExceptionOccurs() + { + const string ExceptionMessage = "exception-message"; + const string OperationCancelledMessage = "operation-cancelled-message"; + var exceptionTags = new[] { "unhealthy-check-tag" }; + var operationExceptionTags = new[] { "degraded-check-tag" }; + + // Arrange + var service = CreateHealthChecksService(b => + { + b.AddAsyncCheck("ExceptionCheck", _ => throw new Exception(ExceptionMessage), exceptionTags); + b.AddAsyncCheck("OperationExceptionCheck", _ => throw new OperationCanceledException(OperationCancelledMessage), operationExceptionTags); + }); + + // Act + var results = await service.CheckHealthAsync(); + + // Assert + Assert.Collection( + results.Entries.OrderBy(kvp => kvp.Key), + actual => + { + Assert.Equal("ExceptionCheck", actual.Key); + Assert.Equal(ExceptionMessage, actual.Value.Description); + Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status); + Assert.Equal(ExceptionMessage, actual.Value.Exception.Message); + Assert.Empty(actual.Value.Data); + Assert.Equal(actual.Value.Tags, exceptionTags); + }, + actual => + { + Assert.Equal("OperationExceptionCheck", actual.Key); + Assert.Equal("A timeout occurred while running check.", actual.Value.Description); + Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status); + Assert.Equal(OperationCancelledMessage, actual.Value.Exception.Message); + Assert.Empty(actual.Value.Data); + Assert.Equal(actual.Value.Tags, operationExceptionTags); + }); + } + [Fact] public async Task CheckAsync_RunsFilteredChecksAndAggregatesResultsAsync() { diff --git a/src/Hosting/.vsconfig b/src/Hosting/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Hosting/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj index ba625f4332..939dfcf376 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj +++ b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj @@ -23,7 +23,6 @@ - diff --git a/src/Hosting/TestHost/src/HttpContextBuilder.cs b/src/Hosting/TestHost/src/HttpContextBuilder.cs index f425a55b2d..736b0458a6 100644 --- a/src/Hosting/TestHost/src/HttpContextBuilder.cs +++ b/src/Hosting/TestHost/src/HttpContextBuilder.cs @@ -115,10 +115,25 @@ namespace Microsoft.AspNetCore.TestHost // This could throw an error if there was a pending server read. Needs to // happen before completing the response so the response returns the error. var requestBodyInProgress = RequestBodyReadInProgress(); + if (requestBodyInProgress) + { + // If request is still in progress then abort it. + CancelRequestBody(); + } // Matches Kestrel server: response is completed before request is drained await CompleteResponseAsync(); - await CompleteRequestAsync(requestBodyInProgress); + + if (!requestBodyInProgress) + { + // Writer was already completed in send request callback. + await _requestPipe.Reader.CompleteAsync(); + + // Don't wait for request to drain. It could block indefinitely. In a real server + // we would wait for a timeout and then kill the socket. + // Potential future improvement: add logging that the request timed out + } + _application.DisposeContext(_testContext, exception: null); } catch (Exception ex) @@ -165,24 +180,6 @@ namespace Microsoft.AspNetCore.TestHost CancelRequestBody(); } - private async Task CompleteRequestAsync(bool requestBodyInProgress) - { - if (requestBodyInProgress) - { - // If request is still in progress then abort it. - CancelRequestBody(); - } - else - { - // Writer was already completed in send request callback. - await _requestPipe.Reader.CompleteAsync(); - } - - // Don't wait for request to drain. It could block indefinitely. In a real server - // we would wait for a timeout and then kill the socket. - // Potential future improvement: add logging that the request timed out - } - private bool RequestBodyReadInProgress() { try diff --git a/src/Http/.vsconfig b/src/Http/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Http/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Http/Http.Extensions/ref/Microsoft.AspNetCore.Http.Extensions.netcoreapp.cs b/src/Http/Http.Extensions/ref/Microsoft.AspNetCore.Http.Extensions.netcoreapp.cs index 14f3400e95..7fa291f591 100644 --- a/src/Http/Http.Extensions/ref/Microsoft.AspNetCore.Http.Extensions.netcoreapp.cs +++ b/src/Http/Http.Extensions/ref/Microsoft.AspNetCore.Http.Extensions.netcoreapp.cs @@ -43,6 +43,7 @@ namespace Microsoft.AspNetCore.Http.Extensions public partial class QueryBuilder : System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable { public QueryBuilder() { } + public QueryBuilder(System.Collections.Generic.IEnumerable> parameters) { } public QueryBuilder(System.Collections.Generic.IEnumerable> parameters) { } public void Add(string key, System.Collections.Generic.IEnumerable values) { } public void Add(string key, string value) { } diff --git a/src/Http/Http.Extensions/src/QueryBuilder.cs b/src/Http/Http.Extensions/src/QueryBuilder.cs index e9feb391b1..ab2d95b79d 100644 --- a/src/Http/Http.Extensions/src/QueryBuilder.cs +++ b/src/Http/Http.Extensions/src/QueryBuilder.cs @@ -3,8 +3,10 @@ using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Text.Encodings.Web; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Http.Extensions { @@ -23,6 +25,12 @@ namespace Microsoft.AspNetCore.Http.Extensions _params = new List>(parameters); } + public QueryBuilder(IEnumerable> parameters) + : this(parameters.SelectMany(kvp => kvp.Value, (kvp, v) => KeyValuePair.Create(kvp.Key, v))) + { + + } + public void Add(string key, IEnumerable values) { foreach (var value in values) @@ -78,4 +86,4 @@ namespace Microsoft.AspNetCore.Http.Extensions return _params.GetEnumerator(); } } -} \ No newline at end of file +} diff --git a/src/Http/Http.Extensions/test/QueryBuilderTests.cs b/src/Http/Http.Extensions/test/QueryBuilderTests.cs index 7d15dd87bf..c2517c45d4 100644 --- a/src/Http/Http.Extensions/test/QueryBuilderTests.cs +++ b/src/Http/Http.Extensions/test/QueryBuilderTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Extensions.Primitives; using Xunit; namespace Microsoft.AspNetCore.Http.Extensions @@ -70,6 +71,18 @@ namespace Microsoft.AspNetCore.Http.Extensions Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString()); } + [Fact] + public void AddMultipleValuesViaConstructor_WithStringValues() + { + var builder = new QueryBuilder(new[] + { + new KeyValuePair("key1", new StringValues(new [] { "value1", string.Empty, "value3" })), + new KeyValuePair("key2", string.Empty), + new KeyValuePair("key3", StringValues.Empty) + }); + Assert.Equal("?key1=value1&key1=&key1=value3&key2=", builder.ToString()); + } + [Fact] public void AddMultipleValuesViaInitializer_AddedInOrder() { @@ -95,4 +108,4 @@ namespace Microsoft.AspNetCore.Http.Extensions Assert.Equal("?key1=value1&key2=value2&key3=value3", builder1.ToString()); } } -} \ No newline at end of file +} diff --git a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs index 886f5c0052..3aa6ce55dc 100644 --- a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs +++ b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs @@ -216,6 +216,8 @@ namespace Microsoft.AspNetCore.WebUtilities public static partial class QueryHelpers { public static string AddQueryString(string uri, System.Collections.Generic.IDictionary queryString) { throw null; } + public static string AddQueryString(string uri, System.Collections.Generic.IEnumerable> queryString) { throw null; } + public static string AddQueryString(string uri, System.Collections.Generic.IEnumerable> queryString) { throw null; } public static string AddQueryString(string uri, string name, string value) { throw null; } public static System.Collections.Generic.Dictionary ParseNullableQuery(string queryString) { throw null; } public static System.Collections.Generic.Dictionary ParseQuery(string queryString) { throw null; } diff --git a/src/Http/WebUtilities/src/QueryHelpers.cs b/src/Http/WebUtilities/src/QueryHelpers.cs index ca71329f03..9c28f4ab2b 100644 --- a/src/Http/WebUtilities/src/QueryHelpers.cs +++ b/src/Http/WebUtilities/src/QueryHelpers.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Text.Encodings.Web; using Microsoft.Extensions.Primitives; @@ -46,10 +47,10 @@ namespace Microsoft.AspNetCore.WebUtilities } /// - /// Append the given query keys and values to the uri. + /// Append the given query keys and values to the URI. /// - /// The base uri. - /// A collection of name value query pairs to append. + /// The base URI. + /// A dictionary of query keys and values to append. /// The combined result. /// is null. /// is null. @@ -68,7 +69,38 @@ namespace Microsoft.AspNetCore.WebUtilities return AddQueryString(uri, (IEnumerable>)queryString); } - private static string AddQueryString( + /// + /// Append the given query keys and values to the URI. + /// + /// The base URI. + /// A collection of query names and values to append. + /// The combined result. + /// is null. + /// is null. + public static string AddQueryString(string uri, IEnumerable> queryString) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (queryString == null) + { + throw new ArgumentNullException(nameof(queryString)); + } + + return AddQueryString(uri, queryString.SelectMany(kvp => kvp.Value, (kvp, v) => KeyValuePair.Create(kvp.Key, v))); + } + + /// + /// Append the given query keys and values to the URI. + /// + /// The base URI. + /// A collection of name value query pairs to append. + /// The combined result. + /// is null. + /// is null. + public static string AddQueryString( string uri, IEnumerable> queryString) { diff --git a/src/Http/WebUtilities/test/QueryHelpersTests.cs b/src/Http/WebUtilities/test/QueryHelpersTests.cs index a64bcbf03b..204813e5b6 100644 --- a/src/Http/WebUtilities/test/QueryHelpersTests.cs +++ b/src/Http/WebUtilities/test/QueryHelpersTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.Primitives; using Xunit; namespace Microsoft.AspNetCore.WebUtilities @@ -119,5 +120,37 @@ namespace Microsoft.AspNetCore.WebUtilities var result = QueryHelpers.AddQueryString(uri, queryStrings); Assert.Equal(expectedUri, result); } + + [Theory] + [InlineData("http://contoso.com/", "http://contoso.com/?param1=value1¶m1=¶m1=value3¶m2=")] + [InlineData("http://contoso.com/someaction", "http://contoso.com/someaction?param1=value1¶m1=¶m1=value3¶m2=")] + [InlineData("http://contoso.com/someaction?param2=1", "http://contoso.com/someaction?param2=1¶m1=value1¶m1=¶m1=value3¶m2=")] + [InlineData("http://contoso.com/some#action", "http://contoso.com/some?param1=value1¶m1=¶m1=value3¶m2=#action")] + [InlineData("http://contoso.com/some?param2=1#action", "http://contoso.com/some?param2=1¶m1=value1¶m1=¶m1=value3¶m2=#action")] + [InlineData("http://contoso.com/#action", "http://contoso.com/?param1=value1¶m1=¶m1=value3¶m2=#action")] + [InlineData( + "http://contoso.com/someaction?q=test#anchor?value", + "http://contoso.com/someaction?q=test¶m1=value1¶m1=¶m1=value3¶m2=#anchor?value")] + [InlineData( + "http://contoso.com/someaction#anchor?stuff", + "http://contoso.com/someaction?param1=value1¶m1=¶m1=value3¶m2=#anchor?stuff")] + [InlineData( + "http://contoso.com/someaction?name?something", + "http://contoso.com/someaction?name?something¶m1=value1¶m1=¶m1=value3¶m2=")] + [InlineData( + "http://contoso.com/someaction#name#something", + "http://contoso.com/someaction?param1=value1¶m1=¶m1=value3¶m2=#name#something")] + public void AddQueryStringWithEnumerableOfKeysAndStringValues(string uri, string expectedUri) + { + var queryStrings = new Dictionary() + { + { "param1", new StringValues(new [] { "value1", string.Empty, "value3" }) }, + { "param2", string.Empty }, + { "param3", StringValues.Empty } + }; + + var result = QueryHelpers.AddQueryString(uri, queryStrings); + Assert.Equal(expectedUri, result); + } } } diff --git a/src/Identity/.vsconfig b/src/Identity/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Identity/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj b/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj index 6da54c6869..23f9d4bad5 100644 --- a/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj +++ b/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj @@ -23,6 +23,7 @@ Bootstrap4 + $(MSBuildThisFileDirectory)THIRD-PARTY-NOTICES.TXT @@ -32,7 +33,6 @@ - diff --git a/src/Identity/test/Identity.Test/IdentityUIScriptsTest.cs b/src/Identity/test/Identity.Test/IdentityUIScriptsTest.cs index 005895f9c0..6bfa6efa5e 100644 --- a/src/Identity/test/Identity.Test/IdentityUIScriptsTest.cs +++ b/src/Identity/test/Identity.Test/IdentityUIScriptsTest.cs @@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.Identity.Test [Theory] [MemberData(nameof(ScriptWithIntegrityData))] + [QuarantinedTest] public async Task IdentityUI_ScriptTags_SubresourceIntegrityCheck(ScriptTag scriptTag) { var integrity = await GetShaIntegrity(scriptTag); diff --git a/src/Installers/Windows/.vsconfig b/src/Installers/Windows/.vsconfig new file mode 100644 index 0000000000..8f411e8f86 --- /dev/null +++ b/src/Installers/Windows/.vsconfig @@ -0,0 +1,16 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Component.VC.ATL", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "Microsoft.VisualStudio.Component.Windows10SDK.17134", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NativeDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/.vsconfig b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/.vsconfig new file mode 100644 index 0000000000..8f411e8f86 --- /dev/null +++ b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/.vsconfig @@ -0,0 +1,16 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Component.VC.ATL", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "Microsoft.VisualStudio.Component.Windows10SDK.17134", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NativeDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/.vsconfig b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/.vsconfig new file mode 100644 index 0000000000..8f411e8f86 --- /dev/null +++ b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/.vsconfig @@ -0,0 +1,16 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Component.VC.ATL", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "Microsoft.VisualStudio.Component.Windows10SDK.17134", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NativeDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Installers/Windows/Wix.targets b/src/Installers/Windows/Wix.targets index 2e4b8dca03..de6636d51d 100644 --- a/src/Installers/Windows/Wix.targets +++ b/src/Installers/Windows/Wix.targets @@ -64,7 +64,7 @@ + BeforeTargets="Build"> <_cabs Include="$(TargetDir)**/*.cab" /> @@ -72,10 +72,4 @@ - - - $(InstallersOutputPath)$(PackageFileName)$(ChecksumExtension) - - - diff --git a/src/Logging.AzureAppServices/Directory.Build.props b/src/Logging.AzureAppServices/Directory.Build.props new file mode 100644 index 0000000000..68f87d4f24 --- /dev/null +++ b/src/Logging.AzureAppServices/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + true + + diff --git a/src/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs b/src/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs new file mode 100644 index 0000000000..9b680e9138 --- /dev/null +++ b/src/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging.AzureAppServices; +using Microsoft.Extensions.Logging.Configuration; +using Microsoft.Extensions.Options; +using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; + +namespace Microsoft.Extensions.Logging +{ + /// + /// Extension methods for adding Azure diagnostics logger. + /// + public static class AzureAppServicesLoggerFactoryExtensions + { + /// + /// Adds an Azure Web Apps diagnostics logger. + /// + /// The extension method argument + public static ILoggingBuilder AddAzureWebAppDiagnostics(this ILoggingBuilder builder) + { + var context = WebAppContext.Default; + + // Only add the provider if we're in Azure WebApp. That cannot change once the apps started + return AddAzureWebAppDiagnostics(builder, context); + } + + internal static ILoggingBuilder AddAzureWebAppDiagnostics(this ILoggingBuilder builder, IWebAppContext context) + { + if (!context.IsRunningInAzureWebApp) + { + return builder; + } + + builder.AddConfiguration(); + + var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(context); + var services = builder.Services; + + var addedFileLogger = TryAddEnumerable(services, Singleton()); + var addedBlobLogger = TryAddEnumerable(services, Singleton()); + + if (addedFileLogger || addedBlobLogger) + { + services.AddSingleton(context); + services.AddSingleton>( + new ConfigurationChangeTokenSource(config)); + } + + if (addedFileLogger) + { + services.AddSingleton>(CreateFileFilterConfigureOptions(config)); + services.AddSingleton>(new FileLoggerConfigureOptions(config, context)); + services.AddSingleton>( + new ConfigurationChangeTokenSource(config)); + LoggerProviderOptions.RegisterProviderOptions(builder.Services); + } + + if (addedBlobLogger) + { + services.AddSingleton>(CreateBlobFilterConfigureOptions(config)); + services.AddSingleton>(new BlobLoggerConfigureOptions(config, context)); + services.AddSingleton>( + new ConfigurationChangeTokenSource(config)); + LoggerProviderOptions.RegisterProviderOptions(builder.Services); + } + + return builder; + } + + private static bool TryAddEnumerable(IServiceCollection collection, ServiceDescriptor descriptor) + { + var beforeCount = collection.Count; + collection.TryAddEnumerable(descriptor); + return beforeCount != collection.Count; + } + + private static ConfigurationBasedLevelSwitcher CreateBlobFilterConfigureOptions(IConfiguration config) + { + return new ConfigurationBasedLevelSwitcher( + configuration: config, + provider: typeof(BlobLoggerProvider), + levelKey: "AzureBlobTraceLevel"); + } + + private static ConfigurationBasedLevelSwitcher CreateFileFilterConfigureOptions(IConfiguration config) + { + return new ConfigurationBasedLevelSwitcher( + configuration: config, + provider: typeof(FileLoggerProvider), + levelKey: "AzureDriveTraceLevel"); + } + } +} diff --git a/src/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs b/src/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs new file mode 100644 index 0000000000..1e1285b358 --- /dev/null +++ b/src/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// Options for Azure diagnostics blob logging. + /// + public class AzureBlobLoggerOptions: BatchingLoggerOptions + { + private string _blobName = "applicationLog.txt"; + + /// + /// Gets or sets the last section of log blob name. + /// Defaults to "applicationLog.txt". + /// + public string BlobName + { + get { return _blobName; } + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException(nameof(value), $"{nameof(BlobName)} must be non-empty string."); + } + _blobName = value; + } + } + + internal string ContainerUrl { get; set; } + + internal string ApplicationName { get; set; } + + internal string ApplicationInstanceId { get; set; } + } +} diff --git a/src/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs b/src/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs new file mode 100644 index 0000000000..af8b5a112e --- /dev/null +++ b/src/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// Options for Azure diagnostics file logging. + /// + public class AzureFileLoggerOptions: BatchingLoggerOptions + { + private int? _fileSizeLimit = 10 * 1024 * 1024; + private int? _retainedFileCountLimit = 2; + private string _fileName = "diagnostics-"; + + /// + /// Gets or sets a strictly positive value representing the maximum log size in bytes or null for no limit. + /// Once the log is full, no more messages will be appended. + /// Defaults to 10MB. + /// + public int? FileSizeLimit + { + get { return _fileSizeLimit; } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FileSizeLimit)} must be positive."); + } + _fileSizeLimit = value; + } + } + + /// + /// Gets or sets a strictly positive value representing the maximum retained file count or null for no limit. + /// Defaults to 2. + /// + public int? RetainedFileCountLimit + { + get { return _retainedFileCountLimit; } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(RetainedFileCountLimit)} must be positive."); + } + _retainedFileCountLimit = value; + } + } + + /// + /// Gets or sets a string representing the prefix of the file name used to store the logging information. + /// The current date, in the format YYYYMMDD will be added after the given value. + /// Defaults to diagnostics-. + /// + public string FileName + { + get { return _fileName; } + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException(nameof(value)); + } + _fileName = value; + } + } + + internal string LogDirectory { get; set; } + } +} diff --git a/src/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs b/src/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs new file mode 100644 index 0000000000..8dc8727b3a --- /dev/null +++ b/src/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal class BatchLoggerConfigureOptions : IConfigureOptions + { + private readonly IConfiguration _configuration; + private readonly string _isEnabledKey; + + public BatchLoggerConfigureOptions(IConfiguration configuration, string isEnabledKey) + { + _configuration = configuration; + _isEnabledKey = isEnabledKey; + } + + public void Configure(BatchingLoggerOptions options) + { + options.IsEnabled = TextToBoolean(_configuration.GetSection(_isEnabledKey)?.Value); + } + + private static bool TextToBoolean(string text) + { + if (string.IsNullOrEmpty(text) || + !bool.TryParse(text, out var result)) + { + result = false; + } + + return result; + } + } +} diff --git a/src/Logging.AzureAppServices/src/BatchingLogger.cs b/src/Logging.AzureAppServices/src/BatchingLogger.cs new file mode 100644 index 0000000000..bd192169f3 --- /dev/null +++ b/src/Logging.AzureAppServices/src/BatchingLogger.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal class BatchingLogger : ILogger + { + private readonly BatchingLoggerProvider _provider; + private readonly string _category; + + public BatchingLogger(BatchingLoggerProvider loggerProvider, string categoryName) + { + _provider = loggerProvider; + _category = categoryName; + } + + public IDisposable BeginScope(TState state) + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return _provider.IsEnabled; + } + + public void Log(DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + var builder = new StringBuilder(); + builder.Append(timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz")); + builder.Append(" ["); + builder.Append(logLevel.ToString()); + builder.Append("] "); + builder.Append(_category); + + var scopeProvider = _provider.ScopeProvider; + if (scopeProvider != null) + { + scopeProvider.ForEachScope((scope, stringBuilder) => + { + stringBuilder.Append(" => ").Append(scope); + }, builder); + + builder.AppendLine(":"); + } + else + { + builder.Append(": "); + } + + builder.AppendLine(formatter(state, exception)); + + if (exception != null) + { + builder.AppendLine(exception.ToString()); + } + + _provider.AddMessage(timestamp, builder.ToString()); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + Log(DateTimeOffset.Now, logLevel, eventId, state, exception, formatter); + } + } +} diff --git a/src/Logging.AzureAppServices/src/BatchingLoggerOptions.cs b/src/Logging.AzureAppServices/src/BatchingLoggerOptions.cs new file mode 100644 index 0000000000..9fbd964800 --- /dev/null +++ b/src/Logging.AzureAppServices/src/BatchingLoggerOptions.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// Options for a logger which batches up log messages. + /// + public class BatchingLoggerOptions + { + private int? _batchSize; + private int? _backgroundQueueSize = 1000; + private TimeSpan _flushPeriod = TimeSpan.FromSeconds(1); + + /// + /// Gets or sets the period after which logs will be flushed to the store. + /// + public TimeSpan FlushPeriod + { + get { return _flushPeriod; } + set + { + if (value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FlushPeriod)} must be positive."); + } + _flushPeriod = value; + } + } + + /// + /// Gets or sets the maximum size of the background log message queue or null for no limit. + /// After maximum queue size is reached log event sink would start blocking. + /// Defaults to 1000. + /// + public int? BackgroundQueueSize + { + get { return _backgroundQueueSize; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BackgroundQueueSize)} must be non-negative."); + } + _backgroundQueueSize = value; + } + } + + /// + /// Gets or sets a maximum number of events to include in a single batch or null for no limit. + /// + /// Defaults to null. + public int? BatchSize + { + get { return _batchSize; } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BatchSize)} must be positive."); + } + _batchSize = value; + } + } + + /// + /// Gets or sets value indicating if logger accepts and queues writes. + /// + public bool IsEnabled { get; set; } + + /// + /// Gets or sets a value indicating whether scopes should be included in the message. + /// Defaults to false. + /// + public bool IncludeScopes { get; set; } = false; + } +} diff --git a/src/Logging.AzureAppServices/src/BatchingLoggerProvider.cs b/src/Logging.AzureAppServices/src/BatchingLoggerProvider.cs new file mode 100644 index 0000000000..227a616f3b --- /dev/null +++ b/src/Logging.AzureAppServices/src/BatchingLoggerProvider.cs @@ -0,0 +1,208 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// A provider of instances. + /// + public abstract class BatchingLoggerProvider : ILoggerProvider, ISupportExternalScope + { + private readonly List _currentBatch = new List(); + private readonly TimeSpan _interval; + private readonly int? _queueSize; + private readonly int? _batchSize; + private readonly IDisposable _optionsChangeToken; + + private int _messagesDropped; + + private BlockingCollection _messageQueue; + private Task _outputTask; + private CancellationTokenSource _cancellationTokenSource; + + private bool _includeScopes; + private IExternalScopeProvider _scopeProvider; + + internal IExternalScopeProvider ScopeProvider => _includeScopes ? _scopeProvider : null; + + internal BatchingLoggerProvider(IOptionsMonitor options) + { + // NOTE: Only IsEnabled is monitored + + var loggerOptions = options.CurrentValue; + if (loggerOptions.BatchSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(loggerOptions.BatchSize), $"{nameof(loggerOptions.BatchSize)} must be a positive number."); + } + if (loggerOptions.FlushPeriod <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(loggerOptions.FlushPeriod), $"{nameof(loggerOptions.FlushPeriod)} must be longer than zero."); + } + + _interval = loggerOptions.FlushPeriod; + _batchSize = loggerOptions.BatchSize; + _queueSize = loggerOptions.BackgroundQueueSize; + + _optionsChangeToken = options.OnChange(UpdateOptions); + UpdateOptions(options.CurrentValue); + } + + /// + /// Checks if the queue is enabled. + /// + public bool IsEnabled { get; private set; } + + private void UpdateOptions(BatchingLoggerOptions options) + { + var oldIsEnabled = IsEnabled; + IsEnabled = options.IsEnabled; + _includeScopes = options.IncludeScopes; + + if (oldIsEnabled != IsEnabled) + { + if (IsEnabled) + { + Start(); + } + else + { + Stop(); + } + } + + } + + internal abstract Task WriteMessagesAsync(IEnumerable messages, CancellationToken token); + + private async Task ProcessLogQueue() + { + while (!_cancellationTokenSource.IsCancellationRequested) + { + var limit = _batchSize ?? int.MaxValue; + + while (limit > 0 && _messageQueue.TryTake(out var message)) + { + _currentBatch.Add(message); + limit--; + } + + var messagesDropped = Interlocked.Exchange(ref _messagesDropped, 0); + if (messagesDropped != 0) + { + _currentBatch.Add(new LogMessage(DateTimeOffset.Now, $"{messagesDropped} message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this.{Environment.NewLine}")); + } + + if (_currentBatch.Count > 0) + { + try + { + await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token); + } + catch + { + // ignored + } + + _currentBatch.Clear(); + } + else + { + await IntervalAsync(_interval, _cancellationTokenSource.Token); + } + } + } + + /// + /// Wait for the given . + /// + /// The amount of time to wait. + /// A that can be used to cancel the delay. + /// A which completes when the has passed or the has been canceled. + protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken) + { + return Task.Delay(interval, cancellationToken); + } + + internal void AddMessage(DateTimeOffset timestamp, string message) + { + if (!_messageQueue.IsAddingCompleted) + { + try + { + if (!_messageQueue.TryAdd(new LogMessage(timestamp, message), millisecondsTimeout: 0, cancellationToken: _cancellationTokenSource.Token)) + { + Interlocked.Increment(ref _messagesDropped); + } + } + catch + { + //cancellation token canceled or CompleteAdding called + } + } + } + + private void Start() + { + _messageQueue = _queueSize == null ? + new BlockingCollection(new ConcurrentQueue()) : + new BlockingCollection(new ConcurrentQueue(), _queueSize.Value); + + _cancellationTokenSource = new CancellationTokenSource(); + _outputTask = Task.Run(ProcessLogQueue); + } + + private void Stop() + { + _cancellationTokenSource.Cancel(); + _messageQueue.CompleteAdding(); + + try + { + _outputTask.Wait(_interval); + } + catch (TaskCanceledException) + { + } + catch (AggregateException ex) when (ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException) + { + } + } + + /// + public void Dispose() + { + _optionsChangeToken?.Dispose(); + if (IsEnabled) + { + Stop(); + } + } + + /// + /// Creates a with the given . + /// + /// The name of the category to create this logger with. + /// The that was created. + public ILogger CreateLogger(string categoryName) + { + return new BatchingLogger(this, categoryName); + } + + /// + /// Sets the scope on this provider. + /// + /// Provides the scope. + void ISupportExternalScope.SetScopeProvider(IExternalScopeProvider scopeProvider) + { + _scopeProvider = scopeProvider; + } + } +} diff --git a/src/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs b/src/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs new file mode 100644 index 0000000000..e9805128b7 --- /dev/null +++ b/src/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + internal class BlobAppendReferenceWrapper : ICloudAppendBlob + { + private readonly Uri _fullUri; + private readonly HttpClient _client; + private readonly Uri _appendUri; + + public BlobAppendReferenceWrapper(string containerUrl, string name, HttpClient client) + { + var uriBuilder = new UriBuilder(containerUrl); + uriBuilder.Path += "/" + name; + _fullUri = uriBuilder.Uri; + + AppendBlockQuery(uriBuilder); + _appendUri = uriBuilder.Uri; + _client = client; + } + + /// + public async Task AppendAsync(ArraySegment data, CancellationToken cancellationToken) + { + Task AppendDataAsync() + { + var message = new HttpRequestMessage(HttpMethod.Put, _appendUri) + { + Content = new ByteArrayContent(data.Array, data.Offset, data.Count) + }; + AddCommonHeaders(message); + + return _client.SendAsync(message, cancellationToken); + } + + var response = await AppendDataAsync(); + + if (response.StatusCode == HttpStatusCode.NotFound) + { + // If no blob exists try creating it + var message = new HttpRequestMessage(HttpMethod.Put, _fullUri) + { + // Set Content-Length to 0 to create "Append Blob" + Content = new ByteArrayContent(Array.Empty()), + Headers = + { + { "If-None-Match", "*" } + } + }; + + AddCommonHeaders(message); + + response = await _client.SendAsync(message, cancellationToken); + + // If result is 2** or 412 try to append again + if (response.IsSuccessStatusCode || + response.StatusCode == HttpStatusCode.PreconditionFailed) + { + // Retry sending data after blob creation + response = await AppendDataAsync(); + } + } + + response.EnsureSuccessStatusCode(); + } + + private static void AddCommonHeaders(HttpRequestMessage message) + { + message.Headers.Add("x-ms-blob-type", "AppendBlob"); + message.Headers.Add("x-ms-version", "2016-05-31"); + message.Headers.Date = DateTimeOffset.UtcNow; + } + + private static void AppendBlockQuery(UriBuilder uriBuilder) + { + // See https://msdn.microsoft.com/en-us/library/system.uribuilder.query.aspx for: + // Note: Do not append a string directly to Query property. + // If the length of Query is greater than 1, retrieve the property value + // as a string, remove the leading question mark, append the new query string, + // and set the property with the combined string. + var queryToAppend = "comp=appendblock"; + if (uriBuilder.Query != null && uriBuilder.Query.Length > 1) + uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + queryToAppend; + else + uriBuilder.Query = queryToAppend; + } + } +} diff --git a/src/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.cs b/src/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.cs new file mode 100644 index 0000000000..f9a186872b --- /dev/null +++ b/src/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.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. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal class BlobLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions + { + private readonly IConfiguration _configuration; + private readonly IWebAppContext _context; + + public BlobLoggerConfigureOptions(IConfiguration configuration, IWebAppContext context) + : base(configuration, "AzureBlobEnabled") + { + _configuration = configuration; + _context = context; + } + + public void Configure(AzureBlobLoggerOptions options) + { + base.Configure(options); + options.ContainerUrl = _configuration.GetSection("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL")?.Value; + options.ApplicationName = _context.SiteName; + options.ApplicationInstanceId = _context.SiteInstanceId; + } + } +} diff --git a/src/Logging.AzureAppServices/src/BlobLoggerProvider.cs b/src/Logging.AzureAppServices/src/BlobLoggerProvider.cs new file mode 100644 index 0000000000..3d62ea2ac6 --- /dev/null +++ b/src/Logging.AzureAppServices/src/BlobLoggerProvider.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. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// The implementation that stores messages by appending them to Azure Blob in batches. + /// + [ProviderAlias("AzureAppServicesBlob")] + public class BlobLoggerProvider : BatchingLoggerProvider + { + private readonly string _appName; + private readonly string _fileName; + private readonly Func _blobReferenceFactory; + private readonly HttpClient _httpClient; + + /// + /// Creates a new instance of + /// + /// The options to use when creating a provider. + public BlobLoggerProvider(IOptionsMonitor options) + : this(options, null) + { + _blobReferenceFactory = name => new BlobAppendReferenceWrapper( + options.CurrentValue.ContainerUrl, + name, + _httpClient); + } + + /// + /// Creates a new instance of + /// + /// The container to store logs to. + /// Options to be used in creating a logger. + internal BlobLoggerProvider( + IOptionsMonitor options, + Func blobReferenceFactory) : + base(options) + { + var value = options.CurrentValue; + _appName = value.ApplicationName; + _fileName = value.ApplicationInstanceId + "_" + value.BlobName; + _blobReferenceFactory = blobReferenceFactory; + _httpClient = new HttpClient(); + } + + internal override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken) + { + var eventGroups = messages.GroupBy(GetBlobKey); + foreach (var eventGroup in eventGroups) + { + var key = eventGroup.Key; + var blobName = $"{_appName}/{key.Year}/{key.Month:00}/{key.Day:00}/{key.Hour:00}/{_fileName}"; + + var blob = _blobReferenceFactory(blobName); + + using (var stream = new MemoryStream()) + using (var writer = new StreamWriter(stream)) + { + foreach (var logEvent in eventGroup) + { + writer.Write(logEvent.Message); + } + + await writer.FlushAsync(); + var tryGetBuffer = stream.TryGetBuffer(out var buffer); + Debug.Assert(tryGetBuffer); + await blob.AppendAsync(buffer, cancellationToken); + } + } + } + + private (int Year, int Month, int Day, int Hour) GetBlobKey(LogMessage e) + { + return (e.Timestamp.Year, + e.Timestamp.Month, + e.Timestamp.Day, + e.Timestamp.Hour); + } + } +} diff --git a/src/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs b/src/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs new file mode 100644 index 0000000000..c62ccb2331 --- /dev/null +++ b/src/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.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. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal class ConfigurationBasedLevelSwitcher: IConfigureOptions + { + private readonly IConfiguration _configuration; + private readonly Type _provider; + private readonly string _levelKey; + + public ConfigurationBasedLevelSwitcher(IConfiguration configuration, Type provider, string levelKey) + { + _configuration = configuration; + _provider = provider; + _levelKey = levelKey; + } + + public void Configure(LoggerFilterOptions options) + { + options.Rules.Add(new LoggerFilterRule(_provider.FullName, null, GetLogLevel(), null)); + } + + private LogLevel GetLogLevel() + { + return TextToLogLevel(_configuration.GetSection(_levelKey)?.Value); + } + + private static LogLevel TextToLogLevel(string text) + { + switch (text?.ToUpperInvariant()) + { + case "ERROR": + return LogLevel.Error; + case "WARNING": + return LogLevel.Warning; + case "INFORMATION": + return LogLevel.Information; + case "VERBOSE": + return LogLevel.Trace; + default: + return LogLevel.None; + } + } + } +} diff --git a/src/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs b/src/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs new file mode 100644 index 0000000000..8cd1f5eb91 --- /dev/null +++ b/src/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal class FileLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions + { + private readonly IWebAppContext _context; + + public FileLoggerConfigureOptions(IConfiguration configuration, IWebAppContext context) + : base(configuration, "AzureDriveEnabled") + { + _context = context; + } + + public void Configure(AzureFileLoggerOptions options) + { + base.Configure(options); + options.LogDirectory = Path.Combine(_context.HomeFolder, "LogFiles", "Application"); + } + } +} diff --git a/src/Logging.AzureAppServices/src/FileLoggerProvider.cs b/src/Logging.AzureAppServices/src/FileLoggerProvider.cs new file mode 100644 index 0000000000..1143d38c07 --- /dev/null +++ b/src/Logging.AzureAppServices/src/FileLoggerProvider.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// A which writes out to a file. + /// + [ProviderAlias("AzureAppServicesFile")] + public class FileLoggerProvider : BatchingLoggerProvider + { + private readonly string _path; + private readonly string _fileName; + private readonly int? _maxFileSize; + private readonly int? _maxRetainedFiles; + + /// + /// Creates a new instance of . + /// + /// The options to use when creating a provider. + public FileLoggerProvider(IOptionsMonitor options) : base(options) + { + var loggerOptions = options.CurrentValue; + _path = loggerOptions.LogDirectory; + _fileName = loggerOptions.FileName; + _maxFileSize = loggerOptions.FileSizeLimit; + _maxRetainedFiles = loggerOptions.RetainedFileCountLimit; + } + + internal override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken) + { + Directory.CreateDirectory(_path); + + foreach (var group in messages.GroupBy(GetGrouping)) + { + var fullName = GetFullName(group.Key); + var fileInfo = new FileInfo(fullName); + if (_maxFileSize > 0 && fileInfo.Exists && fileInfo.Length > _maxFileSize) + { + return; + } + + using (var streamWriter = File.AppendText(fullName)) + { + foreach (var item in group) + { + await streamWriter.WriteAsync(item.Message); + } + } + } + + RollFiles(); + } + + private string GetFullName((int Year, int Month, int Day) group) + { + return Path.Combine(_path, $"{_fileName}{group.Year:0000}{group.Month:00}{group.Day:00}.txt"); + } + + private (int Year, int Month, int Day) GetGrouping(LogMessage message) + { + return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day); + } + + private void RollFiles() + { + if (_maxRetainedFiles > 0) + { + var files = new DirectoryInfo(_path) + .GetFiles(_fileName + "*") + .OrderByDescending(f => f.Name) + .Skip(_maxRetainedFiles.Value); + + foreach (var item in files) + { + item.Delete(); + } + } + } + } +} diff --git a/src/Logging.AzureAppServices/src/ICloudAppendBlob.cs b/src/Logging.AzureAppServices/src/ICloudAppendBlob.cs new file mode 100644 index 0000000000..2f55bbb0d1 --- /dev/null +++ b/src/Logging.AzureAppServices/src/ICloudAppendBlob.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// Represents an append blob, a type of blob where blocks of data are always committed to the end of the blob. + /// + internal interface ICloudAppendBlob + { + /// + /// Initiates an asynchronous operation to open a stream for writing to the blob. + /// + /// A object of type that represents the asynchronous operation. + Task AppendAsync(ArraySegment data, CancellationToken cancellationToken); + } +} diff --git a/src/Logging.AzureAppServices/src/IWebAppContext.cs b/src/Logging.AzureAppServices/src/IWebAppContext.cs new file mode 100644 index 0000000000..f8c826ceb8 --- /dev/null +++ b/src/Logging.AzureAppServices/src/IWebAppContext.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// Represents an Azure WebApp context + /// + internal interface IWebAppContext + { + /// + /// Gets the path to the home folder if running in Azure WebApp + /// + string HomeFolder { get; } + + /// + /// Gets the name of site if running in Azure WebApp + /// + string SiteName { get; } + + /// + /// Gets the id of site if running in Azure WebApp + /// + string SiteInstanceId { get; } + + /// + /// Gets a value indicating whether or new we're in an Azure WebApp + /// + bool IsRunningInAzureWebApp { get; } + } +} diff --git a/src/Logging.AzureAppServices/src/LogMessage.cs b/src/Logging.AzureAppServices/src/LogMessage.cs new file mode 100644 index 0000000000..4a1179ceb3 --- /dev/null +++ b/src/Logging.AzureAppServices/src/LogMessage.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal readonly struct LogMessage + { + public LogMessage(DateTimeOffset timestamp, string message) + { + Timestamp = timestamp; + Message = message; + } + + public DateTimeOffset Timestamp { get; } + public string Message { get; } + } +} diff --git a/src/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj b/src/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj new file mode 100644 index 0000000000..5bedde8c6d --- /dev/null +++ b/src/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj @@ -0,0 +1,24 @@ + + + + Logger implementation to support Azure App Services 'Diagnostics logs' and 'Log stream' features. + netstandard2.0 + $(NoWarn);CS1591 + true + true + + + + + + + + + + + + + + + + diff --git a/src/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs b/src/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..7c7d332545 --- /dev/null +++ b/src/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Logging.AzureAppServices/src/SiteConfigurationProvider.cs b/src/Logging.AzureAppServices/src/SiteConfigurationProvider.cs new file mode 100644 index 0000000000..452c936f93 --- /dev/null +++ b/src/Logging.AzureAppServices/src/SiteConfigurationProvider.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + internal class SiteConfigurationProvider + { + public static IConfiguration GetAzureLoggingConfiguration(IWebAppContext context) + { + var settingsFolder = Path.Combine(context.HomeFolder, "site", "diagnostics"); + var settingsFile = Path.Combine(settingsFolder, "settings.json"); + + return new ConfigurationBuilder() + .AddEnvironmentVariables() + .AddJsonFile(settingsFile, optional: true, reloadOnChange: true) + .Build(); + } + } +} diff --git a/src/Logging.AzureAppServices/src/WebAppContext.cs b/src/Logging.AzureAppServices/src/WebAppContext.cs new file mode 100644 index 0000000000..8bdd3f1c76 --- /dev/null +++ b/src/Logging.AzureAppServices/src/WebAppContext.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.AzureAppServices +{ + /// + /// Represents the default implementation of . + /// + internal class WebAppContext : IWebAppContext + { + /// + /// Gets the default instance of the WebApp context. + /// + public static WebAppContext Default { get; } = new WebAppContext(); + + private WebAppContext() { } + + /// + public string HomeFolder { get; } = Environment.GetEnvironmentVariable("HOME"); + + /// + public string SiteName { get; } = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME"); + + /// + public string SiteInstanceId { get; } = Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID"); + + /// + public bool IsRunningInAzureWebApp => !string.IsNullOrEmpty(HomeFolder) && + !string.IsNullOrEmpty(SiteName); + } +} diff --git a/src/Logging.AzureAppServices/test/AzureAppendBlobTests.cs b/src/Logging.AzureAppServices/test/AzureAppendBlobTests.cs new file mode 100644 index 0000000000..2fd5955e86 --- /dev/null +++ b/src/Logging.AzureAppServices/test/AzureAppendBlobTests.cs @@ -0,0 +1,186 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class AzureAppendBlobTests + { + public string _containerUrl = "https://host/container?query=1"; + public string _blobName = "blob/path"; + + [Fact] + public async Task SendsDataAsStream() + { + var testMessageHandler = new TestMessageHandler(async message => + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString()); + Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync()); + AssertDefaultHeaders(message); + + return new HttpResponseMessage(HttpStatusCode.OK); + }); + + var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler)); + await blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None); + } + + private static void AssertDefaultHeaders(HttpRequestMessage message) + { + Assert.Equal(new[] {"AppendBlob"}, message.Headers.GetValues("x-ms-blob-type")); + Assert.Equal(new[] {"2016-05-31"}, message.Headers.GetValues("x-ms-version")); + Assert.NotNull(message.Headers.Date); + } + + [Theory] + [InlineData(HttpStatusCode.Created)] + [InlineData(HttpStatusCode.PreconditionFailed)] + public async Task CreatesBlobIfNotExist(HttpStatusCode createStatusCode) + { + var stage = 0; + var testMessageHandler = new TestMessageHandler(async message => + { + // First PUT request + if (stage == 0) + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString()); + Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync()); + Assert.Equal(3, message.Content.Headers.ContentLength); + + AssertDefaultHeaders(message); + + stage++; + return new HttpResponseMessage(HttpStatusCode.NotFound); + } + // Create request + if (stage == 1) + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1", message.RequestUri.ToString()); + Assert.Equal(0, message.Content.Headers.ContentLength); + Assert.Equal(new[] { "*" }, message.Headers.GetValues("If-None-Match")); + + AssertDefaultHeaders(message); + + stage++; + return new HttpResponseMessage(createStatusCode); + } + // First PUT request + if (stage == 2) + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString()); + Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync()); + Assert.Equal(3, message.Content.Headers.ContentLength); + + AssertDefaultHeaders(message); + + stage++; + return new HttpResponseMessage(HttpStatusCode.Created); + } + throw new NotImplementedException(); + }); + + var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler)); + await blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None); + + Assert.Equal(3, stage); + } + + [Fact] + public async Task ThrowsForUnknownStatus() + { + var stage = 0; + var testMessageHandler = new TestMessageHandler(async message => + { + // First PUT request + if (stage == 0) + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString()); + Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync()); + Assert.Equal(3, message.Content.Headers.ContentLength); + + AssertDefaultHeaders(message); + + stage++; + return new HttpResponseMessage(HttpStatusCode.InternalServerError); + } + + throw new NotImplementedException(); + }); + + var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler)); + await Assert.ThrowsAsync(() => blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None)); + + Assert.Equal(1, stage); + } + + [Fact] + public async Task ThrowsForUnknownStatusDuringCreation() + { + var stage = 0; + var testMessageHandler = new TestMessageHandler(async message => + { + // First PUT request + if (stage == 0) + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString()); + Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync()); + Assert.Equal(3, message.Content.Headers.ContentLength); + + AssertDefaultHeaders(message); + + stage++; + return new HttpResponseMessage(HttpStatusCode.NotFound); + } + // Create request + if (stage == 1) + { + Assert.Equal(HttpMethod.Put, message.Method); + Assert.Equal("https://host/container/blob/path?query=1", message.RequestUri.ToString()); + Assert.Equal(0, message.Content.Headers.ContentLength); + Assert.Equal(new[] { "*" }, message.Headers.GetValues("If-None-Match")); + + AssertDefaultHeaders(message); + + stage++; + return new HttpResponseMessage(HttpStatusCode.InternalServerError); + } + + throw new NotImplementedException(); + }); + + var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler)); + await Assert.ThrowsAsync(() => blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None)); + + Assert.Equal(2, stage); + } + + + private class TestMessageHandler : HttpMessageHandler + { + private readonly Func> _callback; + + public TestMessageHandler(Func> callback) + { + _callback = callback; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return await _callback(request); + } + } + } +} diff --git a/src/Logging.AzureAppServices/test/AzureBlobSinkTests.cs b/src/Logging.AzureAppServices/test/AzureBlobSinkTests.cs new file mode 100644 index 0000000000..4d9125335a --- /dev/null +++ b/src/Logging.AzureAppServices/test/AzureBlobSinkTests.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class AzureBlobSinkTests + { + DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero); + + [Fact] + public async Task WritesMessagesInBatches() + { + var blob = new Mock(); + var buffers = new List(); + blob.Setup(b => b.AppendAsync(It.IsAny>(), It.IsAny())) + .Callback((ArraySegment s, CancellationToken ct) => buffers.Add(ToArray(s))) + .Returns(Task.CompletedTask); + + var sink = new TestBlobSink(name => blob.Object); + var logger = (BatchingLogger)sink.CreateLogger("Cat"); + + await sink.IntervalControl.Pause; + + for (int i = 0; i < 5; i++) + { + logger.Log(_timestampOne, LogLevel.Information, 0, "Text " + i, null, (state, ex) => state); + } + + sink.IntervalControl.Resume(); + await sink.IntervalControl.Pause; + + Assert.Single(buffers); + Assert.Equal( + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 0" + Environment.NewLine + + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 1" + Environment.NewLine + + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 2" + Environment.NewLine + + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 3" + Environment.NewLine + + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 4" + Environment.NewLine, + Encoding.UTF8.GetString(buffers[0])); + } + + [Fact] + public async Task GroupsByHour() + { + var blob = new Mock(); + var buffers = new List(); + var names = new List(); + + blob.Setup(b => b.AppendAsync(It.IsAny>(), It.IsAny())) + .Callback((ArraySegment s, CancellationToken ct) => buffers.Add(ToArray(s))) + .Returns(Task.CompletedTask); + + var sink = new TestBlobSink(name => + { + names.Add(name); + return blob.Object; + }); + var logger = (BatchingLogger)sink.CreateLogger("Cat"); + + await sink.IntervalControl.Pause; + + var startDate = _timestampOne; + for (int i = 0; i < 3; i++) + { + logger.Log(startDate, LogLevel.Information, 0, "Text " + i, null, (state, ex) => state); + + startDate = startDate.AddHours(1); + } + + sink.IntervalControl.Resume(); + await sink.IntervalControl.Pause; + + Assert.Equal(3, buffers.Count); + + Assert.Equal("appname/2016/05/04/03/42_filename", names[0]); + Assert.Equal("appname/2016/05/04/04/42_filename", names[1]); + Assert.Equal("appname/2016/05/04/05/42_filename", names[2]); + } + + private byte[] ToArray(ArraySegment inputStream) + { + return inputStream.Array + .Skip(inputStream.Offset) + .Take(inputStream.Count) + .ToArray(); + } + } +} diff --git a/src/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs b/src/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs new file mode 100644 index 0000000000..00d7dcd58d --- /dev/null +++ b/src/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class AzureDiagnosticsConfigurationProviderTests + { + [Fact] + public void NoConfigFile() + { + var tempFolder = Path.Combine(Path.GetTempPath(), "AzureWebAppLoggerThisFolderShouldNotExist"); + + var contextMock = new Mock(); + contextMock.SetupGet(c => c.HomeFolder) + .Returns(tempFolder); + + var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(contextMock.Object); + + Assert.NotNull(config); + } + + [Fact] + public void ReadsSettingsFileAndEnvironment() + { + var tempFolder = Path.Combine(Path.GetTempPath(), "WebAppLoggerConfigurationDisabledInSettingsFile"); + + try + { + var settingsFolder = Path.Combine(tempFolder, "site", "diagnostics"); + var settingsFile = Path.Combine(settingsFolder, "settings.json"); + + if (!Directory.Exists(settingsFolder)) + { + Directory.CreateDirectory(settingsFolder); + } + Environment.SetEnvironmentVariable("RANDOM_ENVIRONMENT_VARIABLE", "USEFUL_VALUE"); + File.WriteAllText(settingsFile, @"{ ""key"":""test value"" }"); + + var contextMock = new Mock(); + contextMock.SetupGet(c => c.HomeFolder) + .Returns(tempFolder); + + var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(contextMock.Object); + + Assert.Equal("test value", config["key"]); + Assert.Equal("USEFUL_VALUE", config["RANDOM_ENVIRONMENT_VARIABLE"]); + } + finally + { + if (Directory.Exists(tempFolder)) + { + try + { + Directory.Delete(tempFolder, recursive: true); + } + catch + { + // Don't break the test if temp folder deletion fails. + } + } + } + } + } +} diff --git a/src/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs b/src/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs new file mode 100644 index 0000000000..9ab0c0cb45 --- /dev/null +++ b/src/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class BatchingLoggerProviderTests + { + private DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero); + private string _nl = Environment.NewLine; + private Regex _timeStampRegex = new Regex(@"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3} .\d{2}:\d{2} "); + + [Fact] + public async Task LogsInIntervals() + { + var provider = new TestBatchingLoggingProvider(); + var logger = (BatchingLogger)provider.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + + logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state); + logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state); + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message); + Assert.Equal("2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + _nl, provider.Batches[0][1].Message); + } + + [Fact] + public async Task IncludesScopes() + { + var provider = new TestBatchingLoggingProvider(includeScopes: true); + var factory = new LoggerFactory(new [] { provider }); + var logger = factory.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + + using (logger.BeginScope("Scope")) + { + using (logger.BeginScope("Scope2")) + { + logger.Log(LogLevel.Information, 0, "Info message", null, (state, ex) => state); + } + } + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + Assert.Matches(_timeStampRegex, provider.Batches[0][0].Message); + Assert.EndsWith( + " [Information] Cat => Scope => Scope2:" + _nl + + "Info message" + _nl, + provider.Batches[0][0].Message); + } + + [Fact] + public async Task RespectsBatchSize() + { + var provider = new TestBatchingLoggingProvider(maxBatchSize: 1); + var logger = (BatchingLogger)provider.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + + logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state); + logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state); + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + Assert.Equal(2, provider.Batches.Count); + Assert.Single(provider.Batches[0]); + Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message); + + Assert.Single(provider.Batches[1]); + Assert.Equal("2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + _nl, provider.Batches[1][0].Message); + } + + [Fact] + public async Task DropsMessagesWhenReachingMaxQueue() + { + var provider = new TestBatchingLoggingProvider(maxQueueSize: 1); + var logger = (BatchingLogger)provider.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + + logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state); + logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state); + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + Assert.Equal(2, provider.Batches[0].Length); + Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message); + Assert.Equal("1 message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this." + _nl, provider.Batches[0][1].Message); + } + + private class TestBatchingLoggingProvider: BatchingLoggerProvider + { + public List Batches { get; } = new List(); + public ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl(); + + public TestBatchingLoggingProvider(TimeSpan? interval = null, int? maxBatchSize = null, int? maxQueueSize = null, bool includeScopes = false) + : base(new OptionsWrapperMonitor(new BatchingLoggerOptions + { + FlushPeriod = interval ?? TimeSpan.FromSeconds(1), + BatchSize = maxBatchSize, + BackgroundQueueSize = maxQueueSize, + IsEnabled = true, + IncludeScopes = includeScopes + })) + { + } + + internal override Task WriteMessagesAsync(IEnumerable messages, CancellationToken token) + { + Batches.Add(messages.ToArray()); + return Task.CompletedTask; + } + + protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken) + { + return IntervalControl.IntervalAsync(); + } + } + } +} diff --git a/src/Logging.AzureAppServices/test/ConfigureOptionsTests.cs b/src/Logging.AzureAppServices/test/ConfigureOptionsTests.cs new file mode 100644 index 0000000000..46b72c7a0d --- /dev/null +++ b/src/Logging.AzureAppServices/test/ConfigureOptionsTests.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Configuration; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class ConfigureOptionsTests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + [InlineData(null)] + public void InitializesIsEnabled(bool? enabled) + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("IsEnabledKey", Convert.ToString(enabled)) + }).Build(); + + var options = new BatchingLoggerOptions(); + new BatchLoggerConfigureOptions(configuration, "IsEnabledKey").Configure(options); + + Assert.Equal(enabled ?? false, options.IsEnabled); + } + + [Fact] + public void InitializesLogDirectory() + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url") + }).Build(); + + var contextMock = new Mock(); + contextMock.SetupGet(c => c.HomeFolder).Returns("Home"); + + var options = new AzureFileLoggerOptions(); + new FileLoggerConfigureOptions(configuration, contextMock.Object).Configure(options); + + Assert.Equal(Path.Combine("Home", "LogFiles", "Application"), options.LogDirectory); + } + + [Fact] + public void InitializesBlobUriSiteInstanceAndName() + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection(new [] + { + new KeyValuePair("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url") + }).Build(); + + var contextMock = new Mock(); + contextMock.SetupGet(c => c.HomeFolder).Returns("Home"); + contextMock.SetupGet(c => c.SiteInstanceId).Returns("InstanceId"); + contextMock.SetupGet(c => c.SiteName).Returns("Name"); + + var options = new AzureBlobLoggerOptions(); + new BlobLoggerConfigureOptions(configuration, contextMock.Object).Configure(options); + + Assert.Equal("http://container/url", options.ContainerUrl); + Assert.Equal("InstanceId", options.ApplicationInstanceId); + Assert.Equal("Name", options.ApplicationName); + } + } +} diff --git a/src/Logging.AzureAppServices/test/FileLoggerTests.cs b/src/Logging.AzureAppServices/test/FileLoggerTests.cs new file mode 100644 index 0000000000..a3fcd2587d --- /dev/null +++ b/src/Logging.AzureAppServices/test/FileLoggerTests.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class FileLoggerTests: IDisposable + { + DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero); + + public FileLoggerTests() + { + TempPath = Path.GetTempFileName() + "_"; + } + + public string TempPath { get; } + + public void Dispose() + { + try + { + if (Directory.Exists(TempPath)) + { + Directory.Delete(TempPath, true); + } + } + catch + { + // ignored + } + } + + [Fact] + public async Task WritesToTextFile() + { + var provider = new TestFileLoggerProvider(TempPath); + var logger = (BatchingLogger)provider.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + + logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state); + logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state); + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + Assert.Equal( + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + Environment.NewLine + + "2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + Environment.NewLine, + File.ReadAllText(Path.Combine(TempPath, "LogFile.20160504.txt"))); + } + + [Fact] + public async Task RollsTextFile() + { + var provider = new TestFileLoggerProvider(TempPath); + var logger = (BatchingLogger)provider.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + + logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state); + logger.Log(_timestampOne.AddDays(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state); + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + Assert.Equal( + "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + Environment.NewLine, + File.ReadAllText(Path.Combine(TempPath, "LogFile.20160504.txt"))); + + Assert.Equal( + "2016-05-05 03:02:01.000 +00:00 [Error] Cat: Error message" + Environment.NewLine, + File.ReadAllText(Path.Combine(TempPath, "LogFile.20160505.txt"))); + } + + [Fact] + public async Task RespectsMaxFileCount() + { + Directory.CreateDirectory(TempPath); + File.WriteAllText(Path.Combine(TempPath, "randomFile.txt"), "Text"); + + var provider = new TestFileLoggerProvider(TempPath, maxRetainedFiles: 5); + var logger = (BatchingLogger)provider.CreateLogger("Cat"); + + await provider.IntervalControl.Pause; + var timestamp = _timestampOne; + + for (int i = 0; i < 10; i++) + { + logger.Log(timestamp, LogLevel.Information, 0, "Info message", null, (state, ex) => state); + logger.Log(timestamp.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state); + + timestamp = timestamp.AddDays(1); + } + + provider.IntervalControl.Resume(); + await provider.IntervalControl.Pause; + + var actualFiles = new DirectoryInfo(TempPath) + .GetFiles() + .Select(f => f.Name) + .OrderBy(f => f) + .ToArray(); + + Assert.Equal(6, actualFiles.Length); + Assert.Equal(new[] { + "LogFile.20160509.txt", + "LogFile.20160510.txt", + "LogFile.20160511.txt", + "LogFile.20160512.txt", + "LogFile.20160513.txt", + "randomFile.txt" + }, actualFiles); + } + } +} diff --git a/src/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs b/src/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs new file mode 100644 index 0000000000..cf8bede1a5 --- /dev/null +++ b/src/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class LoggerBuilderExtensionsTests + { + private IWebAppContext _appContext; + + public LoggerBuilderExtensionsTests() + { + var contextMock = new Mock(); + contextMock.SetupGet(c => c.IsRunningInAzureWebApp).Returns(true); + contextMock.SetupGet(c => c.HomeFolder).Returns("."); + _appContext = contextMock.Object; + } + + [Fact] + public void BuilderExtensionAddsSingleSetOfServicesWhenCalledTwice() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext)); + var count = serviceCollection.Count; + + Assert.NotEqual(0, count); + + serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext)); + + Assert.Equal(count, serviceCollection.Count); + } + + [Fact] + public void BuilderExtensionAddsConfigurationChangeTokenSource() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => builder.AddConfiguration(new ConfigurationBuilder().Build())); + + // Tracking for main configuration + Assert.Equal(1, serviceCollection.Count(d => d.ServiceType == typeof(IOptionsChangeTokenSource))); + + serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext)); + + // Make sure we add another config change token for azure diagnostic configuration + Assert.Equal(2, serviceCollection.Count(d => d.ServiceType == typeof(IOptionsChangeTokenSource))); + } + + [Fact] + public void BuilderExtensionAddsIConfigureOptions() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => builder.AddConfiguration(new ConfigurationBuilder().Build())); + + // Tracking for main configuration + Assert.Equal(2, serviceCollection.Count(d => d.ServiceType == typeof(IConfigureOptions))); + + serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext)); + + Assert.Equal(4, serviceCollection.Count(d => d.ServiceType == typeof(IConfigureOptions))); + } + + [Fact] + public void LoggerProviderIsResolvable() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext)); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var loggerFactory = serviceProvider.GetService(); + } + } +} diff --git a/src/Logging.AzureAppServices/test/ManualIntervalControl.cs b/src/Logging.AzureAppServices/test/ManualIntervalControl.cs new file mode 100644 index 0000000000..29cc883a28 --- /dev/null +++ b/src/Logging.AzureAppServices/test/ManualIntervalControl.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + internal class ManualIntervalControl + { + + private TaskCompletionSource _pauseCompletionSource = new TaskCompletionSource(); + private TaskCompletionSource _resumeCompletionSource; + + public Task Pause => _pauseCompletionSource.Task; + + public void Resume() + { + _pauseCompletionSource = new TaskCompletionSource(); + _resumeCompletionSource.SetResult(null); + } + + public async Task IntervalAsync() + { + _resumeCompletionSource = new TaskCompletionSource(); + _pauseCompletionSource.SetResult(null); + + await _resumeCompletionSource.Task; + } + } +} \ No newline at end of file diff --git a/src/Logging.AzureAppServices/test/Microsoft.Extensions.Logging.AzureAppServices.Tests.csproj b/src/Logging.AzureAppServices/test/Microsoft.Extensions.Logging.AzureAppServices.Tests.csproj new file mode 100644 index 0000000000..7365c79076 --- /dev/null +++ b/src/Logging.AzureAppServices/test/Microsoft.Extensions.Logging.AzureAppServices.Tests.csproj @@ -0,0 +1,28 @@ + + + + $(DefaultNetCoreTargetFramework);net472 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Logging.AzureAppServices/test/OptionsWrapperMonitor.cs b/src/Logging.AzureAppServices/test/OptionsWrapperMonitor.cs new file mode 100644 index 0000000000..fbc531c26d --- /dev/null +++ b/src/Logging.AzureAppServices/test/OptionsWrapperMonitor.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. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + internal class OptionsWrapperMonitor : IOptionsMonitor + { + public OptionsWrapperMonitor(T currentValue) + { + CurrentValue = currentValue; + } + + public IDisposable OnChange(Action listener) + { + return null; + } + + public T Get(string name) => CurrentValue; + + public T CurrentValue { get; } + } +} \ No newline at end of file diff --git a/src/Logging.AzureAppServices/test/TestBlobSink.cs b/src/Logging.AzureAppServices/test/TestBlobSink.cs new file mode 100644 index 0000000000..4b9ec445be --- /dev/null +++ b/src/Logging.AzureAppServices/test/TestBlobSink.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + internal class TestBlobSink : BlobLoggerProvider + { + internal ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl(); + + public TestBlobSink(Func blobReferenceFactory) : base( + new OptionsWrapperMonitor(new AzureBlobLoggerOptions() + { + ApplicationInstanceId = "42", + ApplicationName = "appname", + BlobName = "filename", + IsEnabled = true + }), + blobReferenceFactory) + { + } + + protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken) + { + return IntervalControl.IntervalAsync(); + } + } +} diff --git a/src/Logging.AzureAppServices/test/TestFileLoggerProvider.cs b/src/Logging.AzureAppServices/test/TestFileLoggerProvider.cs new file mode 100644 index 0000000000..60fbb88bd8 --- /dev/null +++ b/src/Logging.AzureAppServices/test/TestFileLoggerProvider.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + internal class TestFileLoggerProvider : FileLoggerProvider + { + internal ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl(); + + public TestFileLoggerProvider( + string path, + string fileName = "LogFile.", + int maxFileSize = 32_000, + int maxRetainedFiles = 100) + : base(new OptionsWrapperMonitor(new AzureFileLoggerOptions() + { + LogDirectory = path, + FileName = fileName, + FileSizeLimit = maxFileSize, + RetainedFileCountLimit = maxRetainedFiles, + IsEnabled = true + })) + { + } + + protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken) + { + return IntervalControl.IntervalAsync(); + } + } +} diff --git a/src/Logging.AzureAppServices/test/WebConfigurationLevelSwitchTests.cs b/src/Logging.AzureAppServices/test/WebConfigurationLevelSwitchTests.cs new file mode 100644 index 0000000000..f933b9f2fa --- /dev/null +++ b/src/Logging.AzureAppServices/test/WebConfigurationLevelSwitchTests.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. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Microsoft.Extensions.Logging.AzureAppServices.Test +{ + public class WebConfigurationLevelSwitchTests + { + [Theory] + [InlineData("Error", LogLevel.Error)] + [InlineData("Warning", LogLevel.Warning)] + [InlineData("Information", LogLevel.Information)] + [InlineData("Verbose", LogLevel.Trace)] + [InlineData("ABCD", LogLevel.None)] + public void AddsRuleWithCorrectLevel(string levelValue, LogLevel expectedLevel) + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection( + new[] + { + new KeyValuePair("levelKey", levelValue), + }) + .Build(); + + var levelSwitcher = new ConfigurationBasedLevelSwitcher(configuration, typeof(TestFileLoggerProvider), "levelKey"); + + var filterConfiguration = new LoggerFilterOptions(); + levelSwitcher.Configure(filterConfiguration); + + Assert.Equal(1, filterConfiguration.Rules.Count); + + var rule = filterConfiguration.Rules[0]; + Assert.Equal(typeof(TestFileLoggerProvider).FullName, rule.ProviderName); + Assert.Equal(expectedLevel, rule.LogLevel); + } + } +} diff --git a/src/Middleware/.vsconfig b/src/Middleware/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Middleware/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/MusicStore/.vsconfig b/src/MusicStore/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/MusicStore/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Mvc/.vsconfig b/src/Mvc/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Mvc/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/ProjectTemplates/.vsconfig b/src/ProjectTemplates/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/ProjectTemplates/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/ProjectTemplates/test/BlazorServerTemplateTest.cs b/src/ProjectTemplates/test/BlazorServerTemplateTest.cs index 6d7a5ac7f8..8497ae2e6c 100644 --- a/src/ProjectTemplates/test/BlazorServerTemplateTest.cs +++ b/src/ProjectTemplates/test/BlazorServerTemplateTest.cs @@ -85,6 +85,7 @@ namespace Templates.Test [InlineData(true)] [InlineData(false)] [SkipOnHelix("ef restore no worky")] + [QuarantinedTest] public async Task BlazorServerTemplateWorks_IndividualAuth(bool useLocalDB) { Project = await ProjectFactory.GetOrCreateProject("blazorserverindividual" + (useLocalDB ? "uld" : ""), Output); diff --git a/src/ProjectTemplates/test/Helpers/Project.cs b/src/ProjectTemplates/test/Helpers/Project.cs index 68642c48f4..64f21104c4 100644 --- a/src/ProjectTemplates/test/Helpers/Project.cs +++ b/src/ProjectTemplates/test/Helpers/Project.cs @@ -27,11 +27,11 @@ namespace Templates.Test.Helpers public static bool IsCIEnvironment => typeof(Project).Assembly.GetCustomAttributes() .Any(a => a.Key == "ContinuousIntegrationBuild"); - public static string ArtifactsLogDir => (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HELIX_DIR"))) + public static string ArtifactsLogDir => (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"))) ? GetAssemblyMetadata("ArtifactsLogDir") - : Path.Combine(Environment.GetEnvironmentVariable("HELIX_DIR"), "logs"); - - public static string DotNetEfFullPath => (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DotNetEfFullPath"))) + : Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"); + + public static string DotNetEfFullPath => (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DotNetEfFullPath"))) ? typeof(ProjectFactoryFixture).Assembly.GetCustomAttributes() .First(attribute => attribute.Key == "DotNetEfFullPath") .Value @@ -309,7 +309,7 @@ namespace Templates.Test.Helpers internal async Task RunDotNetEfCreateMigrationAsync(string migrationName) { var args = $"--verbose --no-build migrations add {migrationName}"; - + // Only run one instance of 'dotnet new' at once, as a workaround for // https://github.com/aspnet/templating/issues/63 await DotNetNewLock.WaitAsync(); @@ -324,7 +324,7 @@ namespace Templates.Test.Helpers { command = "dotnet-ef"; } - + var result = ProcessEx.Run(Output, TemplateOutputDir, command, args); await result.Exited; return result; @@ -353,7 +353,7 @@ namespace Templates.Test.Helpers { command = "dotnet-ef"; } - + var result = ProcessEx.Run(Output, TemplateOutputDir, command, args); await result.Exited; return result; diff --git a/src/Razor/.vsconfig b/src/Razor/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Razor/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Security/.vsconfig b/src/Security/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Security/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs b/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs index bc18f861b5..0979640207 100644 --- a/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs +++ b/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs @@ -21,7 +21,6 @@ using Xunit; namespace Microsoft.AspNetCore.Authentication.Negotiate { - [QuarantinedTest] public class EventTests { [Fact] diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs b/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs index 89685b286b..0b0b7b14cd 100644 --- a/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs +++ b/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs @@ -22,7 +22,6 @@ using Xunit.Sdk; namespace Microsoft.AspNetCore.Authentication.Negotiate { - [QuarantinedTest] public class NegotiateHandlerTests { [Fact] diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.Test/ServerDeferralTests.cs b/src/Security/Authentication/Negotiate/test/Negotiate.Test/ServerDeferralTests.cs index e5cf8ca7b6..efd513b829 100644 --- a/src/Security/Authentication/Negotiate/test/Negotiate.Test/ServerDeferralTests.cs +++ b/src/Security/Authentication/Negotiate/test/Negotiate.Test/ServerDeferralTests.cs @@ -15,7 +15,6 @@ using Xunit; namespace Microsoft.AspNetCore.Authentication.Negotiate { - [QuarantinedTest] public class ServerDeferralTests { [Fact] diff --git a/src/Servers/HttpSys/.vsconfig b/src/Servers/HttpSys/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Servers/HttpSys/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Servers/IIS/.vsconfig b/src/Servers/IIS/.vsconfig new file mode 100644 index 0000000000..8f411e8f86 --- /dev/null +++ b/src/Servers/IIS/.vsconfig @@ -0,0 +1,16 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Component.VC.ATL", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "Microsoft.VisualStudio.Component.Windows10SDK.17134", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NativeDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs index b0847f84a1..5503c2b4fa 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs @@ -89,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests } catch (Exception ex) { - Logger.LogError($"Certificate is invalid. Issuer name: {cert.Issuer}"); + Logger.LogError($"Certificate is invalid. Issuer name: {cert?.Issuer}"); using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine)) { Logger.LogError($"List of current certificates in root store:"); diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs index 5f6fed86e9..c030a87161 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs @@ -212,7 +212,6 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/20153")] public async Task DetectsOverriddenServer() { var deploymentParameters = Fixture.GetBaseDeploymentParameters(Fixture.InProcessTestSite); @@ -230,7 +229,6 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/20153")] public async Task LogsStartupExceptionExitError() { var deploymentParameters = Fixture.GetBaseDeploymentParameters(Fixture.InProcessTestSite); @@ -709,7 +707,6 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess [InlineData("DOTNET_ENVIRONMENT", "deVelopment")] [InlineData("ASPNETCORE_DETAILEDERRORS", "1")] [InlineData("ASPNETCORE_DETAILEDERRORS", "TRUE")] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/20153")] public async Task ExceptionIsLoggedToEventLogAndPutInResponseWhenDeveloperExceptionPageIsEnabled(string environmentVariable, string value) { var deploymentParameters = Fixture.GetBaseDeploymentParameters(); @@ -734,7 +731,6 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess [ConditionalFact] [RequiresNewHandler] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/20153")] public async Task ExceptionIsLoggedToEventLogAndPutInResponseWhenDeveloperExceptionPageIsEnabledViaWebConfig() { var deploymentParameters = Fixture.GetBaseDeploymentParameters(); @@ -762,7 +758,6 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess [RequiresNewHandler] [InlineData("ThrowInStartup")] [InlineData("ThrowInStartupGenericHost")] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/20153")] public async Task ExceptionIsLoggedToEventLogAndPutInResponseDuringHostingStartupProcess(string startupType) { var deploymentParameters = Fixture.GetBaseDeploymentParameters(); @@ -785,7 +780,6 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess [ConditionalFact] [RequiresIIS(IISCapability.PoolEnvironmentVariables)] [RequiresNewHandler] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/20153")] public async Task ExceptionIsNotLoggedToResponseWhenStartupHookIsDisabled() { var deploymentParameters = Fixture.GetBaseDeploymentParameters(); @@ -808,7 +802,6 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess [ConditionalFact] [RequiresNewHandler] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/20153")] public async Task ExceptionIsLoggedToEventLogDoesNotWriteToResponse() { var deploymentParameters = Fixture.GetBaseDeploymentParameters(); diff --git a/src/Servers/IIS/IIS/test/testassets/IIS.Common.TestLib/IIS.Common.TestLib.csproj b/src/Servers/IIS/IIS/test/testassets/IIS.Common.TestLib/IIS.Common.TestLib.csproj index ced51607d8..e4ff5df0cc 100644 --- a/src/Servers/IIS/IIS/test/testassets/IIS.Common.TestLib/IIS.Common.TestLib.csproj +++ b/src/Servers/IIS/IIS/test/testassets/IIS.Common.TestLib/IIS.Common.TestLib.csproj @@ -7,8 +7,8 @@ + - diff --git a/src/Servers/Kestrel/.vsconfig b/src/Servers/Kestrel/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Servers/Kestrel/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs index 6673afaa1c..cfbd7ab1e2 100644 --- a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs +++ b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs @@ -127,6 +127,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core { public KestrelServerOptions() { } public bool AddServerHeader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public bool AllowResponseHeaderCompression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public bool AllowSynchronousIO { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index f84ed1d2ce..1d270be8ee 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -599,4 +599,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l Unable to resolve service for type 'Microsoft.AspNetCore.Connections.IConnectionListenerFactory' while attempting to activate 'Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer'. + + A value greater than or equal to zero is required. + \ No newline at end of file diff --git a/src/Servers/Kestrel/Core/src/Http2Limits.cs b/src/Servers/Kestrel/Core/src/Http2Limits.cs index 68d101f076..713159f66a 100644 --- a/src/Servers/Kestrel/Core/src/Http2Limits.cs +++ b/src/Servers/Kestrel/Core/src/Http2Limits.cs @@ -39,9 +39,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } /// - /// Limits the size of the header compression table, in octets, the HPACK decoder on the server can use. + /// Limits the size of the header compression tables, in octets, the HPACK encoder and decoder on the server can use. /// - /// Value must be greater than 0, defaults to 4096 + /// Value must be greater than or equal to 0, defaults to 4096 /// /// public int HeaderTableSize @@ -49,9 +49,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core get => _headerTableSize; set { - if (value <= 0) + if (value < 0) { - throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanZeroRequired); + throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanOrEqualToZeroRequired); } _headerTableSize = value; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 539c386efa..6efacd2e74 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -547,6 +547,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { try { + // We run the request processing loop in a seperate async method so per connection + // exception handling doesn't complicate the generated asm for the loop. await ProcessRequests(application); } catch (BadHttpRequestException ex) @@ -624,91 +626,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http InitializeBodyControl(messageBody); - var context = application.CreateContext(this); - - try - { - KestrelEventSource.Log.RequestStart(this); - - // Run the application code for this request - await application.ProcessRequestAsync(context); - - // Trigger OnStarting if it hasn't been called yet and the app hasn't - // already failed. If an OnStarting callback throws we can go through - // our normal error handling in ProduceEnd. - // https://github.com/aspnet/KestrelHttpServer/issues/43 - if (!HasResponseStarted && _applicationException == null && _onStarting?.Count > 0) - { - await FireOnStarting(); - } - - if (!_connectionAborted && !VerifyResponseContentLength(out var lengthException)) - { - ReportApplicationError(lengthException); - } - } - catch (BadHttpRequestException ex) - { - // Capture BadHttpRequestException for further processing - // This has to be caught here so StatusCode is set properly before disposing the HttpContext - // (DisposeContext logs StatusCode). - SetBadRequestState(ex); - ReportApplicationError(ex); - } - catch (Exception ex) - { - ReportApplicationError(ex); - } - - KestrelEventSource.Log.RequestStop(this); - - // At this point all user code that needs use to the request or response streams has completed. - // Using these streams in the OnCompleted callback is not allowed. - try - { - await _bodyControl.StopAsync(); - } - catch (Exception ex) - { - // BodyControl.StopAsync() can throw if the PipeWriter was completed prior to the application writing - // enough bytes to satisfy the specified Content-Length. This risks double-logging the exception, - // but this scenario generally indicates an app bug, so I don't want to risk not logging it. - ReportApplicationError(ex); - } - - // 4XX responses are written by TryProduceInvalidRequestResponse during connection tear down. - if (_requestRejectedException == null) - { - if (!_connectionAborted) - { - // Call ProduceEnd() before consuming the rest of the request body to prevent - // delaying clients waiting for the chunk terminator: - // - // https://github.com/dotnet/corefx/issues/17330#issuecomment-288248663 - // - // This also prevents the 100 Continue response from being sent if the app - // never tried to read the body. - // https://github.com/aspnet/KestrelHttpServer/issues/2102 - // - // ProduceEnd() must be called before _application.DisposeContext(), to ensure - // HttpContext.Response.StatusCode is correctly set when - // IHttpContextFactory.Dispose(HttpContext) is called. - await ProduceEnd(); - } - else if (!HasResponseStarted) - { - // If the request was aborted and no response was sent, there's no - // meaningful status code to log. - StatusCode = 0; - } - } - - if (_onCompleted?.Count > 0) - { - await FireOnCompleted(); - } - - application.DisposeContext(context, _applicationException); + // We run user controlled request processing in a seperate async method + // so any changes made to ExecutionContext are undone when it returns and + // each request starts with a fresh ExecutionContext state. + await ProcessRequest(application); // Even for non-keep-alive requests, try to consume the entire body to avoid RSTs. if (!_connectionAborted && _requestRejectedException == null && !messageBody.IsEmpty) @@ -723,6 +644,95 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } + private async ValueTask ProcessRequest(IHttpApplication application) + { + var context = application.CreateContext(this); + + try + { + KestrelEventSource.Log.RequestStart(this); + + // Run the application code for this request + await application.ProcessRequestAsync(context); + + // Trigger OnStarting if it hasn't been called yet and the app hasn't + // already failed. If an OnStarting callback throws we can go through + // our normal error handling in ProduceEnd. + // https://github.com/aspnet/KestrelHttpServer/issues/43 + if (!HasResponseStarted && _applicationException == null && _onStarting?.Count > 0) + { + await FireOnStarting(); + } + + if (!_connectionAborted && !VerifyResponseContentLength(out var lengthException)) + { + ReportApplicationError(lengthException); + } + } + catch (BadHttpRequestException ex) + { + // Capture BadHttpRequestException for further processing + // This has to be caught here so StatusCode is set properly before disposing the HttpContext + // (DisposeContext logs StatusCode). + SetBadRequestState(ex); + ReportApplicationError(ex); + } + catch (Exception ex) + { + ReportApplicationError(ex); + } + + KestrelEventSource.Log.RequestStop(this); + + // At this point all user code that needs use to the request or response streams has completed. + // Using these streams in the OnCompleted callback is not allowed. + try + { + await _bodyControl.StopAsync(); + } + catch (Exception ex) + { + // BodyControl.StopAsync() can throw if the PipeWriter was completed prior to the application writing + // enough bytes to satisfy the specified Content-Length. This risks double-logging the exception, + // but this scenario generally indicates an app bug, so I don't want to risk not logging it. + ReportApplicationError(ex); + } + + // 4XX responses are written by TryProduceInvalidRequestResponse during connection tear down. + if (_requestRejectedException == null) + { + if (!_connectionAborted) + { + // Call ProduceEnd() before consuming the rest of the request body to prevent + // delaying clients waiting for the chunk terminator: + // + // https://github.com/dotnet/corefx/issues/17330#issuecomment-288248663 + // + // This also prevents the 100 Continue response from being sent if the app + // never tried to read the body. + // https://github.com/aspnet/KestrelHttpServer/issues/2102 + // + // ProduceEnd() must be called before _application.DisposeContext(), to ensure + // HttpContext.Response.StatusCode is correctly set when + // IHttpContextFactory.Dispose(HttpContext) is called. + await ProduceEnd(); + } + else if (!HasResponseStarted) + { + // If the request was aborted and no response was sent, there's no + // meaningful status code to log. + StatusCode = 0; + } + } + + if (_onCompleted?.Count > 0) + { + await FireOnCompleted(); + } + + application.DisposeContext(context, _applicationException); + } + public void OnStarting(Func callback, object state) { if (HasResponseStarted) @@ -749,108 +759,55 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http protected Task FireOnStarting() { var onStarting = _onStarting; - - if (onStarting == null || onStarting.Count == 0) + if (onStarting?.Count > 0) { - return Task.CompletedTask; - } - else - { - return FireOnStartingMayAwait(onStarting); - } - } - - private Task FireOnStartingMayAwait(Stack, object>> onStarting) - { - try - { - while (onStarting.TryPop(out var entry)) - { - var task = entry.Key.Invoke(entry.Value); - if (!task.IsCompletedSuccessfully) - { - return FireOnStartingAwaited(task, onStarting); - } - } - } - catch (Exception ex) - { - ReportApplicationError(ex); + return ProcessEvents(this, onStarting); } return Task.CompletedTask; - } - private async Task FireOnStartingAwaited(Task currentTask, Stack, object>> onStarting) - { - try + static async Task ProcessEvents(HttpProtocol protocol, Stack, object>> events) { - await currentTask; - - while (onStarting.TryPop(out var entry)) + // Try/Catch is outside the loop as any error that occurs is before the request starts. + // So we want to report it as an ApplicationError to fail the request and not process more events. + try { - await entry.Key.Invoke(entry.Value); + while (events.TryPop(out var entry)) + { + await entry.Key.Invoke(entry.Value); + } + } + catch (Exception ex) + { + protocol.ReportApplicationError(ex); } - } - catch (Exception ex) - { - ReportApplicationError(ex); } } protected Task FireOnCompleted() { var onCompleted = _onCompleted; - - if (onCompleted == null || onCompleted.Count == 0) + if (onCompleted?.Count > 0) { - return Task.CompletedTask; - } - - return FireOnCompletedMayAwait(onCompleted); - } - - private Task FireOnCompletedMayAwait(Stack, object>> onCompleted) - { - while (onCompleted.TryPop(out var entry)) - { - try - { - var task = entry.Key.Invoke(entry.Value); - if (!task.IsCompletedSuccessfully) - { - return FireOnCompletedAwaited(task, onCompleted); - } - } - catch (Exception ex) - { - ReportApplicationError(ex); - } + return ProcessEvents(this, onCompleted); } return Task.CompletedTask; - } - private async Task FireOnCompletedAwaited(Task currentTask, Stack, object>> onCompleted) - { - try + static async Task ProcessEvents(HttpProtocol protocol, Stack, object>> events) { - await currentTask; - } - catch (Exception ex) - { - Log.ApplicationError(ConnectionId, TraceIdentifier, ex); - } - - while (onCompleted.TryPop(out var entry)) - { - try + // Try/Catch is inside the loop as any error that occurs is after the request has finished. + // So we will just log it and keep processing the events, as the completion has already happened. + while (events.TryPop(out var entry)) { - await entry.Key.Invoke(entry.Value); - } - catch (Exception ex) - { - Log.ApplicationError(ConnectionId, TraceIdentifier, ex); + try + { + await entry.Key.Invoke(entry.Value); + } + catch (Exception ex) + { + protocol.Log.ApplicationError(protocol.ConnectionId, protocol.TraceIdentifier, ex); + } } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs index 1598a18c7f..33c7b920f3 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs @@ -4,7 +4,6 @@ using System; using System.Net.Http; using System.Net.Http.HPack; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { @@ -13,57 +12,105 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 /// /// Begin encoding headers in the first HEADERS frame. /// - public static bool BeginEncodeHeaders(int statusCode, Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) + public static bool BeginEncodeHeaders(int statusCode, HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) { - if (!HPackEncoder.EncodeStatusHeader(statusCode, buffer, out var statusCodeLength)) + length = 0; + + if (!hpackEncoder.EnsureDynamicTableSizeUpdate(buffer, out var sizeUpdateLength)) { throw new HPackEncodingException(SR.net_http_hpack_encode_failure); } + length += sizeUpdateLength; + + if (!EncodeStatusHeader(statusCode, hpackEncoder, buffer.Slice(length), out var statusCodeLength)) + { + throw new HPackEncodingException(SR.net_http_hpack_encode_failure); + } + length += statusCodeLength; if (!headersEnumerator.MoveNext()) { - length = statusCodeLength; return true; } // We're ok with not throwing if no headers were encoded because we've already encoded the status. // There is a small chance that the header will encode if there is no other content in the next HEADERS frame. - var done = EncodeHeaders(headersEnumerator, buffer.Slice(statusCodeLength), throwIfNoneEncoded: false, out var headersLength); - length = statusCodeLength + headersLength; - + var done = EncodeHeadersCore(hpackEncoder, headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: false, out var headersLength); + length += headersLength; return done; } /// /// Begin encoding headers in the first HEADERS frame. /// - public static bool BeginEncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) + public static bool BeginEncodeHeaders(HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) { + length = 0; + + if (!hpackEncoder.EnsureDynamicTableSizeUpdate(buffer, out var sizeUpdateLength)) + { + throw new HPackEncodingException(SR.net_http_hpack_encode_failure); + } + length += sizeUpdateLength; + if (!headersEnumerator.MoveNext()) { - length = 0; return true; } - return EncodeHeaders(headersEnumerator, buffer, throwIfNoneEncoded: true, out length); + var done = EncodeHeadersCore(hpackEncoder, headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: true, out var headersLength); + length += headersLength; + return done; } /// /// Continue encoding headers in the next HEADERS frame. The enumerator should already have a current value. /// - public static bool ContinueEncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) + public static bool ContinueEncodeHeaders(HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) { - return EncodeHeaders(headersEnumerator, buffer, throwIfNoneEncoded: true, out length); + return EncodeHeadersCore(hpackEncoder, headersEnumerator, buffer, throwIfNoneEncoded: true, out length); } - private static bool EncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span buffer, bool throwIfNoneEncoded, out int length) + private static bool EncodeStatusHeader(int statusCode, HPackEncoder hpackEncoder, Span buffer, out int length) + { + switch (statusCode) + { + case 200: + case 204: + case 206: + case 304: + case 400: + case 404: + case 500: + // Status codes which exist in the HTTP/2 StaticTable. + return HPackEncoder.EncodeIndexedHeaderField(H2StaticTable.StatusIndex[statusCode], buffer, out length); + default: + const string name = ":status"; + var value = StatusCodes.ToStatusString(statusCode); + return hpackEncoder.EncodeHeader(buffer, H2StaticTable.Status200, HeaderEncodingHint.Index, name, value, out length); + } + } + + private static bool EncodeHeadersCore(HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span buffer, bool throwIfNoneEncoded, out int length) { var currentLength = 0; do { - if (!EncodeHeader(headersEnumerator.KnownHeaderType, headersEnumerator.Current.Key, headersEnumerator.Current.Value, buffer.Slice(currentLength), out int headerLength)) + var staticTableId = headersEnumerator.HPackStaticTableId; + var name = headersEnumerator.Current.Key; + var value = headersEnumerator.Current.Value; + + var hint = ResolveHeaderEncodingHint(staticTableId, name); + + if (!hpackEncoder.EncodeHeader( + buffer.Slice(currentLength), + staticTableId, + hint, + name, + value, + out var headerLength)) { - // The the header wasn't written and no headers have been written then the header is too large. + // If the header wasn't written, and no headers have been written, then the header is too large. // Throw an error to avoid an infinite loop of attempting to write large header. if (currentLength == 0 && throwIfNoneEncoded) { @@ -79,79 +126,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 while (headersEnumerator.MoveNext()); length = currentLength; - return true; } - private static bool EncodeHeader(KnownHeaderType knownHeaderType, string name, string value, Span buffer, out int length) + private static HeaderEncodingHint ResolveHeaderEncodingHint(int staticTableId, string name) { - var hPackStaticTableId = GetResponseHeaderStaticTableId(knownHeaderType); - - if (hPackStaticTableId == -1) + HeaderEncodingHint hint; + if (IsSensitive(staticTableId, name)) { - return HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out length); + hint = HeaderEncodingHint.NeverIndex; + } + else if (IsNotDynamicallyIndexed(staticTableId)) + { + hint = HeaderEncodingHint.IgnoreIndex; } else { - return HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexing(hPackStaticTableId, value, buffer, out length); + hint = HeaderEncodingHint.Index; } + + return hint; } - private static int GetResponseHeaderStaticTableId(KnownHeaderType responseHeaderType) + private static bool IsSensitive(int staticTableIndex, string name) { - switch (responseHeaderType) + // Set-Cookie could contain sensitive data. + if (staticTableIndex == H2StaticTable.SetCookie) { - case KnownHeaderType.CacheControl: - return H2StaticTable.CacheControl; - case KnownHeaderType.Date: - return H2StaticTable.Date; - case KnownHeaderType.TransferEncoding: - return H2StaticTable.TransferEncoding; - case KnownHeaderType.Via: - return H2StaticTable.Via; - case KnownHeaderType.Allow: - return H2StaticTable.Allow; - case KnownHeaderType.ContentType: - return H2StaticTable.ContentType; - case KnownHeaderType.ContentEncoding: - return H2StaticTable.ContentEncoding; - case KnownHeaderType.ContentLanguage: - return H2StaticTable.ContentLanguage; - case KnownHeaderType.ContentLocation: - return H2StaticTable.ContentLocation; - case KnownHeaderType.ContentRange: - return H2StaticTable.ContentRange; - case KnownHeaderType.Expires: - return H2StaticTable.Expires; - case KnownHeaderType.LastModified: - return H2StaticTable.LastModified; - case KnownHeaderType.AcceptRanges: - return H2StaticTable.AcceptRanges; - case KnownHeaderType.Age: - return H2StaticTable.Age; - case KnownHeaderType.ETag: - return H2StaticTable.ETag; - case KnownHeaderType.Location: - return H2StaticTable.Location; - case KnownHeaderType.ProxyAuthenticate: - return H2StaticTable.ProxyAuthenticate; - case KnownHeaderType.RetryAfter: - return H2StaticTable.RetryAfter; - case KnownHeaderType.Server: - return H2StaticTable.Server; - case KnownHeaderType.SetCookie: - return H2StaticTable.SetCookie; - case KnownHeaderType.Vary: - return H2StaticTable.Vary; - case KnownHeaderType.WWWAuthenticate: - return H2StaticTable.WwwAuthenticate; - case KnownHeaderType.AccessControlAllowOrigin: - return H2StaticTable.AccessControlAllowOrigin; - case KnownHeaderType.ContentLength: - return H2StaticTable.ContentLength; - default: - return -1; + return true; } + if (string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + + private static bool IsNotDynamicallyIndexed(int staticTableIndex) + { + // Content-Length is added to static content. Content length is different for each + // file, and is unlikely to be reused because of browser caching. + return staticTableIndex == H2StaticTable.ContentLength; } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 8666bebc64..acf81b3e12 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 httpLimits.MinResponseDataRate, context.ConnectionId, context.MemoryPool, - context.ServiceContext.Log); + context.ServiceContext); var inputOptions = new PipeOptions(pool: context.MemoryPool, readerScheduler: context.ServiceContext.Scheduler, @@ -743,6 +743,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } + // Maximum HPack encoder size is limited by Http2Limits.HeaderTableSize, configured max the server. + // + // Note that the client HPack decoder doesn't care about the ACK so we don't need to lock sending the + // ACK and updating the table size on the server together. + // The client will wait until a size agreed upon by it (sent in SETTINGS_HEADER_TABLE_SIZE) and the + // server (sent as a dynamic table size update in the next HEADERS frame) is received before applying + // the new size. + _frameWriter.UpdateMaxHeaderTableSize(Math.Min(_clientSettings.HeaderTableSize, (uint)Limits.Http2.HeaderTableSize)); + return ackTask.AsTask(); } catch (Http2SettingsParameterOutOfRangeException ex) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index fd8d553067..ca428eae8a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -38,6 +38,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private readonly ITimeoutControl _timeoutControl; private readonly MinDataRate _minResponseDataRate; private readonly TimingPipeFlusher _flusher; + private readonly HPackEncoder _hpackEncoder; private uint _maxFrameSize = Http2PeerSettings.MinAllowedMaxFrameSize; private byte[] _headerEncodingBuffer; @@ -55,7 +56,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 MinDataRate minResponseDataRate, string connectionId, MemoryPool memoryPool, - IKestrelTrace log) + ServiceContext serviceContext) { // Allow appending more data to the PipeWriter when a flush is pending. _outputWriter = new ConcurrentPipeWriter(outputPipeWriter, memoryPool, _writeLock); @@ -63,12 +64,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _http2Connection = http2Connection; _connectionOutputFlowControl = connectionOutputFlowControl; _connectionId = connectionId; - _log = log; + _log = serviceContext.Log; _timeoutControl = timeoutControl; _minResponseDataRate = minResponseDataRate; - _flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, log); + _flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, serviceContext.Log); _outgoingFrame = new Http2Frame(); _headerEncodingBuffer = new byte[_maxFrameSize]; + + _hpackEncoder = new HPackEncoder(serviceContext.ServerOptions.AllowResponseHeaderCompression); + } + + public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize) + { + lock (_writeLock) + { + _hpackEncoder.UpdateMaxHeaderTableSize(maxHeaderTableSize); + } } public void UpdateMaxFrameSize(uint maxFrameSize) @@ -175,7 +186,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _headersEnumerator.Initialize(headers); _outgoingFrame.PrepareHeaders(headerFrameFlags, streamId); var buffer = _headerEncodingBuffer.AsSpan(); - var done = HPackHeaderWriter.BeginEncodeHeaders(statusCode, _headersEnumerator, buffer, out var payloadLength); + var done = HPackHeaderWriter.BeginEncodeHeaders(statusCode, _hpackEncoder, _headersEnumerator, buffer, out var payloadLength); FinishWritingHeaders(streamId, payloadLength, done); } catch (HPackEncodingException hex) @@ -201,7 +212,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _headersEnumerator.Initialize(headers); _outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.END_STREAM, streamId); var buffer = _headerEncodingBuffer.AsSpan(); - var done = HPackHeaderWriter.BeginEncodeHeaders(_headersEnumerator, buffer, out var payloadLength); + var done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out var payloadLength); FinishWritingHeaders(streamId, payloadLength, done); } catch (HPackEncodingException hex) @@ -230,7 +241,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { _outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); - done = HPackHeaderWriter.ContinueEncodeHeaders(_headersEnumerator, buffer, out payloadLength); + done = HPackHeaderWriter.ContinueEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out payloadLength); _outgoingFrame.PayloadLength = payloadLength; if (done) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs index 421650b9fd..db21bfb0fd 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; +using System.Net.Http.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.Extensions.Primitives; @@ -15,8 +16,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private HttpResponseTrailers.Enumerator _trailersEnumerator; private IEnumerator> _genericEnumerator; private StringValues.Enumerator _stringValuesEnumerator; + private KnownHeaderType _knownHeaderType; - public KnownHeaderType KnownHeaderType { get; private set; } + public int HPackStaticTableId => GetResponseHeaderStaticTableId(_knownHeaderType); public KeyValuePair Current { get; private set; } object IEnumerator.Current => Current; @@ -33,7 +35,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _stringValuesEnumerator = default; Current = default; - KnownHeaderType = default; + _knownHeaderType = default; } public void Initialize(HttpResponseTrailers headers) @@ -45,7 +47,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _stringValuesEnumerator = default; Current = default; - KnownHeaderType = default; + _knownHeaderType = default; } public void Initialize(IDictionary headers) @@ -57,7 +59,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _stringValuesEnumerator = default; Current = default; - KnownHeaderType = default; + _knownHeaderType = default; } public bool MoveNext() @@ -110,7 +112,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 else { enumerator = _genericEnumerator.Current.Value.GetEnumerator(); - KnownHeaderType = default; + _knownHeaderType = default; return true; } } @@ -124,7 +126,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 else { enumerator = _trailersEnumerator.Current.Value.GetEnumerator(); - KnownHeaderType = _trailersEnumerator.CurrentKnownType; + _knownHeaderType = _trailersEnumerator.CurrentKnownType; return true; } } @@ -138,7 +140,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 else { enumerator = _headersEnumerator.Current.Value.GetEnumerator(); - KnownHeaderType = _headersEnumerator.CurrentKnownType; + _knownHeaderType = _headersEnumerator.CurrentKnownType; return true; } } @@ -159,11 +161,68 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _headersEnumerator.Reset(); } _stringValuesEnumerator = default; - KnownHeaderType = default; + _knownHeaderType = default; } public void Dispose() { } + + internal static int GetResponseHeaderStaticTableId(KnownHeaderType responseHeaderType) + { + switch (responseHeaderType) + { + case KnownHeaderType.CacheControl: + return H2StaticTable.CacheControl; + case KnownHeaderType.Date: + return H2StaticTable.Date; + case KnownHeaderType.TransferEncoding: + return H2StaticTable.TransferEncoding; + case KnownHeaderType.Via: + return H2StaticTable.Via; + case KnownHeaderType.Allow: + return H2StaticTable.Allow; + case KnownHeaderType.ContentType: + return H2StaticTable.ContentType; + case KnownHeaderType.ContentEncoding: + return H2StaticTable.ContentEncoding; + case KnownHeaderType.ContentLanguage: + return H2StaticTable.ContentLanguage; + case KnownHeaderType.ContentLocation: + return H2StaticTable.ContentLocation; + case KnownHeaderType.ContentRange: + return H2StaticTable.ContentRange; + case KnownHeaderType.Expires: + return H2StaticTable.Expires; + case KnownHeaderType.LastModified: + return H2StaticTable.LastModified; + case KnownHeaderType.AcceptRanges: + return H2StaticTable.AcceptRanges; + case KnownHeaderType.Age: + return H2StaticTable.Age; + case KnownHeaderType.ETag: + return H2StaticTable.ETag; + case KnownHeaderType.Location: + return H2StaticTable.Location; + case KnownHeaderType.ProxyAuthenticate: + return H2StaticTable.ProxyAuthenticate; + case KnownHeaderType.RetryAfter: + return H2StaticTable.RetryAfter; + case KnownHeaderType.Server: + return H2StaticTable.Server; + case KnownHeaderType.SetCookie: + return H2StaticTable.SetCookie; + case KnownHeaderType.Vary: + return H2StaticTable.Vary; + case KnownHeaderType.WWWAuthenticate: + return H2StaticTable.WwwAuthenticate; + case KnownHeaderType.AccessControlAllowOrigin: + return H2StaticTable.AccessControlAllowOrigin; + case KnownHeaderType.ContentLength: + return H2StaticTable.ContentLength; + default: + return -1; + } + } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs index 0f95f70ef7..cc30a8ee66 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs @@ -41,10 +41,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private bool _suffixSent; private bool _streamEnded; private bool _writerComplete; - private bool _disposed; // Internal for testing internal ValueTask _dataWriteProcessingTask; + internal bool _disposed; /// The core logic for the IValueTaskSource implementation. private ManualResetValueTaskSourceCore _responseCompleteTaskSource = new ManualResetValueTaskSourceCore { RunContinuationsAsynchronously = true }; // mutable struct, do not make this readonly diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamStack.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamStack.cs index 1c141dd236..88512a19df 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamStack.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamStack.cs @@ -37,6 +37,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return true; } + public bool TryPeek(out Http2Stream result) + { + int size = _size - 1; + Http2StreamAsValueType[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + result = default; + return false; + } + + result = array[size]; + return true; + } + // Pushes an item to the top of the stack. public void Push(Http2Stream item) { diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index 71def30d73..c849b84caf 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -38,6 +38,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// public bool AddServerHeader { get; set; } = true; + /// + /// Gets or sets a value that controls whether dynamic compression of response headers is allowed. + /// For more information about the security considerations of HPack dynamic header compression, visit + /// https://tools.ietf.org/html/rfc7541#section-7. + /// + /// + /// Defaults to true. + /// + public bool AllowResponseHeaderCompression { get; set; } = true; + /// /// Gets or sets a value that controls whether synchronous IO is allowed for the and /// diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj index 72a38463da..73f77bb773 100644 --- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj +++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj @@ -1,4 +1,4 @@ - + Core components of ASP.NET Core Kestrel cross-platform web server. @@ -16,9 +16,10 @@ - - - + + + + diff --git a/src/Servers/Kestrel/Core/test/HPackHeaderWriterTests.cs b/src/Servers/Kestrel/Core/test/HPackHeaderWriterTests.cs index 3b290d712b..bab3515211 100644 --- a/src/Servers/Kestrel/Core/test/HPackHeaderWriterTests.cs +++ b/src/Servers/Kestrel/Core/test/HPackHeaderWriterTests.cs @@ -1,199 +1,199 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +//// Copyright (c) .NET Foundation. All rights reserved. +//// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; -using Microsoft.Extensions.Primitives; -using Xunit; +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; +//using Microsoft.Extensions.Primitives; +//using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests -{ - public class HPackHeaderWriterTests - { - public static TheoryData[], byte[], int?> SinglePayloadData - { - get - { - var data = new TheoryData[], byte[], int?>(); +//namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +//{ +// public class HPackHeaderWriterTests +// { +// public static TheoryData[], byte[], int?> SinglePayloadData +// { +// get +// { +// var data = new TheoryData[], byte[], int?>(); - // Lowercase header name letters only - data.Add( - new[] - { - new KeyValuePair("CustomHeader", "CustomValue"), - }, - new byte[] - { - // 0 12 c u s t o m - 0x00, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, - // h e a d e r 11 C - 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x0b, 0x43, - // u s t o m V a l - 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, - // u e - 0x75, 0x65 - }, - null); - // Lowercase header name letters only - data.Add( - new[] - { - new KeyValuePair("CustomHeader!#$%&'*+-.^_`|~", "CustomValue"), - }, - new byte[] - { - // 0 27 c u s t o m - 0x00, 0x1b, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, - // h e a d e r ! # - 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x21, 0x23, - // $ % & ' * + - . - 0x24, 0x25, 0x26, 0x27, 0x2a, 0x2b, 0x2d, 0x2e, - // ^ _ ` | ~ 11 C u - 0x5e, 0x5f, 0x60, 0x7c, 0x7e, 0x0b, 0x43, 0x75, - // s t o m V a l u - 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, - // e - 0x65 - }, - null); - // Single Payload - data.Add( - new[] - { - new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"), - new KeyValuePair("content-type", "text/html; charset=utf-8"), - new KeyValuePair("server", "Kestrel") - }, - new byte[] - { - 0x88, 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, - 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, - 0x4a, 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, - 0x20, 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, - 0x30, 0x20, 0x47, 0x4d, 0x54, 0x00, 0x0c, 0x63, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x74, 0x65, 0x78, 0x74, - 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3b, 0x20, 0x63, - 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x75, - 0x74, 0x66, 0x2d, 0x38, 0x00, 0x06, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x07, 0x4b, 0x65, 0x73, - 0x74, 0x72, 0x65, 0x6c - }, - 200); +// // Lowercase header name letters only +// data.Add( +// new[] +// { +// new KeyValuePair("CustomHeader", "CustomValue"), +// }, +// new byte[] +// { +// // 0 12 c u s t o m +// 0x00, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, +// // h e a d e r 11 C +// 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x0b, 0x43, +// // u s t o m V a l +// 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, +// // u e +// 0x75, 0x65 +// }, +// null); +// // Lowercase header name letters only +// data.Add( +// new[] +// { +// new KeyValuePair("CustomHeader!#$%&'*+-.^_`|~", "CustomValue"), +// }, +// new byte[] +// { +// // 0 27 c u s t o m +// 0x00, 0x1b, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, +// // h e a d e r ! # +// 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x21, 0x23, +// // $ % & ' * + - . +// 0x24, 0x25, 0x26, 0x27, 0x2a, 0x2b, 0x2d, 0x2e, +// // ^ _ ` | ~ 11 C u +// 0x5e, 0x5f, 0x60, 0x7c, 0x7e, 0x0b, 0x43, 0x75, +// // s t o m V a l u +// 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, +// // e +// 0x65 +// }, +// null); +// // Single Payload +// data.Add( +// new[] +// { +// new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"), +// new KeyValuePair("content-type", "text/html; charset=utf-8"), +// new KeyValuePair("server", "Kestrel") +// }, +// new byte[] +// { +// 0x88, 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, +// 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, +// 0x4a, 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, +// 0x20, 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, +// 0x30, 0x20, 0x47, 0x4d, 0x54, 0x00, 0x0c, 0x63, +// 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x74, +// 0x79, 0x70, 0x65, 0x18, 0x74, 0x65, 0x78, 0x74, +// 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3b, 0x20, 0x63, +// 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x75, +// 0x74, 0x66, 0x2d, 0x38, 0x00, 0x06, 0x73, 0x65, +// 0x72, 0x76, 0x65, 0x72, 0x07, 0x4b, 0x65, 0x73, +// 0x74, 0x72, 0x65, 0x6c +// }, +// 200); - return data; - } - } +// return data; +// } +// } - [Theory] - [MemberData(nameof(SinglePayloadData))] - public void EncodesHeadersInSinglePayloadWhenSpaceAvailable(KeyValuePair[] headers, byte[] expectedPayload, int? statusCode) - { - var payload = new byte[1024]; - var length = 0; - if (statusCode.HasValue) - { - Assert.True(HPackHeaderWriter.BeginEncodeHeaders(statusCode.Value, GetHeadersEnumerator(headers), payload, out length)); - } - else - { - Assert.True(HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out length)); - } - Assert.Equal(expectedPayload.Length, length); +// [Theory] +// [MemberData(nameof(SinglePayloadData))] +// public void EncodesHeadersInSinglePayloadWhenSpaceAvailable(KeyValuePair[] headers, byte[] expectedPayload, int? statusCode) +// { +// var payload = new byte[1024]; +// var length = 0; +// if (statusCode.HasValue) +// { +// Assert.True(HPackHeaderWriter.BeginEncodeHeaders(statusCode.Value, GetHeadersEnumerator(headers), payload, out length)); +// } +// else +// { +// Assert.True(HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out length)); +// } +// Assert.Equal(expectedPayload.Length, length); - for (var i = 0; i < length; i++) - { - Assert.True(expectedPayload[i] == payload[i], $"{expectedPayload[i]} != {payload[i]} at {i} (len {length})"); - } +// for (var i = 0; i < length; i++) +// { +// Assert.True(expectedPayload[i] == payload[i], $"{expectedPayload[i]} != {payload[i]} at {i} (len {length})"); +// } - Assert.Equal(expectedPayload, new ArraySegment(payload, 0, length)); - } +// Assert.Equal(expectedPayload, new ArraySegment(payload, 0, length)); +// } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void EncodesHeadersInMultiplePayloadsWhenSpaceNotAvailable(bool exactSize) - { - var statusCode = 200; - var headers = new[] - { - new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"), - new KeyValuePair("content-type", "text/html; charset=utf-8"), - new KeyValuePair("server", "Kestrel") - }; +// [Theory] +// [InlineData(true)] +// [InlineData(false)] +// public void EncodesHeadersInMultiplePayloadsWhenSpaceNotAvailable(bool exactSize) +// { +// var statusCode = 200; +// var headers = new[] +// { +// new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"), +// new KeyValuePair("content-type", "text/html; charset=utf-8"), +// new KeyValuePair("server", "Kestrel") +// }; - var expectedStatusCodePayload = new byte[] - { - 0x88 - }; +// var expectedStatusCodePayload = new byte[] +// { +// 0x88 +// }; - var expectedDateHeaderPayload = new byte[] - { - 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, 0x4d, - 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, 0x4a, - 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, 0x20, - 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, 0x30, - 0x20, 0x47, 0x4d, 0x54 - }; +// var expectedDateHeaderPayload = new byte[] +// { +// 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, 0x4d, +// 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, 0x4a, +// 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, 0x20, +// 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, 0x30, +// 0x20, 0x47, 0x4d, 0x54 +// }; - var expectedContentTypeHeaderPayload = new byte[] - { - 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, - 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x18, 0x74, - 0x65, 0x78, 0x74, 0x2f, 0x68, 0x74, 0x6d, 0x6c, - 0x3b, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, - 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38 - }; +// var expectedContentTypeHeaderPayload = new byte[] +// { +// 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, +// 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x18, 0x74, +// 0x65, 0x78, 0x74, 0x2f, 0x68, 0x74, 0x6d, 0x6c, +// 0x3b, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, +// 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38 +// }; - var expectedServerHeaderPayload = new byte[] - { - 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x07, 0x4b, 0x65, 0x73, 0x74, 0x72, 0x65, 0x6c - }; +// var expectedServerHeaderPayload = new byte[] +// { +// 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, +// 0x07, 0x4b, 0x65, 0x73, 0x74, 0x72, 0x65, 0x6c +// }; - Span payload = new byte[1024]; - var offset = 0; - var headerEnumerator = GetHeadersEnumerator(headers); +// Span payload = new byte[1024]; +// var offset = 0; +// var headerEnumerator = GetHeadersEnumerator(headers); - // When !exactSize, slices are one byte short of fitting the next header - var sliceLength = expectedStatusCodePayload.Length + (exactSize ? 0 : expectedDateHeaderPayload.Length - 1); - Assert.False(HPackHeaderWriter.BeginEncodeHeaders(statusCode, headerEnumerator, payload.Slice(offset, sliceLength), out var length)); - Assert.Equal(expectedStatusCodePayload.Length, length); - Assert.Equal(expectedStatusCodePayload, payload.Slice(0, length).ToArray()); +// // When !exactSize, slices are one byte short of fitting the next header +// var sliceLength = expectedStatusCodePayload.Length + (exactSize ? 0 : expectedDateHeaderPayload.Length - 1); +// Assert.False(HPackHeaderWriter.BeginEncodeHeaders(statusCode, headerEnumerator, payload.Slice(offset, sliceLength), out var length)); +// Assert.Equal(expectedStatusCodePayload.Length, length); +// Assert.Equal(expectedStatusCodePayload, payload.Slice(0, length).ToArray()); - offset += length; +// offset += length; - sliceLength = expectedDateHeaderPayload.Length + (exactSize ? 0 : expectedContentTypeHeaderPayload.Length - 1); - Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); - Assert.Equal(expectedDateHeaderPayload.Length, length); - Assert.Equal(expectedDateHeaderPayload, payload.Slice(offset, length).ToArray()); +// sliceLength = expectedDateHeaderPayload.Length + (exactSize ? 0 : expectedContentTypeHeaderPayload.Length - 1); +// Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); +// Assert.Equal(expectedDateHeaderPayload.Length, length); +// Assert.Equal(expectedDateHeaderPayload, payload.Slice(offset, length).ToArray()); - offset += length; +// offset += length; - sliceLength = expectedContentTypeHeaderPayload.Length + (exactSize ? 0 : expectedServerHeaderPayload.Length - 1); - Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); - Assert.Equal(expectedContentTypeHeaderPayload.Length, length); - Assert.Equal(expectedContentTypeHeaderPayload, payload.Slice(offset, length).ToArray()); +// sliceLength = expectedContentTypeHeaderPayload.Length + (exactSize ? 0 : expectedServerHeaderPayload.Length - 1); +// Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); +// Assert.Equal(expectedContentTypeHeaderPayload.Length, length); +// Assert.Equal(expectedContentTypeHeaderPayload, payload.Slice(offset, length).ToArray()); - offset += length; +// offset += length; - sliceLength = expectedServerHeaderPayload.Length; - Assert.True(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); - Assert.Equal(expectedServerHeaderPayload.Length, length); - Assert.Equal(expectedServerHeaderPayload, payload.Slice(offset, length).ToArray()); - } +// sliceLength = expectedServerHeaderPayload.Length; +// Assert.True(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); +// Assert.Equal(expectedServerHeaderPayload.Length, length); +// Assert.Equal(expectedServerHeaderPayload, payload.Slice(offset, length).ToArray()); +// } - private static Http2HeadersEnumerator GetHeadersEnumerator(IEnumerable> headers) - { - var groupedHeaders = headers - .GroupBy(k => k.Key) - .ToDictionary(g => g.Key, g => new StringValues(g.Select(gg => gg.Value).ToArray())); +// private static Http2HeadersEnumerator GetHeadersEnumerator(IEnumerable> headers) +// { +// var groupedHeaders = headers +// .GroupBy(k => k.Key) +// .ToDictionary(g => g.Key, g => new StringValues(g.Select(gg => gg.Value).ToArray())); - var enumerator = new Http2HeadersEnumerator(); - enumerator.Initialize(groupedHeaders); - return enumerator; - } - } -} +// var enumerator = new Http2HeadersEnumerator(); +// enumerator.Initialize(groupedHeaders); +// return enumerator; +// } +// } +//} diff --git a/src/Servers/Kestrel/Core/test/Http2FrameWriterTests.cs b/src/Servers/Kestrel/Core/test/Http2FrameWriterTests.cs index 6300fcf85b..30913d952e 100644 --- a/src/Servers/Kestrel/Core/test/Http2FrameWriterTests.cs +++ b/src/Servers/Kestrel/Core/test/Http2FrameWriterTests.cs @@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { // Arrange var pipe = new Pipe(new PipeOptions(_dirtyMemoryPool, PipeScheduler.Inline, PipeScheduler.Inline)); - var frameWriter = new Http2FrameWriter(pipe.Writer, null, null, null, null, null, null, _dirtyMemoryPool, new Mock().Object); + var frameWriter = CreateFrameWriter(pipe); // Act await frameWriter.WriteWindowUpdateAsync(1, 1); @@ -52,12 +52,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(new byte[] { 0x00, 0x00, 0x00, 0x01 }, payload.Skip(Http2FrameReader.HeaderLength).Take(4).ToArray()); } + private Http2FrameWriter CreateFrameWriter(Pipe pipe) + { + var serviceContext = new Internal.ServiceContext + { + ServerOptions = new KestrelServerOptions(), + Log = new Mock().Object + }; + return new Http2FrameWriter(pipe.Writer, null, null, null, null, null, null, _dirtyMemoryPool, serviceContext); + } + [Fact] public async Task WriteGoAway_UnsetsReservedBit() { // Arrange var pipe = new Pipe(new PipeOptions(_dirtyMemoryPool, PipeScheduler.Inline, PipeScheduler.Inline)); - var frameWriter = new Http2FrameWriter(pipe.Writer, null, null, null, null, null, null, _dirtyMemoryPool, new Mock().Object); + var frameWriter = CreateFrameWriter(pipe); // Act await frameWriter.WriteGoAwayAsync(1, Http2ErrorCode.NO_ERROR); diff --git a/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs b/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs new file mode 100644 index 0000000000..c9dcdb00ec --- /dev/null +++ b/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs @@ -0,0 +1,479 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.HPack; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +{ + public class Http2HPackEncoderTests + { + [Fact] + public void BeginEncodeHeaders_Status302_NewIndexValue() + { + Span buffer = new byte[1024 * 16]; + + var headers = new HttpResponseHeaders(); + var enumerator = new Http2HeadersEnumerator(); + enumerator.Initialize(headers); + + var hpackEncoder = new HPackEncoder(); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length)); + + var result = buffer.Slice(0, length).ToArray(); + var hex = BitConverter.ToString(result); + Assert.Equal("48-03-33-30-32", hex); + + var statusHeader = GetHeaderEntry(hpackEncoder, 0); + Assert.Equal(":status", statusHeader.Name); + Assert.Equal("302", statusHeader.Value); + } + + [Fact] + public void BeginEncodeHeaders_CacheControlPrivate_NewIndexValue() + { + Span buffer = new byte[1024 * 16]; + + var headers = new HttpResponseHeaders(); + headers.HeaderCacheControl = "private"; + + var enumerator = new Http2HeadersEnumerator(); + enumerator.Initialize(headers); + + var hpackEncoder = new HPackEncoder(); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length)); + + var result = buffer.Slice(5, length - 5).ToArray(); + var hex = BitConverter.ToString(result); + Assert.Equal("58-07-70-72-69-76-61-74-65", hex); + + var statusHeader = GetHeaderEntry(hpackEncoder, 0); + Assert.Equal("Cache-Control", statusHeader.Name); + Assert.Equal("private", statusHeader.Value); + } + + [Fact] + public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit() + { + // Test follows example https://tools.ietf.org/html/rfc7541#appendix-C.5 + + Span buffer = new byte[1024 * 16]; + + var headers = new HttpResponseHeaders(); + headers.HeaderCacheControl = "private"; + headers.HeaderDate = "Mon, 21 Oct 2013 20:13:21 GMT"; + headers.HeaderLocation = "https://www.example.com"; + + var enumerator = new Http2HeadersEnumerator(); + + var hpackEncoder = new HPackEncoder(maxHeaderTableSize: 256); + + // First response + enumerator.Initialize(headers); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length)); + + var result = buffer.Slice(0, length).ToArray(); + var hex = BitConverter.ToString(result); + Assert.Equal( + "48-03-33-30-32-58-07-70-72-69-76-61-74-65-61-1D-" + + "4D-6F-6E-2C-20-32-31-20-4F-63-74-20-32-30-31-33-" + + "20-32-30-3A-31-33-3A-32-31-20-47-4D-54-6E-17-68-" + + "74-74-70-73-3A-2F-2F-77-77-77-2E-65-78-61-6D-70-" + + "6C-65-2E-63-6F-6D", hex); + + var entries = GetHeaderEntries(hpackEncoder); + Assert.Collection(entries, + e => + { + Assert.Equal("Location", e.Name); + Assert.Equal("https://www.example.com", e.Value); + }, + e => + { + Assert.Equal("Date", e.Name); + Assert.Equal("Mon, 21 Oct 2013 20:13:21 GMT", e.Value); + }, + e => + { + Assert.Equal("Cache-Control", e.Name); + Assert.Equal("private", e.Value); + }, + e => + { + Assert.Equal(":status", e.Name); + Assert.Equal("302", e.Value); + }); + + // Second response + enumerator.Initialize(headers); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(307, hpackEncoder, enumerator, buffer, out length)); + + result = buffer.Slice(0, length).ToArray(); + hex = BitConverter.ToString(result); + Assert.Equal("48-03-33-30-37-C1-C0-BF", hex); + + entries = GetHeaderEntries(hpackEncoder); + Assert.Collection(entries, + e => + { + Assert.Equal(":status", e.Name); + Assert.Equal("307", e.Value); + }, + e => + { + Assert.Equal("Location", e.Name); + Assert.Equal("https://www.example.com", e.Value); + }, + e => + { + Assert.Equal("Date", e.Name); + Assert.Equal("Mon, 21 Oct 2013 20:13:21 GMT", e.Value); + }, + e => + { + Assert.Equal("Cache-Control", e.Name); + Assert.Equal("private", e.Value); + }); + + // Third response + headers.HeaderDate = "Mon, 21 Oct 2013 20:13:22 GMT"; + headers.HeaderContentEncoding = "gzip"; + headers.HeaderSetCookie = "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"; + + enumerator.Initialize(headers); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out length)); + + result = buffer.Slice(0, length).ToArray(); + hex = BitConverter.ToString(result); + Assert.Equal( + "88-C1-61-1D-4D-6F-6E-2C-20-32-31-20-4F-63-74-20-" + + "32-30-31-33-20-32-30-3A-31-33-3A-32-32-20-47-4D-" + + "54-5A-04-67-7A-69-70-C1-1F-28-38-66-6F-6F-3D-41-" + + "53-44-4A-4B-48-51-4B-42-5A-58-4F-51-57-45-4F-50-" + + "49-55-41-58-51-57-45-4F-49-55-3B-20-6D-61-78-2D-" + + "61-67-65-3D-33-36-30-30-3B-20-76-65-72-73-69-6F-" + + "6E-3D-31", hex); + + entries = GetHeaderEntries(hpackEncoder); + Assert.Collection(entries, + e => + { + Assert.Equal("Content-Encoding", e.Name); + Assert.Equal("gzip", e.Value); + }, + e => + { + Assert.Equal("Date", e.Name); + Assert.Equal("Mon, 21 Oct 2013 20:13:22 GMT", e.Value); + }, + e => + { + Assert.Equal(":status", e.Name); + Assert.Equal("307", e.Value); + }, + e => + { + Assert.Equal("Location", e.Name); + Assert.Equal("https://www.example.com", e.Value); + }); + } + + [Theory] + [InlineData("Set-Cookie", true)] + [InlineData("Content-Disposition", true)] + [InlineData("Content-Length", false)] + public void BeginEncodeHeaders_ExcludedHeaders_NotAddedToTable(string headerName, bool neverIndex) + { + Span buffer = new byte[1024 * 16]; + + var headers = new HttpResponseHeaders(); + headers.Append(headerName, "1"); + + var enumerator = new Http2HeadersEnumerator(); + enumerator.Initialize(headers); + + var hpackEncoder = new HPackEncoder(maxHeaderTableSize: Http2PeerSettings.DefaultHeaderTableSize); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out _)); + + if (neverIndex) + { + Assert.Equal(0x10, buffer[0] & 0x10); + } + else + { + Assert.Equal(0, buffer[0] & 0x40); + } + + Assert.Empty(GetHeaderEntries(hpackEncoder)); + } + + [Fact] + public void BeginEncodeHeaders_HeaderExceedHeaderTableSize_NoIndexAndNoHeaderEntry() + { + Span buffer = new byte[1024 * 16]; + + var headers = new HttpResponseHeaders(); + headers.Append("x-Custom", new string('!', (int)Http2PeerSettings.DefaultHeaderTableSize)); + + var enumerator = new Http2HeadersEnumerator(); + enumerator.Initialize(headers); + + var hpackEncoder = new HPackEncoder(); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out var length)); + + Assert.Empty(GetHeaderEntries(hpackEncoder)); + } + + public static TheoryData[], byte[], int?> SinglePayloadData + { + get + { + var data = new TheoryData[], byte[], int?>(); + + // Lowercase header name letters only + data.Add( + new[] + { + new KeyValuePair("CustomHeader", "CustomValue"), + }, + new byte[] + { + // 12 c u s t o m + 0x40, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, + // h e a d e r 11 C + 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x0b, 0x43, + // u s t o m V a l + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, + // u e + 0x75, 0x65 + }, + null); + // Lowercase header name letters only + data.Add( + new[] + { + new KeyValuePair("CustomHeader!#$%&'*+-.^_`|~", "CustomValue"), + }, + new byte[] + { + // 27 c u s t o m + 0x40, 0x1b, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, + // h e a d e r ! # + 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x21, 0x23, + // $ % & ' * + - . + 0x24, 0x25, 0x26, 0x27, 0x2a, 0x2b, 0x2d, 0x2e, + // ^ _ ` | ~ 11 C u + 0x5e, 0x5f, 0x60, 0x7c, 0x7e, 0x0b, 0x43, 0x75, + // s t o m V a l u + 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, + // e + 0x65 + }, + null); + // Single Payload + data.Add( + new[] + { + new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"), + new KeyValuePair("content-type", "text/html; charset=utf-8"), + new KeyValuePair("server", "Kestrel") + }, + new byte[] + { + 0x88, 0x40, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, + 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, + 0x4a, 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, + 0x20, 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, + 0x30, 0x20, 0x47, 0x4d, 0x54, 0x40, 0x0c, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x74, 0x65, 0x78, 0x74, + 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3b, 0x20, 0x63, + 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x75, + 0x74, 0x66, 0x2d, 0x38, 0x40, 0x06, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x07, 0x4b, 0x65, 0x73, + 0x74, 0x72, 0x65, 0x6c + }, + 200); + + return data; + } + } + + [Theory] + [MemberData(nameof(SinglePayloadData))] + public void EncodesHeadersInSinglePayloadWhenSpaceAvailable(KeyValuePair[] headers, byte[] expectedPayload, int? statusCode) + { + HPackEncoder hpackEncoder = new HPackEncoder(); + + var payload = new byte[1024]; + var length = 0; + if (statusCode.HasValue) + { + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(statusCode.Value, hpackEncoder, GetHeadersEnumerator(headers), payload, out length)); + } + else + { + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, GetHeadersEnumerator(headers), payload, out length)); + } + Assert.Equal(expectedPayload.Length, length); + + for (var i = 0; i < length; i++) + { + Assert.True(expectedPayload[i] == payload[i], $"{expectedPayload[i]} != {payload[i]} at {i} (len {length})"); + } + + Assert.Equal(expectedPayload, new ArraySegment(payload, 0, length)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodesHeadersInMultiplePayloadsWhenSpaceNotAvailable(bool exactSize) + { + var statusCode = 200; + var headers = new[] + { + new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"), + new KeyValuePair("content-type", "text/html; charset=utf-8"), + new KeyValuePair("server", "Kestrel") + }; + + var expectedStatusCodePayload = new byte[] + { + 0x88 + }; + + var expectedDateHeaderPayload = new byte[] + { + 0x40, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, 0x4d, + 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, 0x4a, + 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, 0x20, + 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, 0x30, + 0x20, 0x47, 0x4d, 0x54 + }; + + var expectedContentTypeHeaderPayload = new byte[] + { + 0x40, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x18, 0x74, + 0x65, 0x78, 0x74, 0x2f, 0x68, 0x74, 0x6d, 0x6c, + 0x3b, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, + 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38 + }; + + var expectedServerHeaderPayload = new byte[] + { + 0x40, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x07, 0x4b, 0x65, 0x73, 0x74, 0x72, 0x65, 0x6c + }; + + var hpackEncoder = new HPackEncoder(); + + Span payload = new byte[1024]; + var offset = 0; + var headerEnumerator = GetHeadersEnumerator(headers); + + // When !exactSize, slices are one byte short of fitting the next header + var sliceLength = expectedStatusCodePayload.Length + (exactSize ? 0 : expectedDateHeaderPayload.Length - 1); + Assert.False(HPackHeaderWriter.BeginEncodeHeaders(statusCode, hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out var length)); + Assert.Equal(expectedStatusCodePayload.Length, length); + Assert.Equal(expectedStatusCodePayload, payload.Slice(0, length).ToArray()); + + offset += length; + + sliceLength = expectedDateHeaderPayload.Length + (exactSize ? 0 : expectedContentTypeHeaderPayload.Length - 1); + Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length)); + Assert.Equal(expectedDateHeaderPayload.Length, length); + Assert.Equal(expectedDateHeaderPayload, payload.Slice(offset, length).ToArray()); + + offset += length; + + sliceLength = expectedContentTypeHeaderPayload.Length + (exactSize ? 0 : expectedServerHeaderPayload.Length - 1); + Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length)); + Assert.Equal(expectedContentTypeHeaderPayload.Length, length); + Assert.Equal(expectedContentTypeHeaderPayload, payload.Slice(offset, length).ToArray()); + + offset += length; + + sliceLength = expectedServerHeaderPayload.Length; + Assert.True(HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length)); + Assert.Equal(expectedServerHeaderPayload.Length, length); + Assert.Equal(expectedServerHeaderPayload, payload.Slice(offset, length).ToArray()); + } + + [Fact] + public void BeginEncodeHeaders_MaxHeaderTableSizeUpdated_SizeUpdateInHeaders() + { + Span buffer = new byte[1024 * 16]; + + var hpackEncoder = new HPackEncoder(); + hpackEncoder.UpdateMaxHeaderTableSize(100); + + var enumerator = new Http2HeadersEnumerator(); + + // First request + enumerator.Initialize(new Dictionary()); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out var length)); + + Assert.Equal(2, length); + + const byte DynamicTableSizeUpdateMask = 0xe0; + + var integerDecoder = new IntegerDecoder(); + Assert.False(integerDecoder.BeginTryDecode((byte)(buffer[0] & ~DynamicTableSizeUpdateMask), prefixLength: 5, out _)); + Assert.True(integerDecoder.TryDecode(buffer[1], out var result)); + + Assert.Equal(100, result); + + // Second request + enumerator.Initialize(new Dictionary()); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out length)); + + Assert.Equal(0, length); + } + + private static Http2HeadersEnumerator GetHeadersEnumerator(IEnumerable> headers) + { + var groupedHeaders = headers + .GroupBy(k => k.Key) + .ToDictionary(g => g.Key, g => new StringValues(g.Select(gg => gg.Value).ToArray())); + + var enumerator = new Http2HeadersEnumerator(); + enumerator.Initialize(groupedHeaders); + return enumerator; + } + + private EncoderHeaderEntry GetHeaderEntry(HPackEncoder encoder, int index) + { + var entry = encoder.Head; + while (index-- >= 0) + { + entry = entry.Before; + } + return entry; + } + + private List GetHeaderEntries(HPackEncoder encoder) + { + var headers = new List(); + + var entry = encoder.Head; + while (entry.Before != encoder.Head) + { + entry = entry.Before; + headers.Add(entry); + }; + + return headers; + } + } +} diff --git a/src/Servers/Kestrel/Core/test/KestrelServerLimitsTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerLimitsTests.cs index 5c7623337c..fd8146804e 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerLimitsTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerLimitsTests.cs @@ -333,11 +333,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Theory] [InlineData(int.MinValue)] [InlineData(-1)] - [InlineData(0)] public void Http2HeaderTableSizeInvalid(int value) { var ex = Assert.Throws(() => new KestrelServerLimits().Http2.HeaderTableSize = value); - Assert.StartsWith(CoreStrings.GreaterThanZeroRequired, ex.Message); + Assert.StartsWith(CoreStrings.GreaterThanOrEqualToZeroRequired, ex.Message); } [Fact] diff --git a/src/Servers/Kestrel/Directory.Build.props b/src/Servers/Kestrel/Directory.Build.props index 05b71894da..1b0e999039 100644 --- a/src/Servers/Kestrel/Directory.Build.props +++ b/src/Servers/Kestrel/Directory.Build.props @@ -20,7 +20,5 @@ - - diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs similarity index 92% rename from src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmark.cs rename to src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs index b3e8f15403..3886a38b09 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.IO; using System.IO.Pipelines; using System.Linq; +using System.Net.Http.HPack; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http; @@ -24,28 +25,25 @@ using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Server.Kestrel.Performance { - public class Http2ConnectionBenchmark + public abstract class Http2ConnectionBenchmarkBase { private MemoryPool _memoryPool; private HttpRequestHeaders _httpRequestHeaders; private Http2Connection _connection; + private HPackEncoder _hpackEncoder; private Http2HeadersEnumerator _requestHeadersEnumerator; private int _currentStreamId; private byte[] _headersBuffer; private DuplexPipe.DuplexPipePair _connectionPair; private Http2Frame _httpFrame; - private string _responseData; private int _dataWritten; - [Params(0, 10, 1024 * 1024)] - public int ResponseDataLength { get; set; } + protected abstract Task ProcessRequest(HttpContext httpContext); - [GlobalSetup] - public void GlobalSetup() + public virtual void GlobalSetup() { _memoryPool = SlabMemoryPoolFactory.Create(); _httpFrame = new Http2Frame(); - _responseData = new string('!', ResponseDataLength); var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); @@ -58,6 +56,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance _httpRequestHeaders.Append(HeaderNames.Authority, new StringValues("localhost:80")); _headersBuffer = new byte[1024 * 16]; + _hpackEncoder = new HPackEncoder(); var serviceContext = new ServiceContext { @@ -83,7 +82,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance _currentStreamId = 1; - _ = _connection.ProcessRequestsAsync(new DummyApplication(c => ResponseDataLength == 0 ? Task.CompletedTask : c.Response.WriteAsync(_responseData), new MockHttpContextFactory())); + _ = _connection.ProcessRequestsAsync(new DummyApplication(ProcessRequest, new MockHttpContextFactory())); _connectionPair.Application.Output.Write(Http2Connection.ClientPreface); _connectionPair.Application.Output.WriteSettings(new Http2PeerSettings @@ -102,11 +101,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance } [Benchmark] - public async Task EmptyRequest() + public async Task MakeRequest() { _requestHeadersEnumerator.Initialize(_httpRequestHeaders); _requestHeadersEnumerator.MoveNext(); - _connectionPair.Application.Output.WriteStartStream(streamId: _currentStreamId, _requestHeadersEnumerator, _headersBuffer, endStream: true, frame: _httpFrame); + _connectionPair.Application.Output.WriteStartStream(streamId: _currentStreamId, _hpackEncoder, _requestHeadersEnumerator, _headersBuffer, endStream: true, frame: _httpFrame); await _connectionPair.Application.Output.FlushAsync(); while (true) diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionEmptyBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionEmptyBenchmark.cs new file mode 100644 index 0000000000..6859b5b675 --- /dev/null +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionEmptyBenchmark.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + public class Http2ConnectionBenchmark : Http2ConnectionBenchmarkBase + { + [Params(0, 128, 1024)] + public int ResponseDataLength { get; set; } + + private string _responseData; + + [GlobalSetup] + public override void GlobalSetup() + { + base.GlobalSetup(); + _responseData = new string('!', ResponseDataLength); + } + + protected override Task ProcessRequest(HttpContext httpContext) + { + return ResponseDataLength == 0 ? Task.CompletedTask : httpContext.Response.WriteAsync(_responseData); + } + } +} diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionHeadersBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionHeadersBenchmark.cs new file mode 100644 index 0000000000..5ac19dd0b9 --- /dev/null +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionHeadersBenchmark.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + public class Http2ConnectionHeadersBenchmark : Http2ConnectionBenchmarkBase + { + [Params(1, 4, 32)] + public int HeadersCount { get; set; } + + [Params(true, false)] + public bool HeadersChange { get; set; } + + private int _headerIndex; + private string[] _headerNames; + + [GlobalSetup] + public override void GlobalSetup() + { + base.GlobalSetup(); + + _headerNames = new string[HeadersCount * (HeadersChange ? 1000 : 1)]; + for (var i = 0; i < _headerNames.Length; i++) + { + _headerNames[i] = "CustomHeader" + i; + } + } + + protected override Task ProcessRequest(HttpContext httpContext) + { + for (var i = 0; i < HeadersCount; i++) + { + var headerName = _headerNames[_headerIndex % HeadersCount]; + httpContext.Response.Headers[headerName] = "The quick brown fox jumps over the lazy dog."; + if (HeadersChange) + { + _headerIndex++; + } + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2FrameWriterBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2FrameWriterBenchmark.cs index 839558d1a3..ff58ed573d 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2FrameWriterBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2FrameWriterBenchmark.cs @@ -36,7 +36,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance minResponseDataRate: null, "TestConnectionId", _memoryPool, - new KestrelTrace(NullLogger.Instance)); + new Core.Internal.ServiceContext()); _responseHeaders = new HttpResponseHeaders(); _responseHeaders.HeaderContentType = "application/json"; diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs index 3960ebe388..8a38f97709 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs @@ -26,6 +26,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance } } + [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)] + public void JsonTechEmpower() + { + for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) + { + InsertData(RequestParsingData.JsonTechEmpowerRequest); + ParseData(); + } + } + [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)] public void LiveAspNet() { diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingData.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingData.cs index 5c496960bb..7b50a6fba0 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingData.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingData.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Linq; @@ -19,6 +19,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance "Connection: keep-alive\r\n" + "\r\n"; + private const string _jsonTechEmpowerRequest = + "GET /json HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Accept: Accept:application/json,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7\r\n" + + "Connection: keep-alive\r\n" + + "\r\n"; + // edge-casey - client's don't normally send this private const string _plaintextAbsoluteUriRequest = "GET http://localhost/plaintext HTTP/1.1\r\n" + @@ -59,6 +66,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance public static readonly byte[] PlaintextTechEmpowerPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_plaintextTechEmpowerRequest, Pipelining))); public static readonly byte[] PlaintextTechEmpowerRequest = Encoding.ASCII.GetBytes(_plaintextTechEmpowerRequest); + public static readonly byte[] JsonTechEmpowerRequest = Encoding.ASCII.GetBytes(_jsonTechEmpowerRequest); + public static readonly byte[] PlaintextAbsoluteUriRequest = Encoding.ASCII.GetBytes(_plaintextAbsoluteUriRequest); public static readonly byte[] LiveaspnetPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_liveaspnetRequest, Pipelining))); diff --git a/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs b/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs index 481278ae42..a99db7dfe4 100644 --- a/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs +++ b/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs @@ -25,13 +25,13 @@ namespace Microsoft.AspNetCore.Testing writer.Write(payload); } - public static void WriteStartStream(this PipeWriter writer, int streamId, Http2HeadersEnumerator headers, byte[] headerEncodingBuffer, bool endStream, Http2Frame frame = null) + public static void WriteStartStream(this PipeWriter writer, int streamId, HPackEncoder hpackEncoder, Http2HeadersEnumerator headers, byte[] headerEncodingBuffer, bool endStream, Http2Frame frame = null) { frame ??= new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); var buffer = headerEncodingBuffer.AsSpan(); - var done = HPackHeaderWriter.BeginEncodeHeaders(headers, buffer, out var length); + var done = HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, headers, buffer, out var length); frame.PayloadLength = length; if (done) @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Testing { frame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); - done = HPackHeaderWriter.ContinueEncodeHeaders(headers, buffer, out length); + done = HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headers, buffer, out length); frame.PayloadLength = length; if (done) diff --git a/src/Servers/Kestrel/stress/.vsconfig b/src/Servers/Kestrel/stress/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Servers/Kestrel/stress/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs index 9ce2aa0563..afb1e83ca3 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs @@ -100,6 +100,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2 } [ConditionalFact] + [QuarantinedTest] public async Task GracefulTurnsAbortiveIfRequestsDoNotFinish() { var requestStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 6fd7ce1d8b..d5a40a1127 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -14,10 +14,12 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Moq; using Xunit; @@ -30,10 +32,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [QuarantinedTest] public async Task FlowControl_ParallelStreams_FirstInFirstOutOrder() { - // Increase response buffer size so there is no delay in writing to it. - // We only want to hit flow control back-pressure and not pipe back-pressure. - // This fixes flakyness https://github.com/dotnet/aspnetcore/pull/19949 - _serviceContext.ServerOptions.Limits.MaxResponseBufferSize = 128 * 1024; + // The test will: + // 1. Create a stream with a large response. It will use up the connection window and complete. + // 2. Create two streams will smaller responses. + // 3. Update the connection window one byte at a time. + // 4. Read from them in a FIFO order until they are each complete. var writeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -42,8 +45,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // Send headers await c.Response.Body.FlushAsync(); - // Send large data (3 larger than window size) - var writeTask = c.Response.Body.WriteAsync(new byte[65538]); + var responseBodySize = Convert.ToInt32(c.Request.Headers["ResponseBodySize"]); + var writeTask = c.Response.Body.WriteAsync(new byte[responseBodySize]); // Notify test that write has started writeTcs.SetResult(null); @@ -52,12 +55,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await writeTask; }); - await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + await StartStreamAsync(1, GetHeaders(responseBodySize: 65535), endStream: true); // Ensure the stream window size is large enough - await SendWindowUpdateAsync(streamId: 1, 65538); + await SendWindowUpdateAsync(streamId: 1, 65535); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -79,68 +82,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withLength: 16383, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 1); - - // 3 byte is remaining on stream 1 - - writeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - await StartStreamAsync(3, _browserRequestHeaders, endStream: true); - // Ensure the stream window size is large enough - await SendWindowUpdateAsync(streamId: 3, 65538); - - await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, - withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, - withStreamId: 3); - - await writeTcs.Task; - - await SendWindowUpdateAsync(streamId: 0, 1); - - // FIFO means stream 1 returns data first - await ExpectAsync(Http2FrameType.DATA, - withLength: 1, - withFlags: (byte)Http2DataFrameFlags.NONE, - withStreamId: 1); - - await SendWindowUpdateAsync(streamId: 0, 1); - - // Stream 3 data - await ExpectAsync(Http2FrameType.DATA, - withLength: 1, - withFlags: (byte)Http2DataFrameFlags.NONE, - withStreamId: 3); - - await SendWindowUpdateAsync(streamId: 0, 1); - - // Stream 1 data - await ExpectAsync(Http2FrameType.DATA, - withLength: 1, - withFlags: (byte)Http2DataFrameFlags.NONE, - withStreamId: 1); - - await SendWindowUpdateAsync(streamId: 0, 1); - - // Stream 3 data - await ExpectAsync(Http2FrameType.DATA, - withLength: 1, - withFlags: (byte)Http2DataFrameFlags.NONE, - withStreamId: 3); - - await SendWindowUpdateAsync(streamId: 0, 1); - - // Stream 1 data - await ExpectAsync(Http2FrameType.DATA, - withLength: 1, - withFlags: (byte)Http2DataFrameFlags.NONE, - withStreamId: 1); - - // Stream 1 ends await ExpectAsync(Http2FrameType.DATA, withLength: 0, withFlags: (byte)Http2DataFrameFlags.END_STREAM, withStreamId: 1); + writeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await StartStreamAsync(3, GetHeaders(responseBodySize: 3), endStream: true); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 2, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 3); + + await writeTcs.Task; + + writeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await StartStreamAsync(5, GetHeaders(responseBodySize: 3), endStream: true); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 2, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 5); + + await writeTcs.Task; + + await SendWindowUpdateAsync(streamId: 0, 1); + + // FIFO means stream 3 returns data first + await ExpectAsync(Http2FrameType.DATA, + withLength: 1, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 3); + + await SendWindowUpdateAsync(streamId: 0, 1); + + // Stream 5 data + await ExpectAsync(Http2FrameType.DATA, + withLength: 1, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 5); + await SendWindowUpdateAsync(streamId: 0, 1); // Stream 3 data @@ -149,7 +133,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 3); - await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + await SendWindowUpdateAsync(streamId: 0, 1); + + // Stream 5 data + await ExpectAsync(Http2FrameType.DATA, + withLength: 1, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 5); + + await SendWindowUpdateAsync(streamId: 0, 1); + + // Stream 3 data + await ExpectAsync(Http2FrameType.DATA, + withLength: 1, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 3); + + // Stream 3 ends + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 3); + + await SendWindowUpdateAsync(streamId: 0, 1); + + // Stream 5 data + await ExpectAsync(Http2FrameType.DATA, + withLength: 1, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 5); + + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 5); + + await StopConnectionAsync(expectedLastStreamId: 5, ignoreNonGoAwayFrames: false); + + IEnumerable> GetHeaders(int responseBodySize) + { + foreach (var header in _browserRequestHeaders) + { + yield return header; + } + + yield return new KeyValuePair("ResponseBodySize", responseBodySize.ToString()); + } } [Fact] @@ -170,7 +199,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -247,7 +276,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, requestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -266,7 +295,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(3, requestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -296,7 +325,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests serverTcs.SetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -329,7 +358,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -344,7 +373,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(3, _helloBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -385,22 +414,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests appDelegateTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + // Get the in progress stream + var stream = _connection._streams[1]; + appDelegateTcs.TrySetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); - // Ping will trigger the stream to be returned to the pool so we can assert it - await SendPingAsync(Http2PingFrameFlags.NONE); - await ExpectAsync(Http2FrameType.PING, - withLength: 8, - withFlags: (byte)Http2PingFrameFlags.ACK, - withStreamId: 0); - // Stream has been returned to the pool - Assert.Equal(1, _connection.StreamPool.Count); + await PingUntilStreamPooled(expectedCount: 1).DefaultTimeout(); + Assert.True(_connection.StreamPool.TryPeek(out var pooledStream)); + Assert.Equal(stream, pooledStream); appDelegateTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await StartStreamAsync(3, _browserRequestHeaders, endStream: true); @@ -411,21 +438,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests appDelegateTcs.TrySetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); - // Ping will trigger the stream to be returned to the pool so we can assert it - await SendPingAsync(Http2PingFrameFlags.NONE); - await ExpectAsync(Http2FrameType.PING, - withLength: 8, - withFlags: (byte)Http2PingFrameFlags.ACK, - withStreamId: 0); - // Stream was reused and returned to the pool - Assert.Equal(1, _connection.StreamPool.Count); + await PingUntilStreamPooled(expectedCount: 1).DefaultTimeout(); + Assert.True(_connection.StreamPool.TryPeek(out pooledStream)); + Assert.Equal(stream, pooledStream); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + + async Task PingUntilStreamPooled(int expectedCount) + { + do + { + // Ping will trigger the stream to be returned to the pool so we can assert it + await SendPingAsync(Http2PingFrameFlags.NONE); + await ExpectAsync(Http2FrameType.PING, + withLength: 8, + withFlags: (byte)Http2PingFrameFlags.ACK, + withStreamId: 0); + } while (_connection.StreamPool.Count != expectedCount); + } } [Fact] @@ -436,7 +471,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await InitializeConnectionAsync(async context => { - await serverTcs.Task; + await serverTcs.Task.DefaultTimeout(); await context.Response.WriteAsync("Content"); throw new InvalidOperationException("Put the stream into an invalid state by throwing after writing to response."); @@ -448,7 +483,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests serverTcs.SetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -457,20 +492,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withStreamId: 1); await WaitForStreamErrorAsync(1, Http2ErrorCode.INTERNAL_ERROR, null); - // Ping will trigger the stream to be returned to the pool so we can assert it - await SendPingAsync(Http2PingFrameFlags.NONE); - await ExpectAsync(Http2FrameType.PING, - withLength: 8, - withFlags: (byte)Http2PingFrameFlags.ACK, - withStreamId: 0); + await PingUntilStreamDisposed(stream).DefaultTimeout(); // Stream is not returned to the pool Assert.Equal(0, _connection.StreamPool.Count); var output = (Http2OutputProducer)stream.Output; - await output._dataWriteProcessingTask; + await output._dataWriteProcessingTask.DefaultTimeout(); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + async Task PingUntilStreamDisposed(Http2Stream stream) + { + var output = (Http2OutputProducer)stream.Output; + + do + { + // Ping will trigger the stream to be returned to the pool so we can assert it + await SendPingAsync(Http2PingFrameFlags.NONE); + await ExpectAsync(Http2FrameType.PING, + withLength: 8, + withFlags: (byte)Http2PingFrameFlags.ACK, + withStreamId: 0); + } while (!output._disposed); + } } [Fact] @@ -566,7 +611,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, new byte[length], endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); // The client's settings is still defaulted to Http2PeerSettings.MinAllowedMaxFrameSize so the echo response will come back in two separate frames @@ -595,7 +640,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloWorldBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -621,7 +666,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -664,7 +709,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -785,7 +830,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _noData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -813,7 +858,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloBytes, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var stream1DataFrame1 = await ExpectAsync(Http2FrameType.DATA, @@ -824,7 +869,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(3, _helloBytes, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); var stream3DataFrame1 = await ExpectAsync(Http2FrameType.DATA, @@ -893,7 +938,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -945,7 +990,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withStreamId: 0); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); @@ -1023,7 +1068,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests stream3ReadFinished.TrySetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -1038,7 +1083,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests stream1ReadFinished.TrySetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1065,7 +1110,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataWithPaddingAsync(1, _helloWorldBytes, padLength, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -1110,7 +1155,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1169,7 +1214,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1238,7 +1283,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1413,7 +1458,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _postRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1455,7 +1500,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(3, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -1505,7 +1550,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(3, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -1609,7 +1654,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1630,7 +1675,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(3, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -1664,7 +1709,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1679,7 +1724,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1699,7 +1744,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendHeadersWithPaddingAsync(1, _browserRequestHeaders, padLength, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1716,7 +1761,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendHeadersWithPriorityAsync(1, _browserRequestHeaders, priority: 42, streamDependency: 0, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1736,7 +1781,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendHeadersWithPaddingAndPriorityAsync(1, _browserRequestHeaders, padLength, priority: 42, streamDependency: 0, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1762,7 +1807,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // The second stream should end first, since the first one is waiting for the request body. await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -1774,7 +1819,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, _requestTrailers); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1808,7 +1853,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1858,17 +1903,163 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests finishSecondRequest.TrySetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); finishFirstRequest.TrySetResult(null); + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 6, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + } + + [Fact] + public async Task HEADERS_HeaderTableSizeLimitZero_Received_DynamicTableUpdate() + { + _serviceContext.ServerOptions.Limits.Http2.HeaderTableSize = 0; + + await InitializeConnectionAsync(_noopApplication, expectedSettingsCount: 4); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + _hpackEncoder.UpdateMaxHeaderTableSize(0); + + var headerFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 38, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + const byte DynamicTableSizeUpdateMask = 0xe0; + + var integerDecoder = new IntegerDecoder(); + Assert.True(integerDecoder.BeginTryDecode((byte)(headerFrame.Payload.Span[0] & ~DynamicTableSizeUpdateMask), prefixLength: 5, out var result)); + + // Dynamic table update from the server + Assert.Equal(0, result); + + await StartStreamAsync(3, _browserRequestHeaders, endStream: true); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 37, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 3); + + await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + } + + [Fact] + public async Task HEADERS_ResponseSetsIgnoreIndexAndNeverIndexValues_HeadersParsed() + { + await InitializeConnectionAsync(c => + { + c.Response.ContentLength = 0; + c.Response.Headers[HeaderNames.SetCookie] = "SetCookie!"; + c.Response.Headers[HeaderNames.ContentDisposition] = "ContentDisposition!"; + + return Task.CompletedTask; + }); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + var frame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 90, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + var handler = new TestHttpHeadersHandler(); + + var hpackDecoder = new HPackDecoder(); + hpackDecoder.Decode(new ReadOnlySequence(frame.Payload), endHeaders: true, handler); + hpackDecoder.CompleteDecode(); + + Assert.Equal("200", handler.Headers[":status"]); + Assert.Equal("SetCookie!", handler.Headers[HeaderNames.SetCookie]); + Assert.Equal("ContentDisposition!", handler.Headers[HeaderNames.ContentDisposition]); + Assert.Equal("0", handler.Headers[HeaderNames.ContentLength]); + + await StartStreamAsync(3, _browserRequestHeaders, endStream: true); + + frame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 60, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 3); + + handler = new TestHttpHeadersHandler(); + + hpackDecoder.Decode(new ReadOnlySequence(frame.Payload), endHeaders: true, handler); + hpackDecoder.CompleteDecode(); + + Assert.Equal("200", handler.Headers[":status"]); + Assert.Equal("SetCookie!", handler.Headers[HeaderNames.SetCookie]); + Assert.Equal("ContentDisposition!", handler.Headers[HeaderNames.ContentDisposition]); + Assert.Equal("0", handler.Headers[HeaderNames.ContentLength]); + + await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + } + + private class TestHttpHeadersHandler : IHttpHeadersHandler + { + public readonly Dictionary Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) + { + var nameString = Encoding.ASCII.GetString(name); + var valueString = Encoding.ASCII.GetString(value); + + if (Headers.TryGetValue(nameString, out var values)) + { + var l = values.ToList(); + l.Add(valueString); + + Headers[nameString] = new StringValues(l.ToArray()); + } + else + { + Headers[nameString] = new StringValues(valueString); + } + } + + public void OnHeadersComplete(bool endStream) + { + throw new NotImplementedException(); + } + + public void OnStaticIndexedHeader(int index) + { + throw new NotImplementedException(); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + throw new NotImplementedException(); + } + } + + [Fact] + public async Task HEADERS_DisableDynamicHeaderCompression_HeadersNotCompressed() + { + _serviceContext.ServerOptions.AllowResponseHeaderCompression = false; + + await InitializeConnectionAsync(_noopApplication); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); + await StartStreamAsync(3, _browserRequestHeaders, endStream: true); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 37, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 3); + await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); } @@ -1891,7 +2082,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests requestBlocker.SetResult(0); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1934,7 +2125,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1977,7 +2168,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(3, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -2201,7 +2392,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -2346,7 +2537,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -2484,7 +2675,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2509,7 +2700,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // The headers, but not the data for stream 3, can be sent prior to any window updates. await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); @@ -2588,12 +2779,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } }); - async Task VerifyStreamBackpressure(int streamId) + async Task VerifyStreamBackpressure(int streamId, int headersLength) { await StartStreamAsync(streamId, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: headersLength, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: streamId); @@ -2606,9 +2797,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.False(writeTasks[streamId].IsCompleted); } - await VerifyStreamBackpressure(1); - await VerifyStreamBackpressure(3); - await VerifyStreamBackpressure(5); + await VerifyStreamBackpressure(1, 32); + await VerifyStreamBackpressure(3, 2); + await VerifyStreamBackpressure(5, 2); await SendRstStreamAsync(1); await writeTasks[1].DefaultTimeout(); @@ -2886,6 +3077,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { CreateConnection(); + _connection.ServerSettings.HeaderTableSize = 0; _connection.ServerSettings.MaxConcurrentStreams = 1; _connection.ServerSettings.MaxHeaderListSize = 4 * 1024; _connection.ServerSettings.InitialWindowSize = 1024 * 1024 * 10; @@ -2896,23 +3088,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendSettingsAsync(); var frame = await ExpectAsync(Http2FrameType.SETTINGS, - withLength: Http2FrameReader.SettingSize * 3, + withLength: Http2FrameReader.SettingSize * 4, withFlags: 0, withStreamId: 0); // Only non protocol defaults are sent var settings = Http2FrameReader.ReadSettings(frame.PayloadSequence); - Assert.Equal(3, settings.Count); + Assert.Equal(4, settings.Count); var setting = settings[0]; + Assert.Equal(Http2SettingsParameter.SETTINGS_HEADER_TABLE_SIZE, setting.Parameter); + Assert.Equal(0u, setting.Value); + + setting = settings[1]; Assert.Equal(Http2SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS, setting.Parameter); Assert.Equal(1u, setting.Value); - setting = settings[1]; + setting = settings[2]; Assert.Equal(Http2SettingsParameter.SETTINGS_INITIAL_WINDOW_SIZE, setting.Parameter); Assert.Equal(1024 * 1024 * 10u, setting.Value); - setting = settings[2]; + setting = settings[3]; Assert.Equal(Http2SettingsParameter.SETTINGS_MAX_HEADER_LIST_SIZE, setting.Parameter); Assert.Equal(4 * 1024u, setting.Value); @@ -3073,7 +3269,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _connection.ServerSettings.MaxFrameSize = Http2PeerSettings.MaxAllowedMaxFrameSize; // This includes the default response headers such as :status, etc - var defaultResponseHeaderLength = 33; + var defaultResponseHeaderLength = 32; var headerValueLength = Http2PeerSettings.MinAllowedMaxFrameSize; // First byte is always 0 // Second byte is the length of header name which is 1 @@ -3143,7 +3339,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3177,7 +3373,56 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withFlags: (byte)Http2SettingsFrameFlags.ACK, withStreamId: 0); - await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + // Start request + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + var headerFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 36, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + // Headers start with :status = 200 + Assert.Equal(0x88, headerFrame.Payload.Span[0]); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + } + + [Fact] + public async Task SETTINGS_Received_WithLargeHeaderTableSizeLimit_ChangesHeaderTableSize() + { + _serviceContext.ServerOptions.Limits.Http2.HeaderTableSize = 40000; + + await InitializeConnectionAsync(_noopApplication, expectedSettingsCount: 4); + + // Update client settings + _clientSettings.HeaderTableSize = 65536; // Chrome's default, larger than the 4kb spec default + await SendSettingsAsync(); + + // ACK + await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 0, + withFlags: (byte)Http2SettingsFrameFlags.ACK, + withStreamId: 0); + + // Start request + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + var headerFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 40, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + const byte DynamicTableSizeUpdateMask = 0xe0; + + var integerDecoder = new IntegerDecoder(); + Assert.False(integerDecoder.BeginTryDecode((byte)(headerFrame.Payload.Span[0] & ~DynamicTableSizeUpdateMask), prefixLength: 5, out _)); + Assert.False(integerDecoder.TryDecode(headerFrame.Payload.Span[1], out _)); + Assert.False(integerDecoder.TryDecode(headerFrame.Payload.Span[2], out _)); + Assert.True(integerDecoder.TryDecode(headerFrame.Payload.Span[3], out var result)); + + Assert.Equal(40000, result); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); } [Fact] @@ -3292,7 +3537,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloBytes, true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3305,7 +3550,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withStreamId: 1); await SendDataAsync(3, _helloBytes, true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -3378,7 +3623,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -3401,13 +3646,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // The headers, but not the data for the stream, can still be sent. await StartStreamAsync(3, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await StartStreamAsync(5, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 5); @@ -3466,12 +3711,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } }); - async Task VerifyStreamBackpressure(int streamId) + async Task VerifyStreamBackpressure(int streamId, int headersLength) { await StartStreamAsync(streamId, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: headersLength, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: streamId); @@ -3484,9 +3729,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.False(writeTasks[streamId].IsCompleted); } - await VerifyStreamBackpressure(1); - await VerifyStreamBackpressure(3); - await VerifyStreamBackpressure(5); + await VerifyStreamBackpressure(1, 32); + await VerifyStreamBackpressure(3, 2); + await VerifyStreamBackpressure(5, 2); // Close all pipes and wait for response to drain _pair.Application.Output.Complete(); @@ -3704,7 +3949,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -3761,7 +4006,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloWorldBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -3800,7 +4045,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloWorldBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -3852,7 +4097,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _twoContinuationsRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3879,7 +4124,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // The second stream should end first, since the first one is waiting for the request body. await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -3902,7 +4147,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendContinuationAsync(1, Http2ContinuationFrameFlags.END_HEADERS, trailers); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -4000,7 +4245,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendEmptyContinuationFrameAsync(1, Http2ContinuationFrameFlags.END_HEADERS); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -4015,7 +4260,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 12343, + withLength: 12342, withFlags: (byte)Http2HeadersFrameFlags.END_STREAM, withStreamId: 1); var continuationFrame1 = await ExpectAsync(Http2FrameType.CONTINUATION, @@ -4174,7 +4419,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -4224,7 +4469,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloBytes, true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -4257,8 +4502,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(3, _browserRequestHeaders, endStream: false); await SendDataAsync(1, _helloBytes, true); - await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + var f = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -4271,7 +4516,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withStreamId: 1); await SendDataAsync(3, _helloBytes, true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -4361,7 +4606,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs index a59207ccda..efed04f302 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 52, + withLength: 51, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 53, + withLength: 52, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -131,7 +131,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 57, + withLength: 56, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 58, + withLength: 57, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -193,7 +193,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 100, + withLength: 99, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -235,7 +235,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -326,7 +326,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -355,7 +355,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 47, + withLength: 46, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -386,7 +386,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 47, + withLength: 46, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -417,7 +417,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 47, + withLength: 46, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -448,7 +448,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 47, + withLength: 46, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -570,7 +570,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -611,7 +611,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -655,7 +655,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, new byte[8], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -698,7 +698,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, new byte[8], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -751,7 +751,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, new byte[8], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -983,7 +983,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1015,7 +1015,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.RST_STREAM, @@ -1054,7 +1054,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1092,7 +1092,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1125,7 +1125,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1160,7 +1160,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1198,7 +1198,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1236,7 +1236,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1276,7 +1276,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1323,7 +1323,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1361,7 +1361,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1397,7 +1397,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1441,7 +1441,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1475,7 +1475,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1508,7 +1508,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1552,7 +1552,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1591,7 +1591,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: false); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 41, + withLength: 40, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1634,7 +1634,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1674,7 +1674,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, new byte[6], endStream: false); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 41, + withLength: 40, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1733,7 +1733,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, new byte[6], endStream: false); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 41, + withLength: 40, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1788,7 +1788,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1814,7 +1814,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1852,7 +1852,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_STREAM | Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); @@ -1883,7 +1883,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame1 = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var trailersFrame1 = await ExpectAsync(Http2FrameType.HEADERS, @@ -1894,12 +1894,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(3, _browserRequestHeaders, endStream: true); var headersFrame2 = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 3); var trailersFrame2 = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 25, + withLength: 1, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -1930,7 +1930,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1980,7 +1980,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2039,7 +2039,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2074,7 +2074,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2124,7 +2124,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true).DefaultTimeout(); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1).DefaultTimeout(); @@ -2189,7 +2189,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2235,7 +2235,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -2269,7 +2269,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -2532,7 +2532,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -2623,7 +2623,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2671,7 +2671,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // Just the StatusCode gets written before aborting in the continuation frame await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.NONE, withStreamId: 1); @@ -2700,7 +2700,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -2743,7 +2743,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2789,7 +2789,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -2835,7 +2835,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -2884,7 +2884,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -2937,7 +2937,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -2987,7 +2987,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -3037,7 +3037,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -3080,7 +3080,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -3126,7 +3126,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -3168,7 +3168,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3213,7 +3213,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3279,7 +3279,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3325,7 +3325,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3361,7 +3361,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3413,7 +3413,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3465,7 +3465,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3498,7 +3498,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // Don't receive content length because we called WriteAsync which caused an invalid response var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS | (byte)Http2HeadersFrameFlags.END_STREAM, withStreamId: 1); @@ -3531,7 +3531,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3583,7 +3583,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3639,7 +3639,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS, @@ -3705,7 +3705,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3761,7 +3761,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -3826,7 +3826,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3885,7 +3885,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -3941,7 +3941,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -4003,7 +4003,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4077,7 +4077,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4153,7 +4153,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4224,7 +4224,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4296,7 +4296,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4380,7 +4380,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: false); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4461,7 +4461,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4548,7 +4548,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: false); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4608,7 +4608,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, LatinHeaderData, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index f80e5ad386..03b5e277ed 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -121,6 +121,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests internal readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); internal readonly HPackDecoder _hpackDecoder; + internal readonly HPackEncoder _hpackEncoder; private readonly byte[] _headerEncodingBuffer = new byte[Http2PeerSettings.MinAllowedMaxFrameSize]; internal readonly TimeoutControl _timeoutControl; @@ -165,6 +166,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public Http2TestBase() { _hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize, MaxRequestHeaderFieldSize); + _hpackEncoder = new HPackEncoder(); _timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object); _mockTimeoutControl = new Mock(_timeoutControl) { CallBase = true }; @@ -501,7 +503,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _runningStreams[streamId] = tcs; - writableBuffer.WriteStartStream(streamId, GetHeadersEnumerator(headers), _headerEncodingBuffer, endStream); + writableBuffer.WriteStartStream(streamId, _hpackEncoder, GetHeadersEnumerator(headers), _headerEncodingBuffer, endStream); return FlushAsync(writableBuffer); } @@ -541,7 +543,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests extendedHeader[0] = padLength; var payload = buffer.Slice(extendedHeaderLength, buffer.Length - padLength - extendedHeaderLength); - HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out var length); + HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, GetHeadersEnumerator(headers), payload, out var length); var padding = buffer.Slice(extendedHeaderLength + length, padLength); padding.Fill(0); @@ -584,7 +586,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests extendedHeader[4] = priority; var payload = buffer.Slice(extendedHeaderLength); - HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out var length); + HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, GetHeadersEnumerator(headers), payload, out var length); frame.PayloadLength = extendedHeaderLength + length; @@ -631,7 +633,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests extendedHeader[5] = priority; var payload = buffer.Slice(extendedHeaderLength, buffer.Length - padLength - extendedHeaderLength); - HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out var length); + HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, GetHeadersEnumerator(headers), payload, out var length); var padding = buffer.Slice(extendedHeaderLength + length, padLength); padding.Fill(0); @@ -745,7 +747,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests frame.PrepareHeaders(flags, streamId); var buffer = _headerEncodingBuffer.AsMemory(); - var done = HPackHeaderWriter.BeginEncodeHeaders(headersEnumerator, buffer.Span, out var length); + var done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, headersEnumerator, buffer.Span, out var length); frame.PayloadLength = length; Http2FrameWriter.WriteHeader(frame, outputWriter); @@ -815,7 +817,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests frame.PrepareContinuation(flags, streamId); var buffer = _headerEncodingBuffer.AsMemory(); - var done = HPackHeaderWriter.ContinueEncodeHeaders(headersEnumerator, buffer.Span, out var length); + var done = HPackHeaderWriter.ContinueEncodeHeaders(_hpackEncoder, headersEnumerator, buffer.Span, out var length); frame.PayloadLength = length; Http2FrameWriter.WriteHeader(frame, outputWriter); @@ -843,7 +845,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests frame.PrepareContinuation(flags, streamId); var buffer = _headerEncodingBuffer.AsMemory(); - var done = HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), buffer.Span, out var length); + var done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, GetHeadersEnumerator(headers), buffer.Span, out var length); frame.PayloadLength = length; Http2FrameWriter.WriteHeader(frame, outputWriter); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs index 087664a7e0..4432e85dc6 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs @@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.RequestHeaders), Times.Once); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -283,7 +283,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloWorldBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -336,7 +336,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -390,7 +390,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloWorldBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -445,7 +445,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -501,7 +501,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -513,7 +513,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(3, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -567,7 +567,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloWorldBytes, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -616,7 +616,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -669,7 +669,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -682,7 +682,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(3, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -738,7 +738,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -756,7 +756,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(3, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -813,7 +813,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloWorldBytes, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -885,7 +885,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(3, _helloWorldBytes, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -902,7 +902,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests backpressureTcs.SetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs index dafbd6eca7..178a464b44 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs @@ -285,6 +285,264 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests } } + [Fact] + public async Task ExecutionContextMutationsOfValueTypeDoNotLeakAcrossRequestsOnSameConnection() + { + var local = new AsyncLocal(); + + // It's important this method isn't async as that will revert the ExecutionContext + Task ExecuteApplication(HttpContext context) + { + var value = local.Value; + Assert.Equal(0, value); + + context.Response.OnStarting(() => + { + local.Value++; + return Task.CompletedTask; + }); + + context.Response.OnCompleted(() => + { + local.Value++; + return Task.CompletedTask; + }); + + local.Value++; + context.Response.ContentLength = 1; + return context.Response.WriteAsync($"{value}"); + } + + var testContext = new TestServiceContext(LoggerFactory); + + await using var server = new TestServer(ExecuteApplication, testContext); + await TestAsyncLocalValues(testContext, server); + } + + [Fact] + public async Task ExecutionContextMutationsOfReferenceTypeDoNotLeakAcrossRequestsOnSameConnection() + { + var local = new AsyncLocal(); + + // It's important this method isn't async as that will revert the ExecutionContext + Task ExecuteApplication(HttpContext context) + { + Assert.Null(local.Value); + local.Value = new IntAsClass(); + + var value = local.Value.Value; + Assert.Equal(0, value); + + context.Response.OnStarting(() => + { + local.Value.Value++; + return Task.CompletedTask; + }); + + context.Response.OnCompleted(() => + { + local.Value.Value++; + return Task.CompletedTask; + }); + + local.Value.Value++; + context.Response.ContentLength = 1; + return context.Response.WriteAsync($"{value}"); + } + + var testContext = new TestServiceContext(LoggerFactory); + + await using var server = new TestServer(ExecuteApplication, testContext); + await TestAsyncLocalValues(testContext, server); + } + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + [Fact] + public async Task ExecutionContextMutationsDoNotLeakAcrossAwaits() + { + var local = new AsyncLocal(); + + // It's important this method isn't async as that will revert the ExecutionContext + Task ExecuteApplication(HttpContext context) + { + var value = local.Value; + Assert.Equal(0, value); + + context.Response.OnStarting(async () => + { + local.Value++; + Assert.Equal(1, local.Value); + }); + + context.Response.OnCompleted(async () => + { + local.Value++; + Assert.Equal(1, local.Value); + }); + + context.Response.ContentLength = 1; + return context.Response.WriteAsync($"{value}"); + } + + var testContext = new TestServiceContext(LoggerFactory); + + await using var server = new TestServer(ExecuteApplication, testContext); + await TestAsyncLocalValues(testContext, server); + } + + [Fact] + public async Task ExecutionContextMutationsOfValueTypeFlowIntoButNotOutOfAsyncEvents() + { + var local = new AsyncLocal(); + + async Task ExecuteApplication(HttpContext context) + { + var value = local.Value; + Assert.Equal(0, value); + + context.Response.OnStarting(async () => + { + local.Value++; + Assert.Equal(2, local.Value); + }); + + context.Response.OnCompleted(async () => + { + local.Value++; + Assert.Equal(2, local.Value); + }); + + local.Value++; + Assert.Equal(1, local.Value); + + context.Response.ContentLength = 1; + await context.Response.WriteAsync($"{value}"); + + local.Value++; + Assert.Equal(2, local.Value); + } + + var testContext = new TestServiceContext(LoggerFactory); + + await using var server = new TestServer(ExecuteApplication, testContext); + await TestAsyncLocalValues(testContext, server); + } + + [Fact] + public async Task ExecutionContextMutationsOfReferenceTypeFlowThroughAsyncEvents() + { + var local = new AsyncLocal(); + + async Task ExecuteApplication(HttpContext context) + { + Assert.Null(local.Value); + local.Value = new IntAsClass(); + + var value = local.Value.Value; + Assert.Equal(0, value); // Start + + context.Response.OnStarting(async () => + { + local.Value.Value++; + Assert.Equal(2, local.Value.Value); // Second + }); + + context.Response.OnCompleted(async () => + { + local.Value.Value++; + Assert.Equal(4, local.Value.Value); // Fourth + }); + + local.Value.Value++; + Assert.Equal(1, local.Value.Value); // First + + context.Response.ContentLength = 1; + await context.Response.WriteAsync($"{value}"); + + local.Value.Value++; + Assert.Equal(3, local.Value.Value); // Third + } + + var testContext = new TestServiceContext(LoggerFactory); + + await using var server = new TestServer(ExecuteApplication, testContext); + await TestAsyncLocalValues(testContext, server); + } +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + [Fact] + public async Task ExecutionContextMutationsOfValueTypeFlowIntoButNotOutOfNonAsyncEvents() + { + var local = new AsyncLocal(); + + async Task ExecuteApplication(HttpContext context) + { + var value = local.Value; + Assert.Equal(0, value); + + context.Response.OnStarting(() => + { + local.Value++; + Assert.Equal(2, local.Value); + + return Task.CompletedTask; + }); + + context.Response.OnCompleted(() => + { + local.Value++; + Assert.Equal(2, local.Value); + + return Task.CompletedTask; + }); + + local.Value++; + Assert.Equal(1, local.Value); + + context.Response.ContentLength = 1; + await context.Response.WriteAsync($"{value}"); + + local.Value++; + Assert.Equal(2, local.Value); + } + + var testContext = new TestServiceContext(LoggerFactory); + + await using var server = new TestServer(ExecuteApplication, testContext); + await TestAsyncLocalValues(testContext, server); + } + + private static async Task TestAsyncLocalValues(TestServiceContext testContext, TestServer server) + { + using var connection = server.CreateConnection(); + + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 1", + "", + "0"); + + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 1", + "", + "0"); + } + [Fact] public async Task AppCanSetTraceIdentifier() { @@ -1803,5 +2061,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests } public static TheoryData HostHeaderData => HttpParsingData.HostHeaderData; + + private class IntAsClass + { + public int Value; + } } } diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs index 18faacc44d..f53e083bc3 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs @@ -1118,7 +1118,7 @@ namespace Interop.FunctionalTests Assert.Equal(oneKbString + i, response.Headers.GetValues("header" + i).Single()); } - Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("sending HEADERS frame for stream ID 1 with length 15612 and flags END_STREAM"))); + Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("sending HEADERS frame for stream ID 1 with length 15610 and flags END_STREAM"))); Assert.Equal(2, TestSink.Writes.Where(context => context.Message.Contains("sending CONTINUATION frame for stream ID 1 with length 15585 and flags NONE")).Count()); Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("sending CONTINUATION frame for stream ID 1 with length 14546 and flags END_HEADERS"))); diff --git a/src/Servers/test/FunctionalTests/.vsconfig b/src/Servers/test/FunctionalTests/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Servers/test/FunctionalTests/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Shared/.vsconfig b/src/Shared/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Shared/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Shared/Hpack/EncoderHeaderEntry.cs b/src/Shared/Hpack/EncoderHeaderEntry.cs new file mode 100644 index 0000000000..75a0aebde2 --- /dev/null +++ b/src/Shared/Hpack/EncoderHeaderEntry.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace System.Net.Http.HPack +{ + [DebuggerDisplay("Name = {Name} Value = {Value}")] + internal class EncoderHeaderEntry + { + // Header name and value + public string Name; + public string Value; + + // Chained list of headers in the same bucket + public EncoderHeaderEntry Next; + public int Hash; + + // Compute dynamic table index + public int Index; + + // Doubly linked list + public EncoderHeaderEntry Before; + public EncoderHeaderEntry After; + + /// + /// Initialize header values. An entry will be reinitialized when reused. + /// + public void Initialize(int hash, string name, string value, int index, EncoderHeaderEntry next) + { + Debug.Assert(name != null); + Debug.Assert(value != null); + + Name = name; + Value = value; + Index = index; + Hash = hash; + Next = next; + } + + public uint CalculateSize() + { + return (uint)HeaderField.GetLength(Name.Length, Value.Length); + } + + /// + /// Remove entry from the linked list and reset header values. + /// + public void Remove() + { + Before.After = After; + After.Before = Before; + Before = null; + After = null; + Next = null; + Hash = 0; + Name = null; + Value = null; + } + + /// + /// Add before an entry in the linked list. + /// + public void AddBefore(EncoderHeaderEntry existingEntry) + { + After = existingEntry; + Before = existingEntry.Before; + Before.After = this; + After.Before = this; + } + } +} diff --git a/src/Shared/Hpack/HPackEncoder.Dynamic.cs b/src/Shared/Hpack/HPackEncoder.Dynamic.cs new file mode 100644 index 0000000000..f8e7f4c93d --- /dev/null +++ b/src/Shared/Hpack/HPackEncoder.Dynamic.cs @@ -0,0 +1,295 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable enable +using System.Diagnostics; + +namespace System.Net.Http.HPack +{ + internal partial class HPackEncoder + { + public const int DefaultHeaderTableSize = 4096; + + // Internal for testing + internal readonly EncoderHeaderEntry Head; + + private readonly bool _allowDynamicCompression; + private readonly EncoderHeaderEntry[] _headerBuckets; + private readonly byte _hashMask; + private uint _headerTableSize; + private uint _maxHeaderTableSize; + private bool _pendingTableSizeUpdate; + private EncoderHeaderEntry? _removed; + + public HPackEncoder(bool allowDynamicCompression = true, uint maxHeaderTableSize = DefaultHeaderTableSize) + { + _allowDynamicCompression = allowDynamicCompression; + _maxHeaderTableSize = maxHeaderTableSize; + Head = new EncoderHeaderEntry(); + Head.Initialize(-1, string.Empty, string.Empty, int.MaxValue, null); + // Bucket count balances memory usage and the expected low number of headers (constrained by the header table size). + // Performance with different bucket counts hasn't been measured in detail. + _headerBuckets = new EncoderHeaderEntry[16]; + _hashMask = (byte)(_headerBuckets.Length - 1); + Head.Before = Head.After = Head; + } + + public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize) + { + if (_maxHeaderTableSize != maxHeaderTableSize) + { + _maxHeaderTableSize = maxHeaderTableSize; + + // Dynamic table size update will be written next HEADERS frame + _pendingTableSizeUpdate = true; + + // Check capacity and remove entries that exceed the new capacity + EnsureCapacity(0); + } + } + + public bool EnsureDynamicTableSizeUpdate(Span buffer, out int length) + { + // Check if there is a table size update that should be encoded + if (_pendingTableSizeUpdate) + { + bool success = EncodeDynamicTableSizeUpdate((int)_maxHeaderTableSize, buffer, out length); + _pendingTableSizeUpdate = false; + return success; + } + + length = 0; + return true; + } + + public bool EncodeHeader(Span buffer, int staticTableIndex, HeaderEncodingHint encodingHint, string name, string value, out int bytesWritten) + { + Debug.Assert(!_pendingTableSizeUpdate, "Dynamic table size update should be encoded before headers."); + + // Never index sensitive value. + if (encodingHint == HeaderEncodingHint.NeverIndex) + { + int index = ResolveDynamicTableIndex(staticTableIndex, name); + + return index == -1 + ? EncodeLiteralHeaderFieldNeverIndexingNewName(name, value, buffer, out bytesWritten) + : EncodeLiteralHeaderFieldNeverIndexing(index, value, buffer, out bytesWritten); + } + + // No dynamic table. Only use the static table. + if (!_allowDynamicCompression || _maxHeaderTableSize == 0 || encodingHint == HeaderEncodingHint.IgnoreIndex) + { + return staticTableIndex == -1 + ? EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten) + : EncodeLiteralHeaderFieldWithoutIndexing(staticTableIndex, value, buffer, out bytesWritten); + } + + // Header is greater than the maximum table size. + // Don't attempt to add dynamic header as all existing dynamic headers will be removed. + if (HeaderField.GetLength(name.Length, value.Length) > _maxHeaderTableSize) + { + int index = ResolveDynamicTableIndex(staticTableIndex, name); + + return index == -1 + ? EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten) + : EncodeLiteralHeaderFieldWithoutIndexing(index, value, buffer, out bytesWritten); + } + + return EncodeDynamicHeader(buffer, staticTableIndex, name, value, out bytesWritten); + } + + private int ResolveDynamicTableIndex(int staticTableIndex, string name) + { + if (staticTableIndex != -1) + { + // Prefer static table index. + return staticTableIndex; + } + + return CalculateDynamicTableIndex(name); + } + + private bool EncodeDynamicHeader(Span buffer, int staticTableIndex, string name, string value, out int bytesWritten) + { + EncoderHeaderEntry? headerField = GetEntry(name, value); + if (headerField != null) + { + // Already exists in dynamic table. Write index. + int index = CalculateDynamicTableIndex(headerField.Index); + return EncodeIndexedHeaderField(index, buffer, out bytesWritten); + } + else + { + // Doesn't exist in dynamic table. Add new entry to dynamic table. + uint headerSize = (uint)HeaderField.GetLength(name.Length, value.Length); + + int index = ResolveDynamicTableIndex(staticTableIndex, name); + bool success = index == -1 + ? EncodeLiteralHeaderFieldIndexingNewName(name, value, buffer, out bytesWritten) + : EncodeLiteralHeaderFieldIndexing(index, value, buffer, out bytesWritten); + + if (success) + { + EnsureCapacity(headerSize); + AddHeaderEntry(name, value, headerSize); + } + + return success; + } + } + + /// + /// Ensure there is capacity for the new header. If there is not enough capacity then remove + /// existing headers until space is available. + /// + private void EnsureCapacity(uint headerSize) + { + Debug.Assert(headerSize <= _maxHeaderTableSize, "Header is bigger than dynamic table size."); + + while (_maxHeaderTableSize - _headerTableSize < headerSize) + { + EncoderHeaderEntry? removed = RemoveHeaderEntry(); + Debug.Assert(removed != null); + + // Removed entries are tracked to be reused. + PushRemovedEntry(removed); + } + } + + private EncoderHeaderEntry? GetEntry(string name, string value) + { + if (_headerTableSize == 0) + { + return null; + } + int hash = name.GetHashCode(); + int bucketIndex = CalculateBucketIndex(hash); + for (EncoderHeaderEntry? e = _headerBuckets[bucketIndex]; e != null; e = e.Next) + { + // We've already looked up entries based on a hash of the name. + // Compare value before name as it is more likely to be different. + if (e.Hash == hash && + string.Equals(value, e.Value, StringComparison.Ordinal) && + string.Equals(name, e.Name, StringComparison.Ordinal)) + { + return e; + } + } + return null; + } + + private int CalculateDynamicTableIndex(string name) + { + if (_headerTableSize == 0) + { + return -1; + } + int hash = name.GetHashCode(); + int bucketIndex = CalculateBucketIndex(hash); + for (EncoderHeaderEntry? e = _headerBuckets[bucketIndex]; e != null; e = e.Next) + { + if (e.Hash == hash && string.Equals(name, e.Name, StringComparison.Ordinal)) + { + return CalculateDynamicTableIndex(e.Index); + } + } + return -1; + } + + private int CalculateDynamicTableIndex(int index) + { + return index == -1 ? -1 : index - Head.Before.Index + 1 + H2StaticTable.Count; + } + + private void AddHeaderEntry(string name, string value, uint headerSize) + { + Debug.Assert(headerSize <= _maxHeaderTableSize, "Header is bigger than dynamic table size."); + Debug.Assert(headerSize <= _maxHeaderTableSize - _headerTableSize, "Not enough room in dynamic table."); + + int hash = name.GetHashCode(); + int bucketIndex = CalculateBucketIndex(hash); + EncoderHeaderEntry? oldEntry = _headerBuckets[bucketIndex]; + // Attempt to reuse removed entry + EncoderHeaderEntry? newEntry = PopRemovedEntry() ?? new EncoderHeaderEntry(); + newEntry.Initialize(hash, name, value, Head.Before.Index - 1, oldEntry); + _headerBuckets[bucketIndex] = newEntry; + newEntry.AddBefore(Head); + _headerTableSize += headerSize; + } + + private void PushRemovedEntry(EncoderHeaderEntry removed) + { + if (_removed != null) + { + removed.Next = _removed; + } + _removed = removed; + } + + private EncoderHeaderEntry? PopRemovedEntry() + { + if (_removed != null) + { + EncoderHeaderEntry? removed = _removed; + _removed = _removed.Next; + return removed; + } + + return null; + } + + /// + /// Remove the oldest entry. + /// + private EncoderHeaderEntry? RemoveHeaderEntry() + { + if (_headerTableSize == 0) + { + return null; + } + EncoderHeaderEntry? eldest = Head.After; + int hash = eldest.Hash; + int bucketIndex = CalculateBucketIndex(hash); + EncoderHeaderEntry? prev = _headerBuckets[bucketIndex]; + EncoderHeaderEntry? e = prev; + while (e != null) + { + EncoderHeaderEntry next = e.Next; + if (e == eldest) + { + if (prev == eldest) + { + _headerBuckets[bucketIndex] = next; + } + else + { + prev.Next = next; + } + _headerTableSize -= eldest.CalculateSize(); + eldest.Remove(); + return eldest; + } + prev = e; + e = next; + } + return null; + } + + private int CalculateBucketIndex(int hash) + { + return hash & _hashMask; + } + } + + /// + /// Hint for how the header should be encoded as HPack. This value can be overriden. + /// For example, a header that is larger than the dynamic table won't be indexed. + /// + internal enum HeaderEncodingHint + { + Index, + IgnoreIndex, + NeverIndex + } +} diff --git a/src/Shared/Hpack/README.md b/src/Shared/Hpack/README.md new file mode 100644 index 0000000000..d18485ccea --- /dev/null +++ b/src/Shared/Hpack/README.md @@ -0,0 +1,3 @@ +HPack dynamic compression. These files are kept separate to help avoid ASP.NET Core dependencies being added to them. + +Runtime currently doesn't implement HPack dynamic compression. These files will move into runtime shareable code in the future when support is added to runtime. \ No newline at end of file diff --git a/src/Shared/Hpack/StatusCodes.cs b/src/Shared/Hpack/StatusCodes.cs new file mode 100644 index 0000000000..eb67205586 --- /dev/null +++ b/src/Shared/Hpack/StatusCodes.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Globalization; +using System.Text; + +namespace System.Net.Http.HPack +{ + internal static partial class StatusCodes + { + public static string ToStatusString(int statusCode) + { + switch (statusCode) + { + case (int)HttpStatusCode.Continue: + return "100"; + case (int)HttpStatusCode.SwitchingProtocols: + return "101"; + case (int)HttpStatusCode.Processing: + return "102"; + + case (int)HttpStatusCode.OK: + return "200"; + case (int)HttpStatusCode.Created: + return "201"; + case (int)HttpStatusCode.Accepted: + return "202"; + case (int)HttpStatusCode.NonAuthoritativeInformation: + return "203"; + case (int)HttpStatusCode.NoContent: + return "204"; + case (int)HttpStatusCode.ResetContent: + return "205"; + case (int)HttpStatusCode.PartialContent: + return "206"; + case (int)HttpStatusCode.MultiStatus: + return "207"; + case (int)HttpStatusCode.AlreadyReported: + return "208"; + case (int)HttpStatusCode.IMUsed: + return "226"; + + case (int)HttpStatusCode.MultipleChoices: + return "300"; + case (int)HttpStatusCode.MovedPermanently: + return "301"; + case (int)HttpStatusCode.Found: + return "302"; + case (int)HttpStatusCode.SeeOther: + return "303"; + case (int)HttpStatusCode.NotModified: + return "304"; + case (int)HttpStatusCode.UseProxy: + return "305"; + case (int)HttpStatusCode.Unused: + return "306"; + case (int)HttpStatusCode.TemporaryRedirect: + return "307"; + case (int)HttpStatusCode.PermanentRedirect: + return "308"; + + case (int)HttpStatusCode.BadRequest: + return "400"; + case (int)HttpStatusCode.Unauthorized: + return "401"; + case (int)HttpStatusCode.PaymentRequired: + return "402"; + case (int)HttpStatusCode.Forbidden: + return "403"; + case (int)HttpStatusCode.NotFound: + return "404"; + case (int)HttpStatusCode.MethodNotAllowed: + return "405"; + case (int)HttpStatusCode.NotAcceptable: + return "406"; + case (int)HttpStatusCode.ProxyAuthenticationRequired: + return "407"; + case (int)HttpStatusCode.RequestTimeout: + return "408"; + case (int)HttpStatusCode.Conflict: + return "409"; + case (int)HttpStatusCode.Gone: + return "410"; + case (int)HttpStatusCode.LengthRequired: + return "411"; + case (int)HttpStatusCode.PreconditionFailed: + return "412"; + case (int)HttpStatusCode.RequestEntityTooLarge: + return "413"; + case (int)HttpStatusCode.RequestUriTooLong: + return "414"; + case (int)HttpStatusCode.UnsupportedMediaType: + return "415"; + case (int)HttpStatusCode.RequestedRangeNotSatisfiable: + return "416"; + case (int)HttpStatusCode.ExpectationFailed: + return "417"; + case (int)418: + return "418"; + case (int)419: + return "419"; + case (int)HttpStatusCode.MisdirectedRequest: + return "421"; + case (int)HttpStatusCode.UnprocessableEntity: + return "422"; + case (int)HttpStatusCode.Locked: + return "423"; + case (int)HttpStatusCode.FailedDependency: + return "424"; + case (int)HttpStatusCode.UpgradeRequired: + return "426"; + case (int)HttpStatusCode.PreconditionRequired: + return "428"; + case (int)HttpStatusCode.TooManyRequests: + return "429"; + case (int)HttpStatusCode.RequestHeaderFieldsTooLarge: + return "431"; + case (int)HttpStatusCode.UnavailableForLegalReasons: + return "451"; + + case (int)HttpStatusCode.InternalServerError: + return "500"; + case (int)HttpStatusCode.NotImplemented: + return "501"; + case (int)HttpStatusCode.BadGateway: + return "502"; + case (int)HttpStatusCode.ServiceUnavailable: + return "503"; + case (int)HttpStatusCode.GatewayTimeout: + return "504"; + case (int)HttpStatusCode.HttpVersionNotSupported: + return "505"; + case (int)HttpStatusCode.VariantAlsoNegotiates: + return "506"; + case (int)HttpStatusCode.InsufficientStorage: + return "507"; + case (int)HttpStatusCode.LoopDetected: + return "508"; + case (int)HttpStatusCode.NotExtended: + return "510"; + case (int)HttpStatusCode.NetworkAuthenticationRequired: + return "511"; + + default: + return statusCode.ToString(CultureInfo.InvariantCulture); + + } + } + } +} diff --git a/src/Shared/Process/ProcessEx.cs b/src/Shared/Process/ProcessEx.cs index c1743a2f0a..3f5ca0aead 100644 --- a/src/Shared/Process/ProcessEx.cs +++ b/src/Shared/Process/ProcessEx.cs @@ -26,9 +26,11 @@ namespace Microsoft.AspNetCore.Internal private readonly StringBuilder _stderrCapture; private readonly StringBuilder _stdoutCapture; private readonly object _pipeCaptureLock = new object(); + private readonly object _testOutputLock = new object(); private BlockingCollection _stdoutLines; private TaskCompletionSource _exited; private CancellationTokenSource _stdoutLinesCancellationSource = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + private bool _disposed = false; public ProcessEx(ITestOutputHelper output, Process proc) { @@ -135,7 +137,13 @@ namespace Microsoft.AspNetCore.Internal _stderrCapture.AppendLine(e.Data); } - _output.WriteLine("[ERROR] " + e.Data); + lock (_testOutputLock) + { + if (!_disposed) + { + _output.WriteLine("[ERROR] " + e.Data); + } + } } private void OnOutputData(object sender, DataReceivedEventArgs e) @@ -150,7 +158,13 @@ namespace Microsoft.AspNetCore.Internal _stdoutCapture.AppendLine(e.Data); } - _output.WriteLine(e.Data); + lock (_testOutputLock) + { + if (!_disposed) + { + _output.WriteLine(e.Data); + } + } if (_stdoutLines != null) { @@ -204,6 +218,11 @@ namespace Microsoft.AspNetCore.Internal public void Dispose() { + lock (_testOutputLock) + { + _disposed = true; + } + if (_process != null && !_process.HasExited) { _process.KillTree(); diff --git a/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs b/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs index d09f184134..97cdea1c50 100644 --- a/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs +++ b/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs @@ -8,7 +8,7 @@ using System.Diagnostics; namespace System.Net.Http.HPack { - internal static class HPackEncoder + internal partial class HPackEncoder { // Things we should add: // * Huffman encoding @@ -109,6 +109,70 @@ namespace System.Net.Http.HPack return false; } + /// Encodes a "Literal Header Field never Indexing". + public static bool EncodeLiteralHeaderFieldNeverIndexing(int index, string value, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.2.3 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | Index (4+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + + if ((uint)destination.Length >= 2) + { + destination[0] = 0x10; + if (IntegerEncoder.Encode(index, 4, destination, out int indexLength)) + { + Debug.Assert(indexLength >= 1); + if (EncodeStringLiteral(value, destination.Slice(indexLength), out int nameLength)) + { + bytesWritten = indexLength + nameLength; + return true; + } + } + } + + bytesWritten = 0; + return false; + } + + /// Encodes a "Literal Header Field with Indexing". + public static bool EncodeLiteralHeaderFieldIndexing(int index, string value, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.2.2 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | Index (6+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + + if ((uint)destination.Length >= 2) + { + destination[0] = 0x40; + if (IntegerEncoder.Encode(index, 6, destination, out int indexLength)) + { + Debug.Assert(indexLength >= 1); + if (EncodeStringLiteral(value, destination.Slice(indexLength), out int nameLength)) + { + bytesWritten = indexLength + nameLength; + return true; + } + } + } + + bytesWritten = 0; + return false; + } + /// /// Encodes a "Literal Header Field without Indexing", but only the index portion; /// a subsequent call to EncodeStringLiteral must be used to encode the associated value. @@ -144,6 +208,27 @@ namespace System.Net.Http.HPack return false; } + /// Encodes a "Literal Header Field with Indexing - New Name". + public static bool EncodeLiteralHeaderFieldIndexingNewName(string name, string value, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.2.2 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | 0 | + // +---+---+-----------------------+ + // | H | Name Length (7+) | + // +---+---------------------------+ + // | Name String (Length octets) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + + return EncodeLiteralHeaderNewNameCore(0x40, name, value, destination, out bytesWritten); + } + /// Encodes a "Literal Header Field without Indexing - New Name". public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, string value, Span destination, out int bytesWritten) { @@ -162,9 +247,35 @@ namespace System.Net.Http.HPack // | Value String (Length octets) | // +-------------------------------+ + return EncodeLiteralHeaderNewNameCore(0, name, value, destination, out bytesWritten); + } + + /// Encodes a "Literal Header Field never Indexing - New Name". + public static bool EncodeLiteralHeaderFieldNeverIndexingNewName(string name, string value, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.2.3 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | 0 | + // +---+---+-----------------------+ + // | H | Name Length (7+) | + // +---+---------------------------+ + // | Name String (Length octets) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + + return EncodeLiteralHeaderNewNameCore(0x10, name, value, destination, out bytesWritten); + } + + private static bool EncodeLiteralHeaderNewNameCore(byte mask, string name, string value, Span destination, out int bytesWritten) + { if ((uint)destination.Length >= 3) { - destination[0] = 0; + destination[0] = mask; if (EncodeLiteralHeaderName(name, destination.Slice(1), out int nameLength) && EncodeStringLiteral(value, destination.Slice(1 + nameLength), out int valueLength)) { @@ -372,6 +483,25 @@ namespace System.Net.Http.HPack return false; } + public static bool EncodeDynamicTableSizeUpdate(int value, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.3 + // ---------------------------------------------------- + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | Max size (5+) | + // +---+---------------------------+ + + if (destination.Length != 0) + { + destination[0] = 0x20; + return IntegerEncoder.Encode(value, 5, destination, out bytesWritten); + } + + bytesWritten = 0; + return false; + } + public static bool EncodeStringLiterals(ReadOnlySpan values, string? separator, Span destination, out int bytesWritten) { bytesWritten = 0; diff --git a/src/Shared/runtime/Http2/Hpack/StatusCodes.cs b/src/Shared/runtime/Http2/Hpack/StatusCodes.cs index b701fa79f4..01c42abbc5 100644 --- a/src/Shared/runtime/Http2/Hpack/StatusCodes.cs +++ b/src/Shared/runtime/Http2/Hpack/StatusCodes.cs @@ -7,7 +7,7 @@ using System.Text; namespace System.Net.Http.HPack { - internal static class StatusCodes + internal static partial class StatusCodes { // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static diff --git a/src/SignalR/.vsconfig b/src/SignalR/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/SignalR/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/SignalR/Directory.Build.props b/src/SignalR/Directory.Build.props index 27a3751cd8..74b4c7adee 100644 --- a/src/SignalR/Directory.Build.props +++ b/src/SignalR/Directory.Build.props @@ -21,7 +21,6 @@ PreserveNewest - diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs index 2220685e82..afe4254ded 100644 --- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs +++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs @@ -1335,6 +1335,102 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests } } + [Theory] + [QuarantinedTest] + [MemberData(nameof(HubProtocolsList))] + public async Task ServerLogsErrorIfClientInvokeCannotBeSerialized(string protocolName) + { + // Just to help sanity check that the right exception is thrown + var exceptionSubstring = protocolName switch + { + "json" => "A possible object cycle was detected.", + "newtonsoft-json" => "A possible object cycle was detected.", + "messagepack" => "Failed to serialize Microsoft.AspNetCore.SignalR.Client.FunctionalTests.TestHub+Unserializable value.", + var x => throw new Exception($"The test does not have an exception string for the protocol '{x}'!"), + }; + + var protocol = HubProtocols[protocolName]; + using (var server = await StartServer(write => write.EventId.Name == "FailedWritingMessage")) + { + var connection = CreateHubConnection(server.Url, "/default", HttpTransportType.WebSockets, protocol, LoggerFactory); + var closedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection.Closed += (ex) => { closedTcs.TrySetResult(ex); return Task.CompletedTask; }; + try + { + await connection.StartAsync().OrTimeout(); + + var result = connection.InvokeAsync(nameof(TestHub.CallWithUnserializableObject)); + + // The connection should close. + Assert.Null(await closedTcs.Task.OrTimeout()); + + await Assert.ThrowsAsync(() => result).OrTimeout(); + } + catch (Exception ex) + { + LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); + throw; + } + finally + { + await connection.DisposeAsync().OrTimeout(); + } + + var errorLog = server.GetLogs().SingleOrDefault(r => r.Write.EventId.Name == "FailedWritingMessage"); + Assert.NotNull(errorLog); + Assert.Contains(exceptionSubstring, errorLog.Write.Exception.Message); + Assert.Equal(LogLevel.Error, errorLog.Write.LogLevel); + } + } + + [Theory] + [QuarantinedTest] + [MemberData(nameof(HubProtocolsList))] + public async Task ServerLogsErrorIfReturnValueCannotBeSerialized(string protocolName) + { + // Just to help sanity check that the right exception is thrown + var exceptionSubstring = protocolName switch + { + "json" => "A possible object cycle was detected.", + "newtonsoft-json" => "A possible object cycle was detected.", + "messagepack" => "Failed to serialize Microsoft.AspNetCore.SignalR.Client.FunctionalTests.TestHub+Unserializable value.", + var x => throw new Exception($"The test does not have an exception string for the protocol '{x}'!"), + }; + + var protocol = HubProtocols[protocolName]; + using (var server = await StartServer(write => write.EventId.Name == "FailedWritingMessage")) + { + var connection = CreateHubConnection(server.Url, "/default", HttpTransportType.LongPolling, protocol, LoggerFactory); + var closedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection.Closed += (ex) => { closedTcs.TrySetResult(ex); return Task.CompletedTask; }; + try + { + await connection.StartAsync().OrTimeout(); + + var result = connection.InvokeAsync(nameof(TestHub.GetUnserializableObject)).OrTimeout(); + + // The connection should close. + Assert.Null(await closedTcs.Task.OrTimeout()); + + await Assert.ThrowsAsync(() => result).OrTimeout(); + } + catch (Exception ex) + { + LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); + throw; + } + finally + { + await connection.DisposeAsync().OrTimeout(); + } + + var errorLog = server.GetLogs().SingleOrDefault(r => r.Write.EventId.Name == "FailedWritingMessage"); + Assert.NotNull(errorLog); + Assert.Contains(exceptionSubstring, errorLog.Write.Exception.Message); + Assert.Equal(LogLevel.Error, errorLog.Write.LogLevel); + } + } + [Fact] public async Task RandomGenericIsNotTreatedAsStream() { diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs index 01ce4198c4..90328a6b57 100644 --- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs +++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs @@ -95,6 +95,33 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests { return Context.Features.Get().TransportType.ToString(); } + + public async Task CallWithUnserializableObject() + { + await Clients.All.SendAsync("Foo", Unserializable.Create()); + } + + public Unserializable GetUnserializableObject() + { + return Unserializable.Create(); + } + + public class Unserializable + { + public Unserializable Child { get; private set; } + + private Unserializable() + { + } + + internal static Unserializable Create() + { + // Loops throw off every serializer ;). + var o = new Unserializable(); + o.Child = o; + return o; + } + } } public class DynamicTestHub : DynamicHub diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs index abf6b69524..c9495d7156 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs @@ -84,13 +84,6 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal Features.Set(this); } - internal HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application, ILogger logger = null) - : this(id, null, logger) - { - Transport = transport; - Application = application; - } - public CancellationTokenSource Cancellation { get; set; } public HttpTransportType TransportType { get; set; } diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs index b0f4b079fb..aec84c5dab 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs @@ -10,7 +10,6 @@ using System.IO.Pipelines; using System.Net.WebSockets; using System.Security.Cryptography; using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Internal; @@ -31,24 +30,14 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal private readonly TimerAwaitable _nextHeartbeat; private readonly ILogger _logger; private readonly ILogger _connectionLogger; - private readonly bool _useSendTimeout = true; private readonly TimeSpan _disconnectTimeout; - public HttpConnectionManager(ILoggerFactory loggerFactory, IHostApplicationLifetime appLifetime) - : this(loggerFactory, appLifetime, Options.Create(new ConnectionOptions() { DisconnectTimeout = ConnectionOptionsSetup.DefaultDisconectTimeout })) - { - } - public HttpConnectionManager(ILoggerFactory loggerFactory, IHostApplicationLifetime appLifetime, IOptions connectionOptions) { _logger = loggerFactory.CreateLogger(); _connectionLogger = loggerFactory.CreateLogger(); _nextHeartbeat = new TimerAwaitable(_heartbeatTickRate, _heartbeatTickRate); _disconnectTimeout = connectionOptions.Value.DisconnectTimeout ?? ConnectionOptionsSetup.DefaultDisconectTimeout; - if (AppContext.TryGetSwitch("Microsoft.AspNetCore.Http.Connections.DoNotUseSendTimeout", out var timeoutDisabled)) - { - _useSendTimeout = !timeoutDisabled; - } // Register these last as the callbacks could run immediately appLifetime.ApplicationStarted.Register(() => Start()); @@ -176,7 +165,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal } else { - if (!Debugger.IsAttached && _useSendTimeout) + if (!Debugger.IsAttached) { connection.TryCancelSend(utcNow.Ticks); } diff --git a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs index 34626ca05b..5f084a232f 100644 --- a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs +++ b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs @@ -2347,10 +2347,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory) { - return new HttpConnectionManager(loggerFactory ?? new LoggerFactory(), new EmptyApplicationLifetime()); + return CreateConnectionManager(loggerFactory, null); } - private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory, TimeSpan disconnectTimeout) + private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory, TimeSpan? disconnectTimeout) { var connectionOptions = new ConnectionOptions(); connectionOptions.DisconnectTimeout = disconnectTimeout; diff --git a/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs b/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs index 05a29f0e73..dcf8dfefa9 100644 --- a/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs +++ b/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs @@ -7,6 +7,7 @@ using System.IO.Pipelines; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http.Connections.Internal; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.SignalR.Tests; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -411,7 +412,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory, IHostApplicationLifetime lifetime = null) { lifetime = lifetime ?? new EmptyApplicationLifetime(); - return new HttpConnectionManager(loggerFactory, lifetime); + return new HttpConnectionManager(loggerFactory, lifetime, Options.Create(new ConnectionOptions())); } [Flags] diff --git a/src/SignalR/common/Http.Connections/test/WebSocketsTests.cs b/src/SignalR/common/Http.Connections/test/WebSocketsTests.cs index 27ad53d9d9..b5e50d7894 100644 --- a/src/SignalR/common/Http.Connections/test/WebSocketsTests.cs +++ b/src/SignalR/common/Http.Connections/test/WebSocketsTests.cs @@ -31,11 +31,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application, LoggerFactory.CreateLogger("HttpConnectionContext1")); + var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext1")) + { + Transport = pair.Transport, + Application = pair.Application, + }; using (var feature = new TestWebSocketConnectionFeature()) { - var connectionContext = new HttpConnectionContext(string.Empty, null, null, LoggerFactory.CreateLogger("HttpConnectionContext2")); + var connectionContext = new HttpConnectionContext(string.Empty, connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext2")); var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory); // Give the server socket to the transport and run it @@ -79,11 +83,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application, LoggerFactory.CreateLogger("HttpConnectionContext1")); + var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext1")) + { + Transport = pair.Transport, + Application = pair.Application, + }; using (var feature = new TestWebSocketConnectionFeature()) { - var connectionContext = new HttpConnectionContext(string.Empty, null, null, LoggerFactory.CreateLogger("HttpConnectionContext2")); + var connectionContext = new HttpConnectionContext(string.Empty, connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext2")); connectionContext.ActiveFormat = transferFormat; var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory); @@ -116,7 +124,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application, LoggerFactory.CreateLogger("HttpConnectionContext1")); + var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext1")) + { + Transport = pair.Transport, + Application = pair.Application, + }; using (var feature = new TestWebSocketConnectionFeature()) { @@ -139,7 +151,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests } } - var connectionContext = new HttpConnectionContext(string.Empty, null, null, LoggerFactory.CreateLogger("HttpConnectionContext2")); + var connectionContext = new HttpConnectionContext(string.Empty, connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext2")); var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory); // Give the server socket to the transport and run it @@ -169,7 +181,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application); + var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext))) + { + Transport = pair.Transport, + Application = pair.Application, + }; using (var feature = new TestWebSocketConnectionFeature()) { @@ -201,7 +217,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application); + var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext))) + { + Transport = pair.Transport, + Application = pair.Application, + }; using (var feature = new TestWebSocketConnectionFeature()) { @@ -236,7 +256,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application); + var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext))) + { + Transport = pair.Transport, + Application = pair.Application, + }; using (var feature = new TestWebSocketConnectionFeature()) { @@ -271,7 +295,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application); + var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext))) + { + Transport = pair.Transport, + Application = pair.Application, + }; using (var feature = new TestWebSocketConnectionFeature()) { @@ -311,7 +339,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application); + var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext))) + { + Transport = pair.Transport, + Application = pair.Application, + }; using (var feature = new TestWebSocketConnectionFeature()) { @@ -354,7 +386,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests using (StartVerifiableLog()) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application); + var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext))) + { + Transport = pair.Transport, + Application = pair.Application, + }; using (var feature = new TestWebSocketConnectionFeature()) { diff --git a/src/SignalR/common/Protocols.MessagePack/src/MessagePackHubProtocolOptions.cs b/src/SignalR/common/Protocols.MessagePack/src/MessagePackHubProtocolOptions.cs index 85025b6444..bd2ad8f788 100644 --- a/src/SignalR/common/Protocols.MessagePack/src/MessagePackHubProtocolOptions.cs +++ b/src/SignalR/common/Protocols.MessagePack/src/MessagePackHubProtocolOptions.cs @@ -9,24 +9,31 @@ namespace Microsoft.AspNetCore.SignalR { public class MessagePackHubProtocolOptions { - private IList _formatterResolvers; + private MessagePackSerializerOptions _messagePackSerializerOptions; - public IList FormatterResolvers + /// + /// Gets or sets the used internally by the . + /// If you override the default value, we strongly recommend that you set to by calling: + /// customMessagePackSerializerOptions = customMessagePackSerializerOptions.WithSecurity(MessagePackSecurity.UntrustedData) + /// If you modify the default options you must also assign the updated options back to the property: + /// options.SerializerOptions = options.SerializerOptions.WithResolver(new CustomResolver()); + /// + public MessagePackSerializerOptions SerializerOptions { get { - if (_formatterResolvers == null) + if (_messagePackSerializerOptions == null) { // The default set of resolvers trigger a static constructor that throws on AOT environments. // This gives users the chance to use an AOT friendly formatter. - _formatterResolvers = MessagePackHubProtocol.CreateDefaultFormatterResolvers(); + _messagePackSerializerOptions = MessagePackHubProtocol.CreateDefaultMessagePackSerializerOptions(); } - return _formatterResolvers; + return _messagePackSerializerOptions; } set { - _formatterResolvers = value; + _messagePackSerializerOptions = value; } } } diff --git a/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocol.cs b/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocol.cs index e1a762937b..ffa814da22 100644 --- a/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocol.cs +++ b/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocol.cs @@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol private const int NonVoidResult = 3; private readonly MessagePackSerializerOptions _msgPackSerializerOptions; + private static readonly string ProtocolName = "messagepack"; private static readonly int ProtocolVersion = 1; @@ -52,37 +53,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol /// The options used to initialize the protocol. public MessagePackHubProtocol(IOptions options) { - var msgPackOptions = options.Value; - var resolver = SignalRResolver.Instance; - var hasCustomFormatterResolver = false; - - // if counts don't match then we know users customized resolvers so we set up the options with the provided resolvers - if (msgPackOptions.FormatterResolvers.Count != SignalRResolver.Resolvers.Count) - { - hasCustomFormatterResolver = true; - } - else - { - // Compare each "reference" in the FormatterResolvers IList<> against the default "SignalRResolver.Resolvers" IList<> - for (var i = 0; i < msgPackOptions.FormatterResolvers.Count; i++) - { - // check if the user customized the resolvers - if (msgPackOptions.FormatterResolvers[i] != SignalRResolver.Resolvers[i]) - { - hasCustomFormatterResolver = true; - break; - } - } - } - - if (hasCustomFormatterResolver) - { - resolver = CompositeResolver.Create(Array.Empty(), (IReadOnlyList)msgPackOptions.FormatterResolvers); - } - - _msgPackSerializerOptions = MessagePackSerializerOptions.Standard - .WithResolver(resolver) - .WithSecurity(MessagePackSecurity.UntrustedData); + _msgPackSerializerOptions = options.Value.SerializerOptions; } /// @@ -656,17 +627,17 @@ namespace Microsoft.AspNetCore.SignalR.Protocol } } - internal static List CreateDefaultFormatterResolvers() - { - // Copy to allow users to add/remove resolvers without changing the static SignalRResolver list - return new List(SignalRResolver.Resolvers); - } + internal static MessagePackSerializerOptions CreateDefaultMessagePackSerializerOptions() => + MessagePackSerializerOptions + .Standard + .WithResolver(SignalRResolver.Instance) + .WithSecurity(MessagePackSecurity.UntrustedData); internal class SignalRResolver : IFormatterResolver { public static readonly IFormatterResolver Instance = new SignalRResolver(); - public static readonly IList Resolvers = new IFormatterResolver[] + public static readonly IReadOnlyList Resolvers = new IFormatterResolver[] { DynamicEnumAsStringResolver.Instance, ContractlessStandardResolver.Instance, diff --git a/src/SignalR/common/Shared/ISystemClock.cs b/src/SignalR/common/Shared/ISystemClock.cs new file mode 100644 index 0000000000..86e2b035ea --- /dev/null +++ b/src/SignalR/common/Shared/ISystemClock.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Internal +{ + internal interface ISystemClock + { + /// + /// Retrieves the current UTC system time. + /// + DateTimeOffset UtcNow { get; } + + /// + /// Retrieves ticks for the current UTC system time. + /// + long UtcNowTicks { get; } + + /// + /// Retrieves the current UTC system time. + /// This is only safe to use from code called by the Heartbeat. + /// + DateTimeOffset UtcNowUnsynchronized { get; } + } +} diff --git a/src/SignalR/common/Shared/SystemClock.cs b/src/SignalR/common/Shared/SystemClock.cs new file mode 100644 index 0000000000..de5a9b711c --- /dev/null +++ b/src/SignalR/common/Shared/SystemClock.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Internal +{ + /// + /// Provides access to the normal system clock. + /// + internal class SystemClock : ISystemClock + { + /// + /// Retrieves the current UTC system time. + /// + public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; + + /// + /// Retrieves ticks for the current UTC system time. + /// + public long UtcNowTicks => DateTimeOffset.UtcNow.Ticks; + + /// + /// Retrieves the current UTC system time. + /// + public DateTimeOffset UtcNowUnsynchronized => DateTimeOffset.UtcNow; + } +} diff --git a/src/SignalR/common/testassets/Tests.Utils/InProcessTestServer.cs b/src/SignalR/common/testassets/Tests.Utils/InProcessTestServer.cs index a02e4bb1de..631220c7de 100644 --- a/src/SignalR/common/testassets/Tests.Utils/InProcessTestServer.cs +++ b/src/SignalR/common/testassets/Tests.Utils/InProcessTestServer.cs @@ -84,6 +84,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests _logger = _loggerFactory.CreateLogger>(); } + public IList GetLogs() => _logSinkProvider.GetLogs(); + private async Task StartServerInner() { // We're using 127.0.0.1 instead of localhost to ensure that we use IPV4 across different OSes diff --git a/src/SignalR/common/testassets/Tests.Utils/LogRecord.cs b/src/SignalR/common/testassets/Tests.Utils/LogRecord.cs index 9193044270..7c9d9f071c 100644 --- a/src/SignalR/common/testassets/Tests.Utils/LogRecord.cs +++ b/src/SignalR/common/testassets/Tests.Utils/LogRecord.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging.Testing; namespace Microsoft.AspNetCore.SignalR.Tests { // WriteContext, but with a timestamp... - internal class LogRecord + public class LogRecord { public DateTime Timestamp { get; } public WriteContext Write { get; } diff --git a/src/SignalR/common/testassets/Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj b/src/SignalR/common/testassets/Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj index 9e69675720..782c8410b8 100644 --- a/src/SignalR/common/testassets/Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj +++ b/src/SignalR/common/testassets/Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj @@ -21,7 +21,6 @@ - diff --git a/src/SignalR/server/Core/src/HubConnectionContext.cs b/src/SignalR/server/Core/src/HubConnectionContext.cs index 1dc7ad89c4..fcd704c428 100644 --- a/src/SignalR/server/Core/src/HubConnectionContext.cs +++ b/src/SignalR/server/Core/src/HubConnectionContext.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.Logging; @@ -34,13 +35,11 @@ namespace Microsoft.AspNetCore.SignalR private readonly long _keepAliveInterval; private readonly long _clientTimeoutInterval; private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1); - private readonly bool _useAbsoluteClientTimeout; private readonly object _receiveMessageTimeoutLock = new object(); + private readonly ISystemClock _systemClock; private StreamTracker _streamTracker; - private long _lastSendTimeStamp = DateTime.UtcNow.Ticks; - private long _lastReceivedTimeStamp = DateTime.UtcNow.Ticks; - private bool _receivedMessageThisInterval = false; + private long _lastSendTimeStamp; private ReadOnlyMemory _cachedPingMessage; private bool _clientTimeoutActive; private volatile bool _connectionAborted; @@ -70,10 +69,8 @@ namespace Microsoft.AspNetCore.SignalR HubCallerContext = new DefaultHubCallerContext(this); - if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SignalR.UseAbsoluteClientTimeout", out var useAbsoluteClientTimeout)) - { - _useAbsoluteClientTimeout = useAbsoluteClientTimeout; - } + _systemClock = contextOptions.SystemClock ?? new SystemClock(); + _lastSendTimeStamp = _systemClock.UtcNowTicks; } internal StreamTracker StreamTracker @@ -558,7 +555,7 @@ namespace Microsoft.AspNetCore.SignalR private void KeepAliveTick() { - var currentTime = DateTime.UtcNow.Ticks; + var currentTime = _systemClock.UtcNowTicks; // Implements the keep-alive tick behavior // Each tick, we check if the time since the last send is larger than the keep alive duration (in ticks). @@ -597,35 +594,17 @@ namespace Microsoft.AspNetCore.SignalR return; } - if (_useAbsoluteClientTimeout) + lock (_receiveMessageTimeoutLock) { - // If it's been too long since we've heard from the client, then close this - if (DateTime.UtcNow.Ticks - Volatile.Read(ref _lastReceivedTimeStamp) > _clientTimeoutInterval) + if (_receivedMessageTimeoutEnabled) { - if (!_receivedMessageThisInterval) + _receivedMessageElapsedTicks = _systemClock.UtcNowTicks - _receivedMessageTimestamp; + + if (_receivedMessageElapsedTicks >= _clientTimeoutInterval) { Log.ClientTimeout(_logger, TimeSpan.FromTicks(_clientTimeoutInterval)); AbortAllowReconnect(); } - - _receivedMessageThisInterval = false; - Volatile.Write(ref _lastReceivedTimeStamp, DateTime.UtcNow.Ticks); - } - } - else - { - lock (_receiveMessageTimeoutLock) - { - if (_receivedMessageTimeoutEnabled) - { - _receivedMessageElapsedTicks = DateTime.UtcNow.Ticks - _receivedMessageTimestamp; - - if (_receivedMessageElapsedTicks >= _clientTimeoutInterval) - { - Log.ClientTimeout(_logger, TimeSpan.FromTicks(_clientTimeoutInterval)); - AbortAllowReconnect(); - } - } } } } @@ -670,37 +649,24 @@ namespace Microsoft.AspNetCore.SignalR } } - internal void ResetClientTimeout() - { - _receivedMessageThisInterval = true; - } - internal void BeginClientTimeout() { - // check if new timeout behavior is in use - if (!_useAbsoluteClientTimeout) + lock (_receiveMessageTimeoutLock) { - lock (_receiveMessageTimeoutLock) - { - _receivedMessageTimeoutEnabled = true; - _receivedMessageTimestamp = DateTime.UtcNow.Ticks; - } + _receivedMessageTimeoutEnabled = true; + _receivedMessageTimestamp = _systemClock.UtcNowTicks; } } internal void StopClientTimeout() { - // check if new timeout behavior is in use - if (!_useAbsoluteClientTimeout) + lock (_receiveMessageTimeoutLock) { - lock (_receiveMessageTimeoutLock) - { - // we received a message so stop the timer and reset it - // it will resume after the message has been processed - _receivedMessageElapsedTicks = 0; - _receivedMessageTimestamp = 0; - _receivedMessageTimeoutEnabled = false; - } + // we received a message so stop the timer and reset it + // it will resume after the message has been processed + _receivedMessageElapsedTicks = 0; + _receivedMessageTimestamp = 0; + _receivedMessageTimeoutEnabled = false; } } @@ -723,7 +689,7 @@ namespace Microsoft.AspNetCore.SignalR LoggerMessage.Define(LogLevel.Debug, new EventId(5, "HandshakeFailed"), "Failed connection handshake."); private static readonly Action _failedWritingMessage = - LoggerMessage.Define(LogLevel.Debug, new EventId(6, "FailedWritingMessage"), "Failed writing message. Aborting connection."); + LoggerMessage.Define(LogLevel.Error, new EventId(6, "FailedWritingMessage"), "Failed writing message. Aborting connection."); private static readonly Action _protocolVersionFailed = LoggerMessage.Define(LogLevel.Debug, new EventId(7, "ProtocolVersionFailed"), "Server does not support version {Version} of the {Protocol} protocol."); diff --git a/src/SignalR/server/Core/src/HubConnectionContextOptions.cs b/src/SignalR/server/Core/src/HubConnectionContextOptions.cs index c95c58afe6..fc1d34383e 100644 --- a/src/SignalR/server/Core/src/HubConnectionContextOptions.cs +++ b/src/SignalR/server/Core/src/HubConnectionContextOptions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Internal; namespace Microsoft.AspNetCore.SignalR { @@ -29,5 +30,7 @@ namespace Microsoft.AspNetCore.SignalR /// Gets or sets the maximum message size the client can send. /// public long? MaximumReceiveMessageSize { get; set; } + + internal ISystemClock SystemClock { get; set; } } } diff --git a/src/SignalR/server/Core/src/HubConnectionHandler.cs b/src/SignalR/server/Core/src/HubConnectionHandler.cs index 0a8f3380f9..6667890496 100644 --- a/src/SignalR/server/Core/src/HubConnectionHandler.cs +++ b/src/SignalR/server/Core/src/HubConnectionHandler.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.DependencyInjection; @@ -31,6 +32,9 @@ namespace Microsoft.AspNetCore.SignalR private readonly bool _enableDetailedErrors; private readonly long? _maximumMessageSize; + // Internal for testing + internal ISystemClock SystemClock { get; set; } = new SystemClock(); + /// /// Initializes a new instance of the class. /// @@ -98,6 +102,7 @@ namespace Microsoft.AspNetCore.SignalR ClientTimeoutInterval = _hubOptions.ClientTimeoutInterval ?? _globalHubOptions.ClientTimeoutInterval ?? HubOptionsSetup.DefaultClientTimeoutInterval, StreamBufferCapacity = _hubOptions.StreamBufferCapacity ?? _globalHubOptions.StreamBufferCapacity ?? HubOptionsSetup.DefaultStreamBufferCapacity, MaximumReceiveMessageSize = _maximumMessageSize, + SystemClock = SystemClock, }; Log.ConnectedStarting(_logger); @@ -223,8 +228,6 @@ namespace Microsoft.AspNetCore.SignalR var result = await input.ReadAsync(); var buffer = result.Buffer; - connection.ResetClientTimeout(); - try { if (result.IsCanceled) diff --git a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj index 9a18d8ac74..385a449849 100644 --- a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj +++ b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj @@ -14,6 +14,8 @@ + + diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/MockSystemClock.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/MockSystemClock.cs new file mode 100644 index 0000000000..2cd2eb617d --- /dev/null +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/MockSystemClock.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using Microsoft.AspNetCore.Internal; + +namespace Microsoft.AspNetCore.SignalR.Tests +{ + public class MockSystemClock : ISystemClock + { + private static Random _random = new Random(); + + private long _utcNowTicks; + + public MockSystemClock() + { + // Use a random DateTimeOffset to ensure tests that incorrectly use the current DateTimeOffset fail always instead of only rarely. + // Pick a date between the min DateTimeOffset and a day before the max DateTimeOffset so there's room to advance the clock. + _utcNowTicks = NextLong(DateTimeOffset.MinValue.Ticks, DateTimeOffset.MaxValue.Ticks - TimeSpan.FromDays(1).Ticks); + } + + public DateTimeOffset UtcNow + { + get + { + UtcNowCalled++; + return new DateTimeOffset(Interlocked.Read(ref _utcNowTicks), TimeSpan.Zero); + } + set + { + Interlocked.Exchange(ref _utcNowTicks, value.Ticks); + } + } + + public long UtcNowTicks => UtcNow.Ticks; + + public DateTimeOffset UtcNowUnsynchronized => UtcNow; + + public int UtcNowCalled { get; private set; } + + private long NextLong(long minValue, long maxValue) + { + return (long)(_random.NextDouble() * (maxValue - minValue) + minValue); + } + } +} diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs index aaf4cfa582..eb6d11a9dc 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs @@ -14,10 +14,12 @@ using System.Threading; using System.Threading.Tasks; using MessagePack; using MessagePack.Formatters; +using MessagePack.Resolvers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Connections.Features; +using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.AspNetCore.Testing; @@ -2370,7 +2372,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests services.AddSignalR() .AddMessagePackProtocol(options => { - options.FormatterResolvers.Insert(0, new CustomFormatter()); + options.SerializerOptions = MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new CustomFormatter(), options.SerializerOptions.Resolver)); }); }, LoggerFactory); @@ -2659,24 +2661,25 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { + var interval = 100; + var clock = new MockSystemClock(); var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => services.Configure(options => - options.KeepAliveInterval = TimeSpan.FromMilliseconds(100)), LoggerFactory); + options.KeepAliveInterval = TimeSpan.FromMilliseconds(interval)), LoggerFactory); var connectionHandler = serviceProvider.GetService>(); + connectionHandler.SystemClock = clock; using (var client = new TestClient(new NewtonsoftJsonHubProtocol())) { var connectionHandlerTask = await client.ConnectAsync(connectionHandler); await client.Connected.OrTimeout(); - // Wait 500 ms, but make sure to yield some time up to unblock concurrent threads - // This is useful on AppVeyor because it's slow enough to end up with no time - // being available for the endpoint to run. - for (var i = 0; i < 50; i += 1) + // Trigger multiple keep alives + var heartbeatCount = 5; + for (var i = 0; i < heartbeatCount; i++) { + clock.UtcNow = clock.UtcNow.AddMilliseconds(interval + 1); client.TickHeartbeat(); - await Task.Yield(); - await Task.Delay(10); } // Shut down @@ -2710,7 +2713,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests break; } } - Assert.InRange(pingCounter, 1, Int32.MaxValue); + Assert.Equal(heartbeatCount, pingCounter); } } } @@ -2720,10 +2723,13 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { + var timeout = 100; + var clock = new MockSystemClock(); var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => services.Configure(options => - options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(100)), LoggerFactory); + options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeout)), LoggerFactory); var connectionHandler = serviceProvider.GetService>(); + connectionHandler.SystemClock = clock; using (var client = new TestClient(new NewtonsoftJsonHubProtocol())) { @@ -2731,9 +2737,16 @@ namespace Microsoft.AspNetCore.SignalR.Tests await client.Connected.OrTimeout(); // This is a fake client -- it doesn't auto-ping to signal - // We go over the 100 ms timeout interval... - await Task.Delay(120); - client.TickHeartbeat(); + // We go over the 100 ms timeout interval multiple times + for (var i = 0; i < 3; i++) + { + clock.UtcNow = clock.UtcNow.AddMilliseconds(timeout + 1); + client.TickHeartbeat(); + } + + // Invoke a Hub method and wait for the result to reliably test if the connection is still active + var id = await client.SendInvocationAsync(nameof(MethodHub.ValueMethod)).OrTimeout(); + var result = await client.ReadAsync().OrTimeout(); // but client should still be open, since it never pinged to activate the timeout checking Assert.False(connectionHandlerTask.IsCompleted); @@ -2746,10 +2759,13 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { + var timeout = 100; + var clock = new MockSystemClock(); var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => services.Configure(options => - options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(100)), LoggerFactory); + options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeout)), LoggerFactory); var connectionHandler = serviceProvider.GetService>(); + connectionHandler.SystemClock = clock; using (var client = new TestClient(new NewtonsoftJsonHubProtocol())) { @@ -2757,10 +2773,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests await client.Connected.OrTimeout(); await client.SendHubMessageAsync(PingMessage.Instance); - await Task.Delay(300); - client.TickHeartbeat(); - - await Task.Delay(300); + clock.UtcNow = clock.UtcNow.AddMilliseconds(timeout + 1); client.TickHeartbeat(); await connectionHandlerTask.OrTimeout(); @@ -2774,10 +2787,13 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { + var timeout = 300; + var clock = new MockSystemClock(); var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => services.Configure(options => - options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(300)), LoggerFactory); + options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeout)), LoggerFactory); var connectionHandler = serviceProvider.GetService>(); + connectionHandler.SystemClock = clock; using (var client = new TestClient(new NewtonsoftJsonHubProtocol())) { @@ -2787,11 +2803,17 @@ namespace Microsoft.AspNetCore.SignalR.Tests for (int i = 0; i < 10; i++) { - await Task.Delay(100); + clock.UtcNow = clock.UtcNow.AddMilliseconds(timeout - 1); client.TickHeartbeat(); await client.SendHubMessageAsync(PingMessage.Instance); } + // Invoke a Hub method and wait for the result to reliably test if the connection is still active + var id = await client.SendInvocationAsync(nameof(MethodHub.ValueMethod)).OrTimeout(); + var result = await client.ReadAsync().OrTimeout(); + + Assert.IsType(result); + Assert.False(connectionHandlerTask.IsCompleted); } } @@ -3277,7 +3299,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests [Fact] public async Task ConnectionAbortedIfSendFailsWithProtocolError() { - using (StartVerifiableLog()) + using (StartVerifiableLog(write => write.EventId.Name == "FailedWritingMessage")) { var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => { diff --git a/src/Testing/src/Logging/BeginScopeContext.cs b/src/Testing/src/Logging/BeginScopeContext.cs new file mode 100644 index 0000000000..14ef991e0d --- /dev/null +++ b/src/Testing/src/Logging/BeginScopeContext.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Extensions.Logging.Testing +{ + public class BeginScopeContext + { + public object Scope { get; set; } + + public string LoggerName { get; set; } + } +} \ No newline at end of file diff --git a/src/Testing/src/Logging/ITestSink.cs b/src/Testing/src/Logging/ITestSink.cs new file mode 100644 index 0000000000..b328e5c595 --- /dev/null +++ b/src/Testing/src/Logging/ITestSink.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; + +namespace Microsoft.Extensions.Logging.Testing +{ + public interface ITestSink + { + event Action MessageLogged; + + event Action ScopeStarted; + + Func WriteEnabled { get; set; } + + Func BeginEnabled { get; set; } + + IProducerConsumerCollection Scopes { get; set; } + + IProducerConsumerCollection Writes { get; set; } + + void Write(WriteContext context); + + void Begin(BeginScopeContext context); + } +} diff --git a/src/Testing/src/Logging/LogLevelAttribute.cs b/src/Testing/src/Logging/LogLevelAttribute.cs new file mode 100644 index 0000000000..74aa395d4b --- /dev/null +++ b/src/Testing/src/Logging/LogLevelAttribute.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.Testing +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = false)] + public class LogLevelAttribute : Attribute + { + public LogLevelAttribute(LogLevel logLevel) + { + LogLevel = logLevel; + } + + public LogLevel LogLevel { get; } + } +} diff --git a/src/Testing/src/Logging/LogValuesAssert.cs b/src/Testing/src/Logging/LogValuesAssert.cs new file mode 100644 index 0000000000..ef2ff1f406 --- /dev/null +++ b/src/Testing/src/Logging/LogValuesAssert.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. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public static class LogValuesAssert + { + /// + /// Asserts that the given key and value are present in the actual values. + /// + /// The key of the item to be found. + /// The value of the item to be found. + /// The actual values. + public static void Contains( + string key, + object value, + IEnumerable> actualValues) + { + Contains(new[] { new KeyValuePair(key, value) }, actualValues); + } + + /// + /// Asserts that all the expected values are present in the actual values by ignoring + /// the order of values. + /// + /// Expected subset of values + /// Actual set of values + public static void Contains( + IEnumerable> expectedValues, + IEnumerable> actualValues) + { + if (expectedValues == null) + { + throw new ArgumentNullException(nameof(expectedValues)); + } + + if (actualValues == null) + { + throw new ArgumentNullException(nameof(actualValues)); + } + + var comparer = new LogValueComparer(); + + foreach (var expectedPair in expectedValues) + { + if (!actualValues.Contains(expectedPair, comparer)) + { + throw new EqualException( + expected: GetString(expectedValues), + actual: GetString(actualValues)); + } + } + } + + private static string GetString(IEnumerable> logValues) + { + return string.Join(",", logValues.Select(kvp => $"[{kvp.Key} {kvp.Value}]")); + } + + private class LogValueComparer : IEqualityComparer> + { + public bool Equals(KeyValuePair x, KeyValuePair y) + { + return string.Equals(x.Key, y.Key) && object.Equals(x.Value, y.Value); + } + + public int GetHashCode(KeyValuePair obj) + { + // We are never going to put this KeyValuePair in a hash table, + // so this is ok. + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Testing/src/Logging/TestLogger.cs b/src/Testing/src/Logging/TestLogger.cs new file mode 100644 index 0000000000..1f1b1d6aba --- /dev/null +++ b/src/Testing/src/Logging/TestLogger.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class TestLogger : ILogger + { + private object _scope; + private readonly ITestSink _sink; + private readonly string _name; + private readonly Func _filter; + + public TestLogger(string name, ITestSink sink, bool enabled) + : this(name, sink, _ => enabled) + { + } + + public TestLogger(string name, ITestSink sink, Func filter) + { + _sink = sink; + _name = name; + _filter = filter; + } + + public string Name { get; set; } + + public IDisposable BeginScope(TState state) + { + _scope = state; + + _sink.Begin(new BeginScopeContext() + { + LoggerName = _name, + Scope = state, + }); + + return TestDisposable.Instance; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + _sink.Write(new WriteContext() + { + LogLevel = logLevel, + EventId = eventId, + State = state, + Exception = exception, + Formatter = (s, e) => formatter((TState)s, e), + LoggerName = _name, + Scope = _scope + }); + } + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel != LogLevel.None && _filter(logLevel); + } + + private class TestDisposable : IDisposable + { + public static readonly TestDisposable Instance = new TestDisposable(); + + public void Dispose() + { + // intentionally does nothing + } + } + } +} \ No newline at end of file diff --git a/src/Testing/src/Logging/TestLoggerFactory.cs b/src/Testing/src/Logging/TestLoggerFactory.cs new file mode 100644 index 0000000000..a7f2f1398c --- /dev/null +++ b/src/Testing/src/Logging/TestLoggerFactory.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Extensions.Logging.Testing +{ + public class TestLoggerFactory : ILoggerFactory + { + private readonly ITestSink _sink; + private readonly bool _enabled; + + public TestLoggerFactory(ITestSink sink, bool enabled) + { + _sink = sink; + _enabled = enabled; + } + + public ILogger CreateLogger(string name) + { + return new TestLogger(name, _sink, _enabled); + } + + public void AddProvider(ILoggerProvider provider) + { + } + + public void Dispose() + { + } + } +} diff --git a/src/Testing/src/Logging/TestLoggerProvider.cs b/src/Testing/src/Logging/TestLoggerProvider.cs new file mode 100644 index 0000000000..e604bda36e --- /dev/null +++ b/src/Testing/src/Logging/TestLoggerProvider.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Extensions.Logging.Testing +{ + public class TestLoggerProvider : ILoggerProvider + { + private readonly ITestSink _sink; + + public TestLoggerProvider(ITestSink sink) + { + _sink = sink; + } + + public ILogger CreateLogger(string categoryName) + { + return new TestLogger(categoryName, _sink, enabled: true); + } + + public void Dispose() + { + } + } +} diff --git a/src/Testing/src/Logging/TestLoggerT.cs b/src/Testing/src/Logging/TestLoggerT.cs new file mode 100644 index 0000000000..096bb96535 --- /dev/null +++ b/src/Testing/src/Logging/TestLoggerT.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. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class TestLogger : ILogger + { + private readonly ILogger _logger; + + public TestLogger(TestLoggerFactory factory) + { + _logger = factory.CreateLogger(); + } + + public IDisposable BeginScope(TState state) + { + return _logger.BeginScope(state); + } + + public bool IsEnabled(LogLevel logLevel) + { + return _logger.IsEnabled(logLevel); + } + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception exception, + Func formatter) + { + _logger.Log(logLevel, eventId, state, exception, formatter); + } + } +} diff --git a/src/Testing/src/Logging/TestSink.cs b/src/Testing/src/Logging/TestSink.cs new file mode 100644 index 0000000000..5285b3068f --- /dev/null +++ b/src/Testing/src/Logging/TestSink.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class TestSink : ITestSink + { + private ConcurrentQueue _scopes; + private ConcurrentQueue _writes; + + public TestSink( + Func writeEnabled = null, + Func beginEnabled = null) + { + WriteEnabled = writeEnabled; + BeginEnabled = beginEnabled; + + _scopes = new ConcurrentQueue(); + _writes = new ConcurrentQueue(); + } + + public Func WriteEnabled { get; set; } + + public Func BeginEnabled { get; set; } + + public IProducerConsumerCollection Scopes { get => _scopes; set => _scopes = new ConcurrentQueue(value); } + + public IProducerConsumerCollection Writes { get => _writes; set => _writes = new ConcurrentQueue(value); } + + public event Action MessageLogged; + + public event Action ScopeStarted; + + public void Write(WriteContext context) + { + if (WriteEnabled == null || WriteEnabled(context)) + { + _writes.Enqueue(context); + } + MessageLogged?.Invoke(context); + } + + public void Begin(BeginScopeContext context) + { + if (BeginEnabled == null || BeginEnabled(context)) + { + _scopes.Enqueue(context); + } + ScopeStarted?.Invoke(context); + } + + public static bool EnableWithTypeName(WriteContext context) + { + return context.LoggerName.Equals(typeof(T).FullName); + } + + public static bool EnableWithTypeName(BeginScopeContext context) + { + return context.LoggerName.Equals(typeof(T).FullName); + } + } +} \ No newline at end of file diff --git a/src/Testing/src/Logging/WriteContext.cs b/src/Testing/src/Logging/WriteContext.cs new file mode 100644 index 0000000000..0ecfc8f1a9 --- /dev/null +++ b/src/Testing/src/Logging/WriteContext.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. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class WriteContext + { + public LogLevel LogLevel { get; set; } + + public EventId EventId { get; set; } + + public object State { get; set; } + + public Exception Exception { get; set; } + + public Func Formatter { get; set; } + + public object Scope { get; set; } + + public string LoggerName { get; set; } + + public string Message + { + get + { + return Formatter(State, Exception); + } + } + } +} \ No newline at end of file diff --git a/src/Testing/src/Logging/XunitLoggerFactoryExtensions.cs b/src/Testing/src/Logging/XunitLoggerFactoryExtensions.cs new file mode 100644 index 0000000000..7d053d45dd --- /dev/null +++ b/src/Testing/src/Logging/XunitLoggerFactoryExtensions.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Testing; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.Logging +{ + public static class XunitLoggerFactoryExtensions + { + public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output) + { + builder.Services.AddSingleton(new XunitLoggerProvider(output)); + return builder; + } + + public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output, LogLevel minLevel) + { + builder.Services.AddSingleton(new XunitLoggerProvider(output, minLevel)); + return builder; + } + + public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) + { + builder.Services.AddSingleton(new XunitLoggerProvider(output, minLevel, logStart)); + return builder; + } + + public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output) + { + loggerFactory.AddProvider(new XunitLoggerProvider(output)); + return loggerFactory; + } + + public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output, LogLevel minLevel) + { + loggerFactory.AddProvider(new XunitLoggerProvider(output, minLevel)); + return loggerFactory; + } + + public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) + { + loggerFactory.AddProvider(new XunitLoggerProvider(output, minLevel, logStart)); + return loggerFactory; + } + } +} diff --git a/src/Testing/src/Logging/XunitLoggerProvider.cs b/src/Testing/src/Logging/XunitLoggerProvider.cs new file mode 100644 index 0000000000..3a1d751413 --- /dev/null +++ b/src/Testing/src/Logging/XunitLoggerProvider.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using System.Text; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class XunitLoggerProvider : ILoggerProvider + { + private readonly ITestOutputHelper _output; + private readonly LogLevel _minLevel; + private readonly DateTimeOffset? _logStart; + + public XunitLoggerProvider(ITestOutputHelper output) + : this(output, LogLevel.Trace) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel) + : this(output, minLevel, null) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) + { + _output = output; + _minLevel = minLevel; + _logStart = logStart; + } + + public ILogger CreateLogger(string categoryName) + { + return new XunitLogger(_output, categoryName, _minLevel, _logStart); + } + + public void Dispose() + { + } + } + + public class XunitLogger : ILogger + { + private static readonly string[] NewLineChars = new[] { Environment.NewLine }; + private readonly string _category; + private readonly LogLevel _minLogLevel; + private readonly ITestOutputHelper _output; + private DateTimeOffset? _logStart; + + public XunitLogger(ITestOutputHelper output, string category, LogLevel minLogLevel, DateTimeOffset? logStart) + { + _minLogLevel = minLogLevel; + _category = category; + _output = output; + _logStart = logStart; + } + + public void Log( + LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + // Buffer the message into a single string in order to avoid shearing the message when running across multiple threads. + var messageBuilder = new StringBuilder(); + + var timestamp = _logStart.HasValue ? $"{(DateTimeOffset.UtcNow - _logStart.Value).TotalSeconds.ToString("N3")}s" : DateTimeOffset.UtcNow.ToString("s"); + + var firstLinePrefix = $"| [{timestamp}] {_category} {logLevel}: "; + var lines = formatter(state, exception).Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries); + messageBuilder.AppendLine(firstLinePrefix + lines.FirstOrDefault() ?? string.Empty); + + var additionalLinePrefix = "|" + new string(' ', firstLinePrefix.Length - 1); + foreach (var line in lines.Skip(1)) + { + messageBuilder.AppendLine(additionalLinePrefix + line); + } + + if (exception != null) + { + lines = exception.ToString().Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries); + additionalLinePrefix = "| "; + foreach (var line in lines) + { + messageBuilder.AppendLine(additionalLinePrefix + line); + } + } + + // Remove the last line-break, because ITestOutputHelper only has WriteLine. + var message = messageBuilder.ToString(); + if (message.EndsWith(Environment.NewLine)) + { + message = message.Substring(0, message.Length - Environment.NewLine.Length); + } + + try + { + _output.WriteLine(message); + } + catch (Exception) + { + // We could fail because we're on a background thread and our captured ITestOutputHelper is + // busted (if the test "completed" before the background thread fired). + // So, ignore this. There isn't really anything we can do but hope the + // caller has additional loggers registered + } + } + + public bool IsEnabled(LogLevel logLevel) + => logLevel >= _minLogLevel; + + public IDisposable BeginScope(TState state) + => new NullScope(); + + private class NullScope : IDisposable + { + public void Dispose() + { + } + } + } +} diff --git a/src/Testing/src/Microsoft.AspNetCore.Testing.csproj b/src/Testing/src/Microsoft.AspNetCore.Testing.csproj index 5ddad7b645..7d95e026a7 100644 --- a/src/Testing/src/Microsoft.AspNetCore.Testing.csproj +++ b/src/Testing/src/Microsoft.AspNetCore.Testing.csproj @@ -19,7 +19,9 @@ - + + + diff --git a/src/Testing/test/LogValuesAssertTest.cs b/src/Testing/test/LogValuesAssertTest.cs new file mode 100644 index 0000000000..dc2db9d83d --- /dev/null +++ b/src/Testing/test/LogValuesAssertTest.cs @@ -0,0 +1,222 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing.Tests +{ + public class LogValuesAssertTest + { + public static TheoryData< + IEnumerable>, + IEnumerable>> ExpectedValues_SubsetOf_ActualValuesData + { + get + { + return new TheoryData< + IEnumerable>, + IEnumerable>>() + { + { + new KeyValuePair[] { }, + new KeyValuePair[] { } + }, + { + // subset + new KeyValuePair[] { }, + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id") + } + }, + { + // subset + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id") + }, + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteConstraint", "Something") + } + }, + { + // equal number of values + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id") + }, + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id"), + } + } + }; + } + } + + [Theory] + [MemberData(nameof(ExpectedValues_SubsetOf_ActualValuesData))] + public void Asserts_Success_ExpectedValues_SubsetOf_ActualValues( + IEnumerable> expectedValues, + IEnumerable> actualValues) + { + // Act && Assert + LogValuesAssert.Contains(expectedValues, actualValues); + } + + public static TheoryData< + IEnumerable>, + IEnumerable>> ExpectedValues_MoreThan_ActualValuesData + { + get + { + return new TheoryData< + IEnumerable>, + IEnumerable>>() + { + { + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id") + }, + new KeyValuePair[] { } + }, + { + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteConstraint", "Something") + }, + new[] + { + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id") + } + } + }; + } + } + + [Theory] + [MemberData(nameof(ExpectedValues_MoreThan_ActualValuesData))] + public void Asserts_Failure_ExpectedValues_MoreThan_ActualValues( + IEnumerable> expectedValues, + IEnumerable> actualValues) + { + // Act && Assert + var equalException = Assert.Throws( + () => LogValuesAssert.Contains(expectedValues, actualValues)); + + Assert.Equal(GetString(expectedValues), equalException.Expected); + Assert.Equal(GetString(actualValues), equalException.Actual); + } + + [Fact] + public void Asserts_Success_IgnoringOrderOfItems() + { + // Arrange + var expectedLogValues = new[] + { + new KeyValuePair("RouteConstraint", "Something"), + new KeyValuePair("RouteValue", "Failure"), + new KeyValuePair("RouteKey", "id") + }; + var actualLogValues = new[] + { + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteConstraint", "Something"), + new KeyValuePair("RouteValue", "Failure"), + }; + + // Act && Assert + LogValuesAssert.Contains(expectedLogValues, actualLogValues); + } + + [Fact] + public void Asserts_Success_OnSpecifiedKeyAndValue() + { + // Arrange + var actualLogValues = new[] + { + new KeyValuePair("RouteConstraint", "Something"), + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteValue", "Failure"), + }; + + // Act && Assert + LogValuesAssert.Contains("RouteKey", "id", actualLogValues); + } + + public static TheoryData< + IEnumerable>, + IEnumerable>> CaseSensitivityComparisionData + { + get + { + return new TheoryData< + IEnumerable>, + IEnumerable>>() + { + { + new[] + { + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteValue", "Failure"), + }, + new[] + { + new KeyValuePair("ROUTEKEY", "id"), + new KeyValuePair("RouteValue", "Failure"), + } + }, + { + new[] + { + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteValue", "Failure"), + }, + new[] + { + new KeyValuePair("RouteKey", "id"), + new KeyValuePair("RouteValue", "FAILURE"), + } + } + }; + } + } + + [Theory] + [MemberData(nameof(CaseSensitivityComparisionData))] + public void DefaultComparer_Performs_CaseSensitiveComparision( + IEnumerable> expectedValues, + IEnumerable> actualValues) + { + // Act && Assert + var equalException = Assert.Throws( + () => LogValuesAssert.Contains(expectedValues, actualValues)); + + Assert.Equal(GetString(expectedValues), equalException.Expected); + Assert.Equal(GetString(actualValues), equalException.Actual); + } + + private string GetString(IEnumerable> logValues) + { + return logValues == null ? + "Null" : + string.Join(",", logValues.Select(kvp => $"[{kvp.Key} {kvp.Value}]")); + } + } +} diff --git a/src/Testing/test/XunitLoggerProviderTest.cs b/src/Testing/test/XunitLoggerProviderTest.cs new file mode 100644 index 0000000000..e43447c465 --- /dev/null +++ b/src/Testing/test/XunitLoggerProviderTest.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text.RegularExpressions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Logging.Testing.Tests +{ + public class XunitLoggerProviderTest + { + [Fact] + public void LoggerProviderWritesToTestOutputHelper() + { + var testTestOutputHelper = new TestTestOutputHelper(); + + var loggerFactory = CreateTestLogger(builder => builder + .SetMinimumLevel(LogLevel.Trace) + .AddXunit(testTestOutputHelper)); + + var logger = loggerFactory.CreateLogger("TestCategory"); + logger.LogInformation("This is some great information"); + logger.LogTrace("This is some unimportant information"); + + var expectedOutput = + "| [TIMESTAMP] TestCategory Information: This is some great information" + Environment.NewLine + + "| [TIMESTAMP] TestCategory Trace: This is some unimportant information" + Environment.NewLine; + + Assert.Equal(expectedOutput, MakeConsistent(testTestOutputHelper.Output)); + } + + [Fact] + public void LoggerProviderDoesNotWriteLogMessagesBelowMinimumLevel() + { + var testTestOutputHelper = new TestTestOutputHelper(); + var loggerFactory = CreateTestLogger(builder => builder + .AddXunit(testTestOutputHelper, LogLevel.Warning)); + + var logger = loggerFactory.CreateLogger("TestCategory"); + logger.LogInformation("This is some great information"); + logger.LogError("This is a bad error"); + + Assert.Equal("| [TIMESTAMP] TestCategory Error: This is a bad error" + Environment.NewLine, MakeConsistent(testTestOutputHelper.Output)); + } + + [Fact] + public void LoggerProviderPrependsPrefixToEachLine() + { + var testTestOutputHelper = new TestTestOutputHelper(); + var loggerFactory = CreateTestLogger(builder => builder + .AddXunit(testTestOutputHelper)); + + var logger = loggerFactory.CreateLogger("TestCategory"); + logger.LogInformation("This is a" + Environment.NewLine + "multi-line" + Environment.NewLine + "message"); + + // The lines after the first one are indented more because the indentation was calculated based on the timestamp's actual length. + var expectedOutput = + "| [TIMESTAMP] TestCategory Information: This is a" + Environment.NewLine + + "| multi-line" + Environment.NewLine + + "| message" + Environment.NewLine; + + Assert.Equal(expectedOutput, MakeConsistent(testTestOutputHelper.Output)); + } + + [Fact] + public void LoggerProviderDoesNotThrowIfOutputHelperThrows() + { + var testTestOutputHelper = new TestTestOutputHelper(); + var loggerFactory = CreateTestLogger(builder => builder + + .AddXunit(testTestOutputHelper)); + + testTestOutputHelper.Throw = true; + + var logger = loggerFactory.CreateLogger("TestCategory"); + logger.LogInformation("This is a" + Environment.NewLine + "multi-line" + Environment.NewLine + "message"); + + Assert.Equal(0, testTestOutputHelper.Output.Length); + } + + private static readonly Regex TimestampRegex = new Regex(@"\d+-\d+-\d+T\d+:\d+:\d+"); + + private string MakeConsistent(string input) => TimestampRegex.Replace(input, "TIMESTAMP"); + + private static ILoggerFactory CreateTestLogger(Action configure) + { + return new ServiceCollection() + .AddLogging(configure) + .BuildServiceProvider() + .GetRequiredService(); + } + } +} diff --git a/src/Tools/.vsconfig b/src/Tools/.vsconfig new file mode 100644 index 0000000000..7a520fe61c --- /dev/null +++ b/src/Tools/.vsconfig @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Net.Component.4.6.1.TargetingPack", + "Microsoft.Net.Component.4.7.2.SDK", + "Microsoft.Net.Component.4.7.2.TargetingPack", + "Microsoft.VisualStudio.Workload.ManagedDesktop", + "Microsoft.VisualStudio.Workload.NetCoreTools", + "Microsoft.VisualStudio.Workload.NetWeb", + "Microsoft.VisualStudio.Workload.VisualStudioExtension" + ] +} diff --git a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs index d5fa7c9f50..6e25f0ccd6 100644 --- a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs +++ b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs @@ -424,7 +424,7 @@ namespace Microsoft.DotNet.OpenApi.Add.Tests { var project = CreateBasicProject(withOpenApi: false); - var app = GetApplication(realHttp: true); + var app = GetApplication(); var url = BrokenUrl; var run = app.Execute(new[] { "add", "url", url }); @@ -446,34 +446,5 @@ namespace Microsoft.DotNet.OpenApi.Add.Tests var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName); Assert.False(File.Exists(jsonFile)); } - - [Fact] - public void OpenApi_Add_URL_ActualResponse() - { - var project = CreateBasicProject(withOpenApi: false); - - var app = GetApplication(realHttp: true); - var url = ActualUrl; - var run = app.Execute(new[] { "add", "url", url }); - - AssertNoErrors(run); - - app = GetApplication(realHttp: true); - run = app.Execute(new[] { "add", "url", url }); - - AssertNoErrors(run); - - // csproj contents - var csproj = new FileInfo(project.Project.Path); - using var csprojStream = csproj.OpenRead(); - using var reader = new StreamReader(csprojStream); - var content = reader.ReadToEnd(); - var escapedPkgRef = Regex.Escape("", content); - } } } diff --git a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs index fb228eeb96..b8bf1cb8c3 100644 --- a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs +++ b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs @@ -106,7 +106,8 @@ namespace Microsoft.DotNet.OpenApi.Tests { PackageUrl, Tuple.Create(PackageUrlContent, null) }, { NoDispositionUrl, Tuple.Create(Content, null) }, { NoExtensionUrl, Tuple.Create(Content, noExtension) }, - { NoSegmentUrl, Tuple.Create(Content, justAttachments) } + { NoSegmentUrl, Tuple.Create(Content, justAttachments) }, + { BrokenUrl, null } }; } @@ -139,10 +140,14 @@ namespace Microsoft.DotNet.OpenApi.Tests public Task GetResponseAsync(string url) { var result = _results[url]; - byte[] byteArray = Encoding.ASCII.GetBytes(result.Item1); - var stream = new MemoryStream(byteArray); + MemoryStream stream = null; + if(result != null) + { + byte[] byteArray = Encoding.ASCII.GetBytes(result.Item1); + stream = new MemoryStream(byteArray); + } - return Task.FromResult(new TestHttpResponseMessageWrapper(stream, result.Item2)); + return Task.FromResult(new TestHttpResponseMessageWrapper(stream, result?.Item2)); } } @@ -154,7 +159,17 @@ namespace Microsoft.DotNet.OpenApi.Tests public bool IsSuccessCode() { - return true; + switch(StatusCode) + { + case HttpStatusCode.OK: + case HttpStatusCode.Created: + case HttpStatusCode.NoContent: + case HttpStatusCode.Accepted: + return true; + case HttpStatusCode.NotFound: + default: + return false; + } } private readonly ContentDispositionHeaderValue _contentDisposition; @@ -164,6 +179,10 @@ namespace Microsoft.DotNet.OpenApi.Tests ContentDispositionHeaderValue header) { Stream = Task.FromResult(stream); + if (header is null && stream is null) + { + StatusCode = HttpStatusCode.NotFound; + } _contentDisposition = header; } diff --git a/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs b/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs index 5b8b038596..cbbcee8233 100644 --- a/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs +++ b/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs @@ -76,7 +76,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal var projectPath = ResolveProjectPath(ProjectPath, WorkingDirectory); // Load the project file as XML - var projectDocument = XDocument.Load(projectPath); + var projectDocument = XDocument.Load(projectPath, LoadOptions.PreserveWhitespace); // Accept the `--id` CLI option to the main app string newSecretsId = string.IsNullOrWhiteSpace(OverrideId) @@ -120,19 +120,18 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal } // Add UserSecretsId element + propertyGroup.Add(" "); propertyGroup.Add(new XElement("UserSecretsId", newSecretsId)); + propertyGroup.Add($"{Environment.NewLine} "); } var settings = new XmlWriterSettings { - Indent = true, OmitXmlDeclaration = true, }; - using (var xw = XmlWriter.Create(projectPath, settings)) - { - projectDocument.Save(xw); - } + using var xw = XmlWriter.Create(projectPath, settings); + projectDocument.Save(xw); context.Reporter.Output(Resources.FormatMessage_SetUserSecretsIdForProject(newSecretsId, projectPath)); } diff --git a/src/Tools/dotnet-user-secrets/test/InitCommandTest.cs b/src/Tools/dotnet-user-secrets/test/InitCommandTest.cs index d299c208ca..acfda466b7 100644 --- a/src/Tools/dotnet-user-secrets/test/InitCommandTest.cs +++ b/src/Tools/dotnet-user-secrets/test/InitCommandTest.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Linq; using System.Text; using System.Xml.Linq; using Microsoft.AspNetCore.Testing; @@ -72,6 +73,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests } [Fact] + [QuarantinedTest] public void DoesNotGenerateIdForProjectWithSecretId() { const string SecretId = "AlreadyExists"; @@ -97,6 +99,22 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests Assert.Null(projectDocument.Declaration); } + [Fact] + public void DoesNotRemoveBlankLines() + { + var projectDir = _fixture.CreateProject(null); + var projectFile = Path.Combine(projectDir, "TestProject.csproj"); + var projectDocumentWithoutSecret = XDocument.Load(projectFile, LoadOptions.PreserveWhitespace); + var lineCountWithoutSecret = projectDocumentWithoutSecret.ToString().Split(Environment.NewLine).Length; + + new InitCommand(null, null).Execute(MakeCommandContext(), projectDir); + + var projectDocumentWithSecret = XDocument.Load(projectFile, LoadOptions.PreserveWhitespace); + var lineCountWithSecret = projectDocumentWithSecret.ToString().Split(Environment.NewLine).Length; + + Assert.True(lineCountWithSecret == lineCountWithoutSecret + 1); + } + [Fact] public void OverridesIdForProjectWithSecretId() { diff --git a/src/Tools/dotnet-watch/test/ProgramTests.cs b/src/Tools/dotnet-watch/test/ProgramTests.cs index 40c2af5214..478fc6c846 100644 --- a/src/Tools/dotnet-watch/test/ProgramTests.cs +++ b/src/Tools/dotnet-watch/test/ProgramTests.cs @@ -24,6 +24,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests } [Fact] + [QuarantinedTest] public async Task ConsoleCancelKey() { _tempDir