diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml index 56ba9bcad7..16cad09b0f 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) 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/CONTRIBUTING.md b/CONTRIBUTING.md index 238c74186d..a125150797 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,4 +57,4 @@ Your pull request will now go through extensive checks by the subject matter exp ## 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.md](./CODE-OF-CONDUCT.md) diff --git a/Directory.Build.props b/Directory.Build.props index c40d203441..6e658ac596 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,7 +13,7 @@ true $(MSBuildProjectName)-ref - true + true true false true @@ -105,8 +105,18 @@ $(RuntimeInstallerBaseName)-internal - - + + @@ -179,7 +189,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..acbfb7e1c9 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -29,7 +29,6 @@ and are generated based on the last package release. - @@ -105,7 +104,6 @@ and are generated based on the last package release. - @@ -120,13 +118,11 @@ 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/SharedFramework.External.props b/eng/SharedFramework.External.props index 9812152edd..59987b0a2c 100644 --- a/eng/SharedFramework.External.props +++ b/eng/SharedFramework.External.props @@ -18,9 +18,8 @@ $(SystemWindowsExtensionsPackageVersion.Split('.')[0]).$(SystemWindowsExtensionsPackageVersion.Split('.')[1]).0 - - + @@ -54,8 +53,6 @@ - - diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index e507920463..39a62cd7ee 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -13,320 +13,292 @@ https://github.com/dotnet/blazor dd7fb4d3931d556458f62642c2edfc59f6295bfb - + https://github.com/dotnet/aspnetcore-tooling - 8587529296124ad1d7e2a9b75f536e2a2c301e48 + 4ec71cb57e45db101bbd4ffcf64dafa1711de0af - + https://github.com/dotnet/aspnetcore-tooling - 8587529296124ad1d7e2a9b75f536e2a2c301e48 + 4ec71cb57e45db101bbd4ffcf64dafa1711de0af - + https://github.com/dotnet/aspnetcore-tooling - 8587529296124ad1d7e2a9b75f536e2a2c301e48 + 4ec71cb57e45db101bbd4ffcf64dafa1711de0af - + https://github.com/dotnet/aspnetcore-tooling - 8587529296124ad1d7e2a9b75f536e2a2c301e48 + 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 - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 - - + 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 + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 + + + https://github.com/dotnet/runtime + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 + e1fa5d7648d46f067e265211fc2c695d409fe788 - + https://github.com/dotnet/runtime - 2b487f31064fe07d3b3398a7432edd1fa5777796 - - - https://github.com/dotnet/extensions - bdc30606a08a10db8fb6909b75e9ebf6f9f482d4 + e1fa5d7648d46f067e265211fc2c695d409fe788 https://github.com/dotnet/arcade @@ -340,9 +312,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 c499646ad1..b1190c5562 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -9,7 +9,7 @@ 5 0 0 - 3 + 4 @@ -64,92 +64,83 @@ 5.0.0-beta.20180.5 - 3.6.0-3.20177.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.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 + 3.6.0-3.20201.6 + + 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.4.20201.1 + 5.0.0-preview.4.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 5.0.0-preview.4-runtime.20201.1 + 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.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.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 5.0.0-preview.3.20204.4 - 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.20205.2 - 5.0.0-preview.3.20205.2 - 5.0.0-preview.3.20205.2 - 5.0.0-preview.3.20205.2 + 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 + 5.0.0-preview.4.20180.4 3.0.0-build-20190530.3 1.0.0-beta-64023-03 diff --git a/eng/helix/content/RunTests/Directory.Build.props b/eng/helix/content/RunTests/Directory.Build.props new file mode 100644 index 0000000000..c1df2220dd --- /dev/null +++ b/eng/helix/content/RunTests/Directory.Build.props @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/eng/helix/content/RunTests/Directory.Build.targets b/eng/helix/content/RunTests/Directory.Build.targets new file mode 100644 index 0000000000..c1df2220dd --- /dev/null +++ b/eng/helix/content/RunTests/Directory.Build.targets @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/eng/helix/content/RunTests/ProcessResult.cs b/eng/helix/content/RunTests/ProcessResult.cs new file mode 100644 index 0000000000..9e293c02c8 --- /dev/null +++ b/eng/helix/content/RunTests/ProcessResult.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. + +namespace RunTests +{ + public class ProcessResult + { + public ProcessResult(string standardOutput, string standardError, int exitCode) + { + StandardOutput = standardOutput; + StandardError = standardError; + ExitCode = exitCode; + } + + public string StandardOutput { get; } + public string StandardError { get; } + public int ExitCode { get; } + } +} diff --git a/eng/helix/content/RunTests/ProcessUtil.cs b/eng/helix/content/RunTests/ProcessUtil.cs new file mode 100644 index 0000000000..8c25f9fb5d --- /dev/null +++ b/eng/helix/content/RunTests/ProcessUtil.cs @@ -0,0 +1,158 @@ +// 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.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace RunTests +{ + public static class ProcessUtil + { + [DllImport("libc", SetLastError = true, EntryPoint = "kill")] + private static extern int sys_kill(int pid, int sig); + + public static async Task RunAsync( + string filename, + string arguments, + string? workingDirectory = null, + bool throwOnError = true, + IDictionary? environmentVariables = null, + Action? outputDataReceived = null, + Action? errorDataReceived = null, + Action? onStart = null, + CancellationToken cancellationToken = default) + { + Console.WriteLine($"Running '{filename} {arguments}'"); + using var process = new Process() + { + StartInfo = + { + FileName = filename, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }, + EnableRaisingEvents = true + }; + + + if (workingDirectory != null) + { + process.StartInfo.WorkingDirectory = workingDirectory; + } + + if (environmentVariables != null) + { + foreach (var kvp in environmentVariables) + { + process.StartInfo.Environment.Add(kvp); + } + } + + var outputBuilder = new StringBuilder(); + process.OutputDataReceived += (_, e) => + { + if (e.Data != null) + { + if (outputDataReceived != null) + { + outputDataReceived.Invoke(e.Data); + } + else + { + outputBuilder.AppendLine(e.Data); + } + } + }; + + var errorBuilder = new StringBuilder(); + process.ErrorDataReceived += (_, e) => + { + if (e.Data != null) + { + if (errorDataReceived != null) + { + errorDataReceived.Invoke(e.Data); + } + else + { + errorBuilder.AppendLine(e.Data); + } + } + }; + + var processLifetimeTask = new TaskCompletionSource(); + + process.Exited += (_, e) => + { + Console.WriteLine($"'{process.StartInfo.FileName} {process.StartInfo.Arguments}' completed with exit code '{process.ExitCode}'"); + if (throwOnError && process.ExitCode != 0) + { + processLifetimeTask.TrySetException(new InvalidOperationException($"Command {filename} {arguments} returned exit code {process.ExitCode}")); + } + else + { + processLifetimeTask.TrySetResult(new ProcessResult(outputBuilder.ToString(), errorBuilder.ToString(), process.ExitCode)); + } + }; + + process.Start(); + onStart?.Invoke(process.Id); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + var cancelledTcs = new TaskCompletionSource(); + await using var _ = cancellationToken.Register(() => cancelledTcs.TrySetResult(null)); + + var result = await Task.WhenAny(processLifetimeTask.Task, cancelledTcs.Task); + + if (result == cancelledTcs.Task) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + sys_kill(process.Id, sig: 2); // SIGINT + + var cancel = new CancellationTokenSource(); + + await Task.WhenAny(processLifetimeTask.Task, Task.Delay(TimeSpan.FromSeconds(5), cancel.Token)); + + cancel.Cancel(); + } + + if (!process.HasExited) + { + process.CloseMainWindow(); + + if (!process.HasExited) + { + process.Kill(); + } + } + } + + return await processLifetimeTask.Task; + } + + public static void KillProcess(int pid) + { + try + { + using var process = Process.GetProcessById(pid); + process?.Kill(); + } + catch (ArgumentException) { } + catch (InvalidOperationException) { } + } + } +} diff --git a/eng/helix/content/RunTests/Program.cs b/eng/helix/content/RunTests/Program.cs new file mode 100644 index 0000000000..f9d3ec2752 --- /dev/null +++ b/eng/helix/content/RunTests/Program.cs @@ -0,0 +1,54 @@ +// 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.CommandLine; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace RunTests +{ + class Program + { + static async Task Main(string[] args) + { + try + { + var runner = new TestRunner(RunTestsOptions.Parse(args)); + + var keepGoing = runner.SetupEnvironment(); + if (keepGoing) + { + keepGoing = await runner.InstallAspNetAppIfNeededAsync(); + } + + runner.DisplayContents(); + + if (keepGoing) + { + if (!await runner.CheckTestDiscoveryAsync()) + { + Console.WriteLine("RunTest stopping due to test discovery failure."); + Environment.Exit(1); + return; + } + + var exitCode = await runner.RunTestsAsync(); + runner.UploadResults(); + Console.WriteLine($"Completed Helix job with exit code '{exitCode}'"); + Environment.Exit(exitCode); + } + + Console.WriteLine("Tests were not run due to previous failures. Exit code=1"); + Environment.Exit(1); + } + catch (Exception e) + { + Console.WriteLine($"RunTests uncaught exception: {e.ToString()}"); + Environment.Exit(1); + } + } + } +} diff --git a/eng/helix/content/RunTests/RunTests.csproj b/eng/helix/content/RunTests/RunTests.csproj new file mode 100644 index 0000000000..39f671c641 --- /dev/null +++ b/eng/helix/content/RunTests/RunTests.csproj @@ -0,0 +1,11 @@ + + + + Exe + netcoreapp5.0 + + + + + + diff --git a/eng/helix/content/RunTests/RunTestsOptions.cs b/eng/helix/content/RunTests/RunTestsOptions.cs new file mode 100644 index 0000000000..fcfdc84e42 --- /dev/null +++ b/eng/helix/content/RunTests/RunTestsOptions.cs @@ -0,0 +1,81 @@ +// 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.CommandLine; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace RunTests +{ + public class RunTestsOptions + { + public static RunTestsOptions Parse(string[] args) + { + var command = new RootCommand() + { + new Option( + aliases: new string[] { "--target", "-t" }, + description: "The test dll to run") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--sdk" }, + description: "The version of the sdk being used") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--runtime" }, + description: "The version of the runtime being used") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--queue" }, + description: "The name of the Helix queue being run on") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--arch" }, + description: "The architecture being run on") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--quarantined" }, + description: "Whether quarantined tests should run or not") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--ef" }, + description: "The version of the EF tool to use") + { Argument = new Argument(), Required = true }, + }; + + var parseResult = command.Parse(args); + var options = new RunTestsOptions(); + options.Target = parseResult.ValueForOption("--target"); + options.SdkVersion = parseResult.ValueForOption("--sdk"); + options.RuntimeVersion = parseResult.ValueForOption("--runtime"); + options.HelixQueue = parseResult.ValueForOption("--queue"); + options.Architecture = parseResult.ValueForOption("--arch"); + options.Quarantined = parseResult.ValueForOption("--quarantined"); + options.EfVersion = parseResult.ValueForOption("--ef"); + options.HELIX_WORKITEM_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"); + options.Path = Environment.GetEnvironmentVariable("PATH"); + options.DotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT"); + return options; + } + + public string Target { get; set;} + public string SdkVersion { get; set;} + public string RuntimeVersion { get; set;} + public string HelixQueue { get; set;} + public string Architecture { get; set;} + public bool Quarantined { get; set;} + public string EfVersion { get; set;} + public string HELIX_WORKITEM_ROOT { get; set;} + public string DotnetRoot { get; set; } + public string Path { get; set; } + } +} diff --git a/eng/helix/content/RunTests/TestRunner.cs b/eng/helix/content/RunTests/TestRunner.cs new file mode 100644 index 0000000000..850d0b629d --- /dev/null +++ b/eng/helix/content/RunTests/TestRunner.cs @@ -0,0 +1,251 @@ +// 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.CommandLine; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace RunTests +{ + public class TestRunner + { + public TestRunner(RunTestsOptions options) + { + Options = options; + EnvironmentVariables = new Dictionary(); + } + + public RunTestsOptions Options { get; set; } + public Dictionary EnvironmentVariables { get; set; } + + public bool SetupEnvironment() + { + try + { + // Rename default.NuGet.config to NuGet.config if there is not a custom one from the project + // We use a local NuGet.config file to avoid polluting global machine state and avoid relying on global machine state + if (!File.Exists("NuGet.config")) + { + File.Copy("default.NuGet.config", "NuGet.config"); + } + + EnvironmentVariables.Add("PATH", Options.Path); + EnvironmentVariables.Add("DOTNET_ROOT", Options.DotnetRoot); + EnvironmentVariables.Add("helix", Options.HelixQueue); + + Console.WriteLine($"Current Directory: {Options.HELIX_WORKITEM_ROOT}"); + var helixDir = Options.HELIX_WORKITEM_ROOT; + Console.WriteLine($"Setting HELIX_DIR: {helixDir}"); + EnvironmentVariables.Add("HELIX_DIR", helixDir); + EnvironmentVariables.Add("NUGET_FALLBACK_PACKAGES", helixDir); + var nugetRestore = Path.Combine(helixDir, "nugetRestore"); + EnvironmentVariables.Add("NUGET_RESTORE", nugetRestore); + var dotnetEFFullPath = Path.Combine(nugetRestore, $"dotnet-ef/{Options.EfVersion}/tools/netcoreapp3.1/any/dotnet-ef.exe"); + Console.WriteLine($"Set DotNetEfFullPath: {dotnetEFFullPath}"); + EnvironmentVariables.Add("DotNetEfFullPath", dotnetEFFullPath); + + Console.WriteLine($"Creating nuget restore directory: {nugetRestore}"); + Directory.CreateDirectory(nugetRestore); + + // Rename default.runner.json to xunit.runner.json if there is not a custom one from the project + if (!File.Exists("xunit.runner.json")) + { + File.Copy("default.runner.json", "xunit.runner.json"); + } + + return true; + } + catch (Exception e) + { + Console.WriteLine($"Exception in SetupEnvironment: {e.ToString()}"); + return false; + } + } + + public void DisplayContents() + { + try + { + Console.WriteLine(); + Console.WriteLine("Displaying directory contents:"); + foreach (var file in Directory.EnumerateFiles("./")) + { + Console.WriteLine(Path.GetFileName(file)); + } + foreach (var file in Directory.EnumerateDirectories("./")) + { + Console.WriteLine(Path.GetFileName(file)); + } + Console.WriteLine(); + } + catch (Exception e) + { + Console.WriteLine($"Exception in DisplayInitialState: {e.ToString()}"); + } + } + + public async Task InstallAspNetAppIfNeededAsync() + { + try + { + Console.WriteLine("Checking for Microsoft.AspNetCore.App/"); + if (Directory.Exists("Microsoft.AspNetCore.App")) + { + var appRuntimePath = $"{Options.DotnetRoot}/shared/Microsoft.AspNetCore.App/{Options.RuntimeVersion}"; + Console.WriteLine($"Found Microsoft.AspNetCore.App/, copying to {appRuntimePath}"); + foreach (var file in Directory.EnumerateFiles("Microsoft.AspNetCore.App", "*.*", SearchOption.AllDirectories)) + { + File.Copy(file, Path.Combine(appRuntimePath, file), overwrite: true); + } + + Console.WriteLine($"Adding current directory to nuget sources: {Options.HELIX_WORKITEM_ROOT}"); + + await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + $"nuget add source {Options.HELIX_WORKITEM_ROOT} --configfile NuGet.config", + environmentVariables: EnvironmentVariables); + + await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + "nuget add source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json --configfile NuGet.config", + environmentVariables: EnvironmentVariables); + + // Write nuget sources to console, useful for debugging purposes + await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + "nuget list source", + environmentVariables: EnvironmentVariables, + outputDataReceived: Console.WriteLine, + errorDataReceived: Console.WriteLine); + + await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + $"tool install dotnet-ef --global --version {Options.EfVersion}", + environmentVariables: EnvironmentVariables); + + // ';' is the path separator on Windows, and ':' on Unix + Options.Path += RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":"; + Options.Path += $"{Environment.GetEnvironmentVariable("DOTNET_CLI_HOME")}/.dotnet/tools"; + EnvironmentVariables["PATH"] = Options.Path; + } + else + { + Console.WriteLine($"No app runtime found, skipping..."); + } + return true; + } + catch (Exception e) + { + Console.WriteLine($"Exception in InstallAspNetAppIfNeeded: {e.ToString()}"); + return false; + } + } + + public async Task CheckTestDiscoveryAsync() + { + try + { + // Run test discovery so we know if there are tests to run + var discoveryResult = await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + $"vstest {Options.Target} -lt", + environmentVariables: EnvironmentVariables); + + if (discoveryResult.StandardOutput.Contains("Exception thrown")) + { + Console.WriteLine("Exception thrown during test discovery."); + Console.WriteLine(discoveryResult.StandardOutput); + return false; + } + return true; + } + catch (Exception e) + { + Console.WriteLine($"Exception in CheckTestDiscovery: {e.ToString()}"); + return false; + } + } + + public async Task RunTestsAsync() + { + var exitCode = 0; + try + { + var commonTestArgs = $"vstest {Options.Target} --logger:xunit --logger:\"console;verbosity=normal\" --blame"; + if (Options.Quarantined) + { + Console.WriteLine("Running quarantined tests."); + + // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md + var result = await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + commonTestArgs + " --TestCaseFilter:\"Quarantined=true\"", + environmentVariables: EnvironmentVariables, + outputDataReceived: Console.WriteLine, + errorDataReceived: Console.WriteLine, + throwOnError: false); + + if (result.ExitCode != 0) + { + Console.WriteLine($"Failure in quarantined tests. Exit code: {result.ExitCode}."); + } + } + else + { + Console.WriteLine("Running non-quarantined tests."); + + // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md + var result = await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + commonTestArgs + " --TestCaseFilter:\"Quarantined!=true\"", + environmentVariables: EnvironmentVariables, + outputDataReceived: Console.WriteLine, + errorDataReceived: Console.Error.WriteLine, + throwOnError: false); + + if (result.ExitCode != 0) + { + Console.WriteLine($"Failure in non-quarantined tests. Exit code: {result.ExitCode}."); + exitCode = result.ExitCode; + } + } + } + catch (Exception e) + { + Console.WriteLine($"Exception in RunTests: {e.ToString()}"); + exitCode = 1; + } + return exitCode; + } + + public void UploadResults() + { + // 'testResults.xml' is the file Helix looks for when processing test results + Console.WriteLine("Trying to upload results..."); + if (File.Exists("TestResults/TestResults.xml")) + { + Console.WriteLine("Copying TestResults/TestResults.xml to ./testResults.xml"); + File.Copy("TestResults/TestResults.xml", "testResults.xml"); + } + else + { + Console.WriteLine("No test results found."); + } + + var HELIX_WORKITEM_UPLOAD_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"); + Console.WriteLine($"Copying artifacts/log/ to {HELIX_WORKITEM_UPLOAD_ROOT}/"); + if (Directory.Exists("artifacts/log")) + { + foreach (var file in Directory.EnumerateFiles("artifacts/log", "*.log", SearchOption.AllDirectories)) + { + // Combine the directory name + log name for the copied log file name to avoid overwriting duplicate test names in different test projects + var logName = $"{Path.GetFileName(Path.GetDirectoryName(file))}_{Path.GetFileName(file)}"; + Console.WriteLine($"Copying: {file} to {Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName)}"); + // Need to copy to HELIX_WORKITEM_UPLOAD_ROOT and HELIX_WORKITEM_UPLOAD_ROOT/../ in order for Azure Devops attachments to link properly and for Helix to store the logs + File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName)); + File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, "..", logName)); + } + } + else + { + Console.WriteLine("No logs found in artifacts/log"); + } + } + } +} diff --git a/eng/helix/content/default.NuGet.config b/eng/helix/content/default.NuGet.config new file mode 100644 index 0000000000..3a9f6b3272 --- /dev/null +++ b/eng/helix/content/default.NuGet.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/eng/helix/content/runtests.cmd b/eng/helix/content/runtests.cmd index 968ab223f9..1078def35b 100644 --- a/eng/helix/content/runtests.cmd +++ b/eng/helix/content/runtests.cmd @@ -3,13 +3,9 @@ REM Need delayed expansion !PATH! so parens in the path don't mess up the parens setlocal enabledelayedexpansion REM Use '$' as a variable name prefix to avoid MSBuild variable collisions with these variables -set $target=%1 set $sdkVersion=%2 set $runtimeVersion=%3 -set $helixQueue=%4 set $arch=%5 -set $quarantined=%6 -set $efVersion=%7 set DOTNET_HOME=%HELIX_CORRELATION_PAYLOAD%\sdk set DOTNET_ROOT=%DOTNET_HOME%\%$arch% @@ -23,84 +19,14 @@ echo "Installing SDK" powershell.exe -NoProfile -ExecutionPolicy unrestricted -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; &([scriptblock]::Create((Invoke-WebRequest -useb 'https://dot.net/v1/dotnet-install.ps1'))) -Architecture %$arch% -Version %$sdkVersion% -InstallDir %DOTNET_ROOT%" echo "Installing Runtime" powershell.exe -NoProfile -ExecutionPolicy unrestricted -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; &([scriptblock]::Create((Invoke-WebRequest -useb 'https://dot.net/v1/dotnet-install.ps1'))) -Architecture %$arch% -Runtime dotnet -Version %$runtimeVersion% -InstallDir %DOTNET_ROOT%" -echo "Checking for Microsoft.AspNetCore.App" -if EXIST ".\Microsoft.AspNetCore.App" ( - echo "Found Microsoft.AspNetCore.App, copying to %DOTNET_ROOT%\shared\Microsoft.AspNetCore.App\%runtimeVersion%" - xcopy /i /y ".\Microsoft.AspNetCore.App" %DOTNET_ROOT%\shared\Microsoft.AspNetCore.App\%runtimeVersion%\ - - echo "Adding current directory to nuget sources: %HELIX_WORKITEM_ROOT%" - dotnet nuget add source %HELIX_WORKITEM_ROOT% - dotnet nuget add source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json - dotnet nuget list source - dotnet tool install dotnet-ef --global --version %$efVersion% - - set PATH=!PATH!;%DOTNET_CLI_HOME%\.dotnet\tools -) - -echo "Current Directory: %HELIX_WORKITEM_ROOT%" -set HELIX=%$helixQueue% -set HELIX_DIR=%HELIX_WORKITEM_ROOT% -set NUGET_FALLBACK_PACKAGES=%HELIX_DIR% -set NUGET_RESTORE=%HELIX_DIR%\nugetRestore -set DotNetEfFullPath=%HELIX_DIR%\nugetRestore\dotnet-ef\%$efVersion%\tools\netcoreapp3.1\any\dotnet-ef.exe -echo "Set DotNetEfFullPath: %DotNetEfFullPath%" -echo "Setting HELIX_DIR: %HELIX_DIR%" -echo Creating nuget restore directory: %NUGET_RESTORE% -mkdir %NUGET_RESTORE% -mkdir logs - -REM "Rename default.runner.json to xunit.runner.json if there is not a custom one from the project" -if not EXIST ".\xunit.runner.json" ( - copy default.runner.json xunit.runner.json -) - -dir - -%DOTNET_ROOT%\dotnet vstest %$target% -lt >discovered.txt -find /c "Exception thrown" discovered.txt -REM "ERRORLEVEL is not %ERRORLEVEL%" https://blogs.msdn.microsoft.com/oldnewthing/20080926-00/?p=20743/ -if not errorlevel 1 ( - echo Exception thrown during test discovery. 1>&2 - type discovered.txt 1>&2 - exit /b 1 -) set exit_code=0 - -if %$quarantined%==True ( - set %$quarantined=true +echo "Restore for RunTests..." +dotnet restore RunTests\RunTests.csproj --source https://api.nuget.org/v3/index.json --ignore-failed-sources +echo "Running tests..." +dotnet run --project RunTests\RunTests.csproj -- --target %1 --sdk %2 --runtime %3 --queue %4 --arch %5 --quarantined %6 --ef %7 +if errorlevel 1 ( + set exit_code=1 ) - -REM Disable "!Foo!" expansions because they break the filter syntax -setlocal disabledelayedexpansion -set NONQUARANTINE_FILTER="Quarantined!=true" -set QUARANTINE_FILTER="Quarantined=true" -if %$quarantined%==true ( - echo Running quarantined tests. - %DOTNET_ROOT%\dotnet vstest %$target% --logger:xunit --logger:"console;verbosity=normal" --blame --TestCaseFilter:%QUARANTINE_FILTER% - if errorlevel 1 ( - echo Failure in quarantined test 1>&2 - REM DO NOT EXIT and DO NOT SET EXIT_CODE to 1 - ) -) else ( - REM Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md - echo Running non-quarantined tests. - %DOTNET_ROOT%\dotnet vstest %$target% --logger:xunit --logger:"console;verbosity=normal" --blame --TestCaseFilter:%NONQUARANTINE_FILTER% - if errorlevel 1 ( - echo Failure in non-quarantined test 1>&2 - set exit_code=1 - REM DO NOT EXIT - ) -) - -echo "Copying TestResults\TestResults.xml to ." -copy TestResults\TestResults.xml testResults.xml -echo "Copying artifacts/logs to %HELIX_WORKITEM_UPLOAD_ROOT%\..\" -for /R artifacts/log %%f in (*.log) do ( - echo "Copying: %%f" - copy "%%f" %HELIX_WORKITEM_UPLOAD_ROOT%\..\ - copy "%%f" %HELIX_WORKITEM_UPLOAD_ROOT%\ -) - +echo "Finished running tests: exit_code=%exit_code%" exit /b %exit_code% - diff --git a/eng/helix/content/runtests.sh b/eng/helix/content/runtests.sh index c8c84f1c33..2ce725910c 100755 --- a/eng/helix/content/runtests.sh +++ b/eng/helix/content/runtests.sh @@ -1,12 +1,7 @@ #!/usr/bin/env bash -test_binary_path="$1" dotnet_sdk_version="$2" dotnet_runtime_version="$3" -helix_queue_name="$4" -target_arch="$5" -quarantined="$6" -efVersion="$7" RESET="\033[0m" RED="\033[0;31m" @@ -30,19 +25,6 @@ export DOTNET_CLI_HOME="$DIR/.home$RANDOM" export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 -# Used by SkipOnHelix attribute -export helix="$helix_queue_name" -export HELIX_DIR="$DIR" -export NUGET_FALLBACK_PACKAGES="$DIR" -export DotNetEfFullPath=$DIR\nugetRestore\dotnet-ef\$efVersion\tools\netcoreapp3.1\any\dotnet-ef.dll -echo "Set DotNetEfFullPath: $DotNetEfFullPath" -export NUGET_RESTORE="$DIR/nugetRestore" -echo "Creating nugetRestore directory: $NUGET_RESTORE" -mkdir $NUGET_RESTORE -mkdir logs - -ls -laR - RESET="\033[0m" RED="\033[0;31m" YELLOW="\033[0;33m" @@ -93,29 +75,6 @@ if [ $? -ne 0 ]; then done fi -# Copy over any local shared fx if found -if [ -d "Microsoft.AspNetCore.App" ] -then - echo "Found Microsoft.AspNetCore.App directory, copying to $DOTNET_ROOT/shared/Microsoft.AspNetCore.App/$dotnet_runtime_version." - cp -r Microsoft.AspNetCore.App $DOTNET_ROOT/shared/Microsoft.AspNetCore.App/$dotnet_runtime_version - - echo "Adding current directory to nuget sources: $DIR" - dotnet nuget add source $DIR - dotnet nuget add source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json - dotnet nuget list source - - dotnet tool install dotnet-ef --global --version $efVersion - - # Ensure tools are on on PATH - export PATH="$PATH:$DOTNET_CLI_HOME/.dotnet/tools" -fi - -# Rename default.runner.json to xunit.runner.json if there is not a custom one from the project -if [ ! -f "xunit.runner.json" ] -then - cp default.runner.json xunit.runner.json -fi - if [ -e /proc/self/coredump_filter ]; then # Include memory in private and shared file-backed mappings in the dump. # This ensures that we can see disassembly from our shared libraries when @@ -123,40 +82,14 @@ if [ -e /proc/self/coredump_filter ]; then echo -n 0x3F > /proc/self/coredump_filter fi +# dontet-install.sh seems to affect the Linux filesystem and causes test flakiness unless we sync the filesystem before running tests sync -$DOTNET_ROOT/dotnet vstest $test_binary_path -lt >discovered.txt -if grep -q "Exception thrown" discovered.txt; then - echo -e "${RED}Exception thrown during test discovery${RESET}". - cat discovered.txt - exit 1 -fi - exit_code=0 - -# Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md -NONQUARANTINE_FILTER="Quarantined!=true" -QUARANTINE_FILTER="Quarantined=true" -if [ "$quarantined" == true ]; then - echo "Running all tests including quarantined." - $DOTNET_ROOT/dotnet vstest $test_binary_path --logger:xunit --logger:"console;verbosity=normal" --blame --TestCaseFilter:"$QUARANTINE_FILTER" - if [ $? != 0 ]; then - echo "Quarantined tests failed!" 1>&2 - # DO NOT EXIT - fi -else - echo "Running non-quarantined tests." - $DOTNET_ROOT/dotnet vstest $test_binary_path --logger:xunit --logger:"console;verbosity=normal" --blame --TestCaseFilter:"$NONQUARANTINE_FILTER" - exit_code=$? - if [ $exit_code != 0 ]; then - echo "Non-quarantined tests failed!" 1>&2 - # DO NOT EXIT - fi -fi - -echo "Copying TestResults/TestResults to ." -cp TestResults/TestResults.xml testResults.xml -echo "Copying artifacts/logs to $HELIX_WORKITEM_UPLOAD_ROOT/" -cp `find . -name \*.log` $HELIX_WORKITEM_UPLOAD_ROOT/../ -cp `find . -name \*.log` $HELIX_WORKITEM_UPLOAD_ROOT/ +echo "Restore for RunTests..." +$DOTNET_ROOT/dotnet restore RunTests/RunTests.csproj --source https://api.nuget.org/v3/index.json --ignore-failed-sources +echo "Running tests..." +$DOTNET_ROOT/dotnet run --project RunTests/RunTests.csproj -- --target $1 --sdk $2 --runtime $3 --queue $4 --arch $5 --quarantined $6 --ef $7 +exit_code = $? +echo "Finished tests...exit_code=$exit_code" exit $exit_code diff --git a/eng/scripts/KillProcesses.ps1 b/eng/scripts/KillProcesses.ps1 index 153234abd9..43d7c5ed92 100644 --- a/eng/scripts/KillProcesses.ps1 +++ b/eng/scripts/KillProcesses.ps1 @@ -38,6 +38,15 @@ function _killSeleniumTrackedProcesses() { } } +function _listProcesses($processName) { + $processes = Get-WmiObject win32_process -Filter "name like '%$processName'" -ErrorAction SilentlyContinue; + if ($processes) { + Write-Host "These processes will be killed..." + $processes | select commandline | Out-String -Width 800 + } +} + +_listProcesses dotnet _kill dotnet.exe _kill testhost.exe _kill iisexpress.exe diff --git a/eng/scripts/KillProcesses.sh b/eng/scripts/KillProcesses.sh index f52511739b..e491dc1693 100755 --- a/eng/scripts/KillProcesses.sh +++ b/eng/scripts/KillProcesses.sh @@ -1,4 +1,12 @@ #!/usr/bin/env bash +# list processes that would be killed so they appear in the log +p=$(pgrep dotnet) +if [ $? -eq 0 ] +then + echo "These processes will be killed..." + ps -p $p +fi + pkill dotnet || true exit 0 diff --git a/eng/scripts/vs.buildtools.json b/eng/scripts/vs.buildtools.json index 173ce44d40..94448dd6a0 100644 --- a/eng/scripts/vs.buildtools.json +++ b/eng/scripts/vs.buildtools.json @@ -7,11 +7,8 @@ ], "add": [ "Microsoft.Net.Component.4.6.1.TargetingPack", - "Microsoft.Net.Component.4.6.2.TargetingPack", - "Microsoft.Net.Component.4.7.1.TargetingPack", "Microsoft.Net.Component.4.7.2.SDK", "Microsoft.Net.Component.4.7.2.TargetingPack", - "Microsoft.Net.Component.4.7.TargetingPack", "Microsoft.VisualStudio.Component.FSharp.MSBuild", "Microsoft.VisualStudio.Component.NuGet", "Microsoft.VisualStudio.Component.NuGet.BuildTools", diff --git a/eng/scripts/vs.buildtools.preview.json b/eng/scripts/vs.buildtools.preview.json index 404fe49865..51888e2500 100644 --- a/eng/scripts/vs.buildtools.preview.json +++ b/eng/scripts/vs.buildtools.preview.json @@ -7,11 +7,8 @@ ], "add": [ "Microsoft.Net.Component.4.6.1.TargetingPack", - "Microsoft.Net.Component.4.6.2.TargetingPack", - "Microsoft.Net.Component.4.7.1.TargetingPack", "Microsoft.Net.Component.4.7.2.SDK", "Microsoft.Net.Component.4.7.2.TargetingPack", - "Microsoft.Net.Component.4.7.TargetingPack", "Microsoft.VisualStudio.Component.FSharp.MSBuild", "Microsoft.VisualStudio.Component.NuGet", "Microsoft.VisualStudio.Component.NuGet.BuildTools", diff --git a/eng/scripts/vs.json b/eng/scripts/vs.json index d5aff9345a..213fa26edf 100644 --- a/eng/scripts/vs.json +++ b/eng/scripts/vs.json @@ -7,12 +7,8 @@ ], "add": [ "Microsoft.Net.Component.4.6.1.TargetingPack", - "Microsoft.Net.Component.4.6.2.TargetingPack", - "Microsoft.Net.Component.4.7.1.TargetingPack", "Microsoft.Net.Component.4.7.2.SDK", "Microsoft.Net.Component.4.7.2.TargetingPack", - "Microsoft.Net.Component.4.7.TargetingPack", - "Microsoft.VisualStudio.Component.Azure.Storage.Emulator", "Microsoft.VisualStudio.Component.VC.ATL", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "Microsoft.VisualStudio.Component.Windows10SDK.17134", diff --git a/eng/scripts/vs.preview.json b/eng/scripts/vs.preview.json index 079ad4b641..0a6a998f36 100644 --- a/eng/scripts/vs.preview.json +++ b/eng/scripts/vs.preview.json @@ -7,12 +7,8 @@ ], "add": [ "Microsoft.Net.Component.4.6.1.TargetingPack", - "Microsoft.Net.Component.4.6.2.TargetingPack", - "Microsoft.Net.Component.4.7.1.TargetingPack", "Microsoft.Net.Component.4.7.2.SDK", "Microsoft.Net.Component.4.7.2.TargetingPack", - "Microsoft.Net.Component.4.7.TargetingPack", - "Microsoft.VisualStudio.Component.Azure.Storage.Emulator", "Microsoft.VisualStudio.Component.VC.ATL", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "Microsoft.VisualStudio.Component.Windows10SDK.17134", diff --git a/eng/targets/Helix.Common.props b/eng/targets/Helix.Common.props index e77bcd8828..a5ec01a94f 100644 --- a/eng/targets/Helix.Common.props +++ b/eng/targets/Helix.Common.props @@ -22,6 +22,7 @@ + diff --git a/eng/targets/ResolveReferences.targets b/eng/targets/ResolveReferences.targets index 37a17b7140..8cc5c717ec 100644 --- a/eng/targets/ResolveReferences.targets +++ b/eng/targets/ResolveReferences.targets @@ -216,16 +216,16 @@ Condition=" '$(CompileUsingReferenceAssemblies)' != false AND '$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)' ">true - + IncludeAssets="Compile" + PrivateAssets="All" + GeneratePathProperty="true" /> 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/Analyzers/Analyzers/test/Microsoft.AspNetCore.Analyzers.Test.csproj b/src/Analyzers/Analyzers/test/Microsoft.AspNetCore.Analyzers.Test.csproj index e0847bc033..ff07bc22a7 100644 --- a/src/Analyzers/Analyzers/test/Microsoft.AspNetCore.Analyzers.Test.csproj +++ b/src/Analyzers/Analyzers/test/Microsoft.AspNetCore.Analyzers.Test.csproj @@ -12,9 +12,9 @@ - + + - + false + true + true + true + + true + + + + + + + diff --git a/src/Analyzers/Internal.AspNetCore.Analyzers/src/PubternalityAnalyzer.cs b/src/Analyzers/Internal.AspNetCore.Analyzers/src/PubternalityAnalyzer.cs new file mode 100644 index 0000000000..b66aa38c2e --- /dev/null +++ b/src/Analyzers/Internal.AspNetCore.Analyzers/src/PubternalityAnalyzer.cs @@ -0,0 +1,279 @@ +// 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.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Internal.AspNetCore.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class PubternalityAnalyzer : DiagnosticAnalyzer + { + public PubternalityAnalyzer() + { + SupportedDiagnostics = ImmutableArray.Create(new[] + { + PubturnalityDescriptors.PUB0001, + PubturnalityDescriptors.PUB0002 + }); + } + + public override ImmutableArray SupportedDiagnostics { get; } + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(analysisContext => + { + analysisContext.RegisterSymbolAction(symbolAnalysisContext => AnalyzeTypeUsage(symbolAnalysisContext), SymbolKind.Namespace); + analysisContext.RegisterSyntaxNodeAction(syntaxContext => AnalyzeTypeUsage(syntaxContext), SyntaxKind.IdentifierName); + }); + } + + private void AnalyzeTypeUsage(SymbolAnalysisContext context) + { + var ns = (INamespaceSymbol)context.Symbol; + if (IsInternal(ns)) + { + return; + } + + foreach (var namespaceOrTypeSymbol in ns.GetMembers()) + { + if (namespaceOrTypeSymbol.IsType) + { + CheckType((ITypeSymbol)namespaceOrTypeSymbol, context); + } + } + } + + private void CheckType(ITypeSymbol typeSymbol, SymbolAnalysisContext context) + { + if (IsPrivate(typeSymbol) || IsPrivate(typeSymbol.ContainingType)) + { + return; + } + + if (typeSymbol.BaseType != null) + { + CheckType(context, typeSymbol.BaseType, typeSymbol.DeclaringSyntaxReferences); + } + + foreach (var member in typeSymbol.GetMembers()) + { + CheckMember(context, member); + } + + foreach (var innerType in typeSymbol.GetTypeMembers()) + { + CheckType(innerType, context); + } + + if (typeSymbol is INamedTypeSymbol namedTypeSymbol) + { + // Check delegate signatures + if (namedTypeSymbol.DelegateInvokeMethod != null) + { + CheckMethod(context, namedTypeSymbol.DelegateInvokeMethod); + } + } + } + + private void CheckMember(SymbolAnalysisContext context, ISymbol symbol) + { + if (IsPrivate(symbol)) + { + return; + } + + switch (symbol) + { + case IFieldSymbol fieldSymbol: + { + CheckType(context, fieldSymbol.Type, fieldSymbol.DeclaringSyntaxReferences); + break; + } + case IPropertySymbol propertySymbol: + { + CheckType(context, propertySymbol.Type, propertySymbol.DeclaringSyntaxReferences); + break; + } + case IMethodSymbol methodSymbol: + { + // Skip compiler generated members that we already explicitly check + switch (methodSymbol.MethodKind) + { + case MethodKind.EventAdd: + case MethodKind.EventRaise: + case MethodKind.EventRemove: + case MethodKind.PropertyGet: + case MethodKind.PropertySet: + case MethodKind.DelegateInvoke: + case MethodKind.Ordinary when methodSymbol.ContainingType.TypeKind == TypeKind.Delegate: + return; + } + + CheckMethod(context, methodSymbol); + break; + } + case IEventSymbol eventSymbol: + CheckType(context, eventSymbol.Type, eventSymbol.DeclaringSyntaxReferences); + break; + } + } + + private void CheckMethod(SymbolAnalysisContext context, IMethodSymbol methodSymbol) + { + if (IsPrivate(methodSymbol)) + { + return; + } + + foreach (var parameter in methodSymbol.Parameters) + { + CheckType(context, parameter.Type, parameter.DeclaringSyntaxReferences); + } + + CheckType(context, methodSymbol.ReturnType, methodSymbol.DeclaringSyntaxReferences); + } + + private static bool IsPrivate(ISymbol symbol) + { + return symbol != null && + (symbol.DeclaredAccessibility == Accessibility.Private || + symbol.DeclaredAccessibility == Accessibility.Internal || + IsInternal(symbol.ContainingNamespace)); + } + + private void CheckAttributes(SymbolAnalysisContext context, ImmutableArray attributes) + { + foreach (var attributeData in attributes) + { + CheckType(context, attributeData.AttributeClass, attributeData.ApplicationSyntaxReference); + } + } + + private void CheckType(SymbolAnalysisContext context, ITypeSymbol symbol, SyntaxReference syntax) + { + var pubternalType = GetPubternalType(symbol); + if (pubternalType != null) + { + ReportPUB0001(context, pubternalType, syntax); + } + } + private void CheckType(SymbolAnalysisContext context, ITypeSymbol symbol, ImmutableArray syntaxReferences) + { + var pubternalType = GetPubternalType(symbol); + if (pubternalType != null) + { + foreach (var syntaxReference in syntaxReferences) + { + ReportPUB0001(context, pubternalType, syntaxReference); + } + } + } + + private static void ReportPUB0001(SymbolAnalysisContext context, ITypeSymbol pubternalType, SyntaxReference syntax) + { + var syntaxNode = syntax.GetSyntax(); + var location = syntaxNode.GetLocation(); + + if (syntaxNode is BaseTypeDeclarationSyntax baseTypeDeclarationSyntax) + { + location = baseTypeDeclarationSyntax.Identifier.GetLocation(); + } + + if (syntaxNode is DelegateDeclarationSyntax delegateDeclarationSyntax) + { + location = delegateDeclarationSyntax.ReturnType.GetLocation(); + } + + if (syntaxNode is BasePropertyDeclarationSyntax propertyDeclaration) + { + location = propertyDeclaration.Type.GetLocation(); + } + + if (syntaxNode is MethodDeclarationSyntax method) + { + location = method.ReturnType.GetLocation(); + } + + if (syntaxNode is VariableDeclaratorSyntax variableDeclarator) + { + if (variableDeclarator.Parent is VariableDeclarationSyntax fieldDeclaration) + { + location = fieldDeclaration.Type.GetLocation(); + } + } + + context.ReportDiagnostic(Diagnostic.Create(PubturnalityDescriptors.PUB0001, location, pubternalType.ToDisplayString())); + } + + private ITypeSymbol GetPubternalType(ITypeSymbol symbol) + { + if (IsInternal(symbol.ContainingNamespace)) + { + return symbol; + } + else + { + if (symbol is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.IsGenericType) + { + foreach (var argument in namedTypeSymbol.TypeArguments) + { + var argumentSymbol = GetPubternalType(argument); + if (argumentSymbol != null) + { + return argumentSymbol; + } + } + } + } + + return null; + } + + private void AnalyzeTypeUsage(SyntaxNodeAnalysisContext syntaxContext) + { + var identifier = (IdentifierNameSyntax)syntaxContext.Node; + + var symbolInfo = ModelExtensions.GetTypeInfo(syntaxContext.SemanticModel, identifier, syntaxContext.CancellationToken); + if (symbolInfo.Type == null) + { + return; + } + + var type = symbolInfo.Type; + if (!IsInternal(type.ContainingNamespace)) + { + // don't care about non-pubternal type references + return; + } + + if (!syntaxContext.ContainingSymbol.ContainingAssembly.Equals(type.ContainingAssembly)) + { + syntaxContext.ReportDiagnostic(Diagnostic.Create(PubturnalityDescriptors.PUB0002, identifier.GetLocation(), type.ToDisplayString())); + } + } + + private static bool IsInternal(INamespaceSymbol ns) + { + while (ns != null) + { + if (ns.Name == "Internal") + { + return true; + } + + ns = ns.ContainingNamespace; + } + + return false; + } + } +} diff --git a/src/Analyzers/Internal.AspNetCore.Analyzers/src/PubturnalityDescriptors.cs b/src/Analyzers/Internal.AspNetCore.Analyzers/src/PubturnalityDescriptors.cs new file mode 100644 index 0000000000..6064ebaf34 --- /dev/null +++ b/src/Analyzers/Internal.AspNetCore.Analyzers/src/PubturnalityDescriptors.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. + +using Microsoft.CodeAnalysis; + +namespace Internal.AspNetCore.Analyzers +{ + internal class PubturnalityDescriptors + { + public static DiagnosticDescriptor PUB0001 = new DiagnosticDescriptor( + "PUB0001", + "Pubternal type in public API", + "Pubternal type ('{0}') usage in public API", + "Usage", + DiagnosticSeverity.Warning, true); + + public static DiagnosticDescriptor PUB0002 = new DiagnosticDescriptor( + "PUB0002", + "Cross assembly pubternal reference", + "Cross assembly pubternal type ('{0}') reference", + "Usage", + DiagnosticSeverity.Error, false); + } +} diff --git a/src/Analyzers/Internal.AspNetCore.Analyzers/test/Internal.AspNetCore.Analyzers.Tests.csproj b/src/Analyzers/Internal.AspNetCore.Analyzers/test/Internal.AspNetCore.Analyzers.Tests.csproj new file mode 100644 index 0000000000..99dcca9dc1 --- /dev/null +++ b/src/Analyzers/Internal.AspNetCore.Analyzers/test/Internal.AspNetCore.Analyzers.Tests.csproj @@ -0,0 +1,15 @@ + + + + $(DefaultNetCoreTargetFramework);net472 + true + + false + + + + + + + + diff --git a/src/Analyzers/Internal.AspNetCore.Analyzers/test/PubternabilityAnalyzerTests.cs b/src/Analyzers/Internal.AspNetCore.Analyzers/test/PubternabilityAnalyzerTests.cs new file mode 100644 index 0000000000..ebcd5fa1e8 --- /dev/null +++ b/src/Analyzers/Internal.AspNetCore.Analyzers/test/PubternabilityAnalyzerTests.cs @@ -0,0 +1,260 @@ +// 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 System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.AspNetCore.Analyzer.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Internal.AspNetCore.Analyzers.Tests +{ + public class PubternabilityAnalyzerTests : DiagnosticVerifier + { + + private const string InternalDefinitions = @" +namespace A.Internal.Namespace +{ + public class C {} + public delegate C CD (); + public class CAAttribute: System.Attribute {} + + public class Program + { + public static void Main() {} + } +}"; + public PubternabilityAnalyzerTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + } + + [Theory] + [MemberData(nameof(PublicMemberDefinitions))] + public async Task PublicExposureOfPubternalTypeProducesPUB0001(string member) + { + var code = GetSourceFromNamespaceDeclaration($@" +namespace A +{{ + public class T + {{ + {member} + }} +}}"); + var diagnostic = Assert.Single(await GetDiagnostics(code.Source)); + Assert.Equal("PUB0001", diagnostic.Id); + AnalyzerAssert.DiagnosticLocation(code.DefaultMarkerLocation, diagnostic.Location); + } + + [Theory] + [MemberData(nameof(PublicMemberWithAllowedDefinitions))] + public async Task PublicExposureOfPubternalMembersSometimesAllowed(string member) + { + var code = GetSourceFromNamespaceDeclaration($@" +namespace A +{{ + public class T + {{ + {member} + }} +}}"); + Assert.Empty(await GetDiagnostics(code.Source)); + } + + + [Theory] + [MemberData(nameof(PublicTypeDefinitions))] + public async Task PublicExposureOfPubternalTypeProducesInTypeDefinitionPUB0001(string member) + { + var code = GetSourceFromNamespaceDeclaration($@" +namespace A +{{ + {member} +}}"); + var diagnostic = Assert.Single(await GetDiagnostics(code.Source)); + Assert.Equal("PUB0001", diagnostic.Id); + AnalyzerAssert.DiagnosticLocation(code.DefaultMarkerLocation, diagnostic.Location); + } + + [Theory] + [MemberData(nameof(PublicMemberDefinitions))] + public async Task PrivateUsageOfPubternalTypeDoesNotProduce(string member) + { + var code = GetSourceFromNamespaceDeclaration($@" +namespace A +{{ + internal class T + {{ + {member} + }} +}}"); + var diagnostics = await GetDiagnostics(code.Source); + Assert.Empty(diagnostics); + } + + [Theory] + [MemberData(nameof(PrivateMemberDefinitions))] + public async Task PrivateUsageOfPubternalTypeDoesNotProduceInPublicClasses(string member) + { + var code = GetSourceFromNamespaceDeclaration($@" +namespace A +{{ + public class T + {{ + {member} + }} +}}"); + var diagnostics = await GetDiagnostics(code.Source); + Assert.Empty(diagnostics); + } + + + [Theory] + [MemberData(nameof(PublicTypeWithAllowedDefinitions))] + public async Task PublicExposureOfPubternalTypeSometimesAllowed(string member) + { + var code = GetSourceFromNamespaceDeclaration($@" +namespace A +{{ + {member} +}}"); + var diagnostics = await GetDiagnostics(code.Source); + Assert.Empty(diagnostics); + } + + [Theory] + [MemberData(nameof(PrivateMemberDefinitions))] + [MemberData(nameof(PublicMemberDefinitions))] + public async Task DefinitionOfPubternalCrossAssemblyProducesPUB0002(string member) + { + var code = TestSource.Read($@" +using A.Internal.Namespace; +namespace A +{{ + internal class T + {{ + {member} + }} +}}"); + + var diagnostic = Assert.Single(await GetDiagnosticWithProjectReference(code.Source)); + Assert.Equal("PUB0002", diagnostic.Id); + AnalyzerAssert.DiagnosticLocation(code.DefaultMarkerLocation, diagnostic.Location); + } + + [Theory] + [MemberData(nameof(TypeUsages))] + public async Task UsageOfPubternalCrossAssemblyProducesPUB0002(string usage) + { + var code = TestSource.Read($@" +using A.Internal.Namespace; +namespace A +{{ + public class T + {{ + private void M() + {{ + {usage} + }} + }} +}}"); + var diagnostic = Assert.Single(await GetDiagnosticWithProjectReference(code.Source)); + Assert.Equal("PUB0002", diagnostic.Id); + AnalyzerAssert.DiagnosticLocation(code.DefaultMarkerLocation, diagnostic.Location); + } + + public static IEnumerable PublicMemberDefinitions => + ApplyModifiers(MemberDefinitions, "public", "protected"); + + public static IEnumerable PublicMemberWithAllowedDefinitions => + ApplyModifiers(AllowedMemberDefinitions, "public"); + + public static IEnumerable PublicTypeDefinitions => + ApplyModifiers(TypeDefinitions, "public"); + + public static IEnumerable PublicTypeWithAllowedDefinitions => + ApplyModifiers(AllowedDefinitions, "public"); + + public static IEnumerable PrivateMemberDefinitions => + ApplyModifiers(MemberDefinitions, "private", "internal"); + + public static IEnumerable TypeUsages => + ApplyModifiers(TypeUsageStrings, string.Empty); + + public static string[] MemberDefinitions => new [] + { + "/*MM*/C c;", + "T(/*MM*/C c) {}", + "/*MM*/CD c { get; }", + "event /*MM*/CD c;", + "delegate /*MM*/C WOW();" + }; + + public static string[] TypeDefinitions => new [] + { + "delegate /*MM*/C WOW();", + "class /*MM*/T: P { } public class P {}", + "class /*MM*/T: C {}", + "class T { public class /*MM*/T1: C { } }" + }; + + public static string[] AllowedMemberDefinitions => new [] + { + "T([CA]int c) {}", + "[CA] MOD int f;", + "[CA] MOD int f { get; set; }", + "[CA] MOD class CC { }" + }; + + public static string[] AllowedDefinitions => new [] + { + "class T: I { } interface I {}" + }; + + public static string[] TypeUsageStrings => new [] + { + "/*MM*/var c = new C();", + "/*MM*/CD d = () => null;", + "var t = typeof(/*MM*/CAAttribute);" + }; + + private static IEnumerable ApplyModifiers(string[] code, params string[] mods) + { + foreach (var mod in mods) + { + foreach (var s in code) + { + if (s.Contains("MOD")) + { + yield return new object[] { s.Replace("MOD", mod) }; + } + else + { + yield return new object[] { mod + " " + s }; + } + } + } + } + + private TestSource GetSourceFromNamespaceDeclaration(string namespaceDefinition) + { + return TestSource.Read("using A.Internal.Namespace;" + InternalDefinitions + namespaceDefinition); + } + + private Task GetDiagnosticWithProjectReference(string code) + { + var libraray = CreateProject(InternalDefinitions); + + var mainProject = CreateProject(code).AddProjectReference(new ProjectReference(libraray.Id)); + + return GetDiagnosticsAsync(mainProject.Documents.ToArray(), new PubternalityAnalyzer(), new [] { "PUB0002" }); + } + + private Task GetDiagnostics(string code) + { + return GetDiagnosticsAsync(new[] { code }, new PubternalityAnalyzer(), new [] { "PUB0002" }); + } + } +} diff --git a/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/Assert.cs b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/Assert.cs new file mode 100644 index 0000000000..b79ae064b0 --- /dev/null +++ b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/Assert.cs @@ -0,0 +1,56 @@ +// 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.CodeAnalysis; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Analyzer.Testing +{ + public class AnalyzerAssert + { + public static void DiagnosticLocation(DiagnosticLocation expected, Location actual) + { + var actualSpan = actual.GetLineSpan(); + var actualLinePosition = actualSpan.StartLinePosition; + + // Only check line position if there is an actual line in the real diagnostic + if (actualLinePosition.Line > 0) + { + if (actualLinePosition.Line + 1 != expected.Line) + { + throw new DiagnosticLocationAssertException( + expected, + actual, + $"Expected diagnostic to be on line \"{expected.Line}\" was actually on line \"{actualLinePosition.Line + 1}\""); + } + } + + // Only check column position if there is an actual column position in the real diagnostic + if (actualLinePosition.Character > 0) + { + if (actualLinePosition.Character + 1 != expected.Column) + { + throw new DiagnosticLocationAssertException( + expected, + actual, + $"Expected diagnostic to start at column \"{expected.Column}\" was actually on column \"{actualLinePosition.Character + 1}\""); + } + } + } + + private class DiagnosticLocationAssertException : EqualException + { + public DiagnosticLocationAssertException( + DiagnosticLocation expected, + Location actual, + string message) + : base(expected, actual) + { + Message = message; + } + + public override string Message { get; } + } + } +} diff --git a/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/CodeFixRunner.cs b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/CodeFixRunner.cs new file mode 100644 index 0000000000..3c21e5c3b4 --- /dev/null +++ b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/CodeFixRunner.cs @@ -0,0 +1,72 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Microsoft.AspNetCore.Analyzer.Testing +{ + public class CodeFixRunner + { + public static CodeFixRunner Default { get; } = new CodeFixRunner(); + + public async Task ApplyCodeFixAsync( + CodeFixProvider codeFixProvider, + Document document, + Diagnostic analyzerDiagnostic, + int codeFixIndex = 0) + { + var actions = new List(); + var context = new CodeFixContext(document, analyzerDiagnostic, (a, d) => actions.Add(a), CancellationToken.None); + await codeFixProvider.RegisterCodeFixesAsync(context); + + Assert.NotEmpty(actions); + + var updatedSolution = await ApplyFixAsync(actions[codeFixIndex]); + + var updatedProject = updatedSolution.GetProject(document.Project.Id); + await EnsureCompilable(updatedProject); + + var updatedDocument = updatedSolution.GetDocument(document.Id); + var sourceText = await updatedDocument.GetTextAsync(); + return sourceText.ToString(); + } + + private async Task EnsureCompilable(Project project) + { + var compilationOptions = ConfigureCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var compilation = await project + .WithCompilationOptions(compilationOptions) + .GetCompilationAsync(); + var diagnostics = compilation.GetDiagnostics(); + if (diagnostics.Length != 0) + { + var message = string.Join( + Environment.NewLine, + diagnostics.Select(d => CSharpDiagnosticFormatter.Instance.Format(d))); + throw new InvalidOperationException($"Compilation failed:{Environment.NewLine}{message}"); + } + } + + private static async Task ApplyFixAsync(CodeAction codeAction) + { + var operations = await codeAction.GetOperationsAsync(CancellationToken.None); + return Assert.Single(operations.OfType()).ChangedSolution; + } + + protected virtual CompilationOptions ConfigureCompilationOptions(CompilationOptions options) + { + return options.WithOutputKind(OutputKind.DynamicallyLinkedLibrary); + } + } +} diff --git a/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticAnalyzerRunner.cs b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticAnalyzerRunner.cs new file mode 100644 index 0000000000..bfc9406335 --- /dev/null +++ b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticAnalyzerRunner.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.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Xunit; + +namespace Microsoft.AspNetCore.Analyzer.Testing +{ + /// + /// Base type for executing a . Derived types implemented in the test assembly will + /// correctly resolve reference assemblies required for compilaiton. + /// + public abstract class DiagnosticAnalyzerRunner + { + /// + /// Given classes in the form of strings, and an DiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document. + /// + /// Classes in the form of strings + /// The analyzer to be run on the sources + /// Additional diagnostics to enable at Info level + /// + /// When true, returns all diagnostics including compilation errors. + /// Otherwise; only returns analyzer diagnostics. + /// + /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location + protected Task GetDiagnosticsAsync( + string[] sources, + DiagnosticAnalyzer analyzer, + string[] additionalEnabledDiagnostics, + bool getAllDiagnostics = true) + { + var project = DiagnosticProject.Create(GetType().Assembly, sources); + return GetDiagnosticsAsync(new[] { project }, analyzer, additionalEnabledDiagnostics); + } + + /// + /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it. + /// The returned diagnostics are then ordered by location in the source document. + /// + /// Projects that the analyzer will be run on + /// The analyzer to run on the documents + /// Additional diagnostics to enable at Info level + /// + /// When true, returns all diagnostics including compilation errors. + /// Otherwise only returns analyzer diagnostics. + /// + /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location + protected async Task GetDiagnosticsAsync( + IEnumerable projects, + DiagnosticAnalyzer analyzer, + string[] additionalEnabledDiagnostics, + bool getAllDiagnostics = true) + { + var diagnostics = new List(); + foreach (var project in projects) + { + var compilation = await project.GetCompilationAsync(); + + // Enable any additional diagnostics + var options = ConfigureCompilationOptions(compilation.Options); + if (additionalEnabledDiagnostics.Length > 0) + { + options = compilation.Options + .WithSpecificDiagnosticOptions( + additionalEnabledDiagnostics.ToDictionary(s => s, s => ReportDiagnostic.Info)); + } + + var compilationWithAnalyzers = compilation + .WithOptions(options) + .WithAnalyzers(ImmutableArray.Create(analyzer)); + + if (getAllDiagnostics) + { + var diags = await compilationWithAnalyzers.GetAllDiagnosticsAsync(); + + Assert.DoesNotContain(diags, d => d.Id == "AD0001"); + + // Filter out non-error diagnostics not produced by our analyzer + // We want to KEEP errors because we might have written bad code. But sometimes we leave warnings in to make the + // test code more convenient + diags = diags.Where(d => d.Severity == DiagnosticSeverity.Error || analyzer.SupportedDiagnostics.Any(s => s.Id.Equals(d.Id))).ToImmutableArray(); + + foreach (var diag in diags) + { + if (diag.Location == Location.None || diag.Location.IsInMetadata) + { + diagnostics.Add(diag); + } + else + { + foreach (var document in projects.SelectMany(p => p.Documents)) + { + var tree = await document.GetSyntaxTreeAsync(); + if (tree == diag.Location.SourceTree) + { + diagnostics.Add(diag); + } + } + } + } + } + else + { + diagnostics.AddRange(await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync()); + } + } + + return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + } + + protected virtual CompilationOptions ConfigureCompilationOptions(CompilationOptions options) + { + return options.WithOutputKind(OutputKind.DynamicallyLinkedLibrary); + } + } +} diff --git a/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticLocation.cs b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticLocation.cs new file mode 100644 index 0000000000..e5321613f7 --- /dev/null +++ b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticLocation.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; + +namespace Microsoft.AspNetCore.Analyzer.Testing +{ + /// + /// Location where the diagnostic appears, as determined by path, line number, and column number. + /// + public class DiagnosticLocation + { + public DiagnosticLocation(int line, int column) + : this($"{DiagnosticProject.DefaultFilePathPrefix}.cs", line, column) + { + } + + public DiagnosticLocation(string path, int line, int column) + { + if (line < -1) + { + throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1"); + } + + if (column < -1) + { + throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); + } + + Path = path; + Line = line; + Column = column; + } + + public string Path { get; } + public int Line { get; } + public int Column { get; } + } +} diff --git a/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticProject.cs b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticProject.cs new file mode 100644 index 0000000000..013bbf151c --- /dev/null +++ b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticProject.cs @@ -0,0 +1,99 @@ +// 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.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.DependencyModel; +using Microsoft.Extensions.DependencyModel.Resolution; + +namespace Microsoft.AspNetCore.Analyzer.Testing +{ + public class DiagnosticProject + { + /// + /// File name prefix used to generate Documents instances from source. + /// + public static string DefaultFilePathPrefix = "Test"; + + /// + /// Project name. + /// + public static string TestProjectName = "TestProject"; + + private static readonly Dictionary _solutionCache = new Dictionary(); + + public static Project Create(Assembly testAssembly, string[] sources) + { + Solution solution; + lock (_solutionCache) + { + if (!_solutionCache.TryGetValue(testAssembly, out solution)) + { + var projectId = ProjectId.CreateNewId(debugName: TestProjectName); + solution = new AdhocWorkspace() + .CurrentSolution + .AddProject(projectId, TestProjectName, TestProjectName, LanguageNames.CSharp); + + foreach (var defaultCompileLibrary in DependencyContext.Load(testAssembly).CompileLibraries) + { + foreach (var resolveReferencePath in defaultCompileLibrary.ResolveReferencePaths(new AppLocalResolver())) + { + solution = solution.AddMetadataReference(projectId, MetadataReference.CreateFromFile(resolveReferencePath)); + } + } + + _solutionCache.Add(testAssembly, solution); + } + } + + var testProject = solution.ProjectIds.Single(); + var fileNamePrefix = DefaultFilePathPrefix; + + for (var i = 0; i < sources.Length; i++) + { + var newFileName = fileNamePrefix; + if (sources.Length > 1) + { + newFileName += i; + } + newFileName += ".cs"; + + var documentId = DocumentId.CreateNewId(testProject, debugName: newFileName); + solution = solution.AddDocument(documentId, newFileName, SourceText.From(sources[i])); + } + + return solution.GetProject(testProject); + } + + // Required to resolve compilation assemblies inside unit tests + private class AppLocalResolver : ICompilationAssemblyResolver + { + public bool TryResolveAssemblyPaths(CompilationLibrary library, List assemblies) + { + foreach (var assembly in library.Assemblies) + { + var dll = Path.Combine(Directory.GetCurrentDirectory(), "refs", Path.GetFileName(assembly)); + if (File.Exists(dll)) + { + assemblies.Add(dll); + return true; + } + + dll = Path.Combine(Directory.GetCurrentDirectory(), Path.GetFileName(assembly)); + if (File.Exists(dll)) + { + assemblies.Add(dll); + return true; + } + } + + return false; + } + } + } +} diff --git a/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticVerifier.cs b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticVerifier.cs new file mode 100644 index 0000000000..d0796f40a8 --- /dev/null +++ b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticVerifier.cs @@ -0,0 +1,213 @@ +// 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.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.DependencyModel; +using Microsoft.Extensions.DependencyModel.Resolution; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Analyzer.Testing +{ + /// + /// Superclass of all Unit Tests for DiagnosticAnalyzers + /// + public abstract class DiagnosticVerifier + { + private readonly ITestOutputHelper _testOutputHelper; + + /// + protected DiagnosticVerifier(): this(null) + { + } + + /// + protected DiagnosticVerifier(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + /// + /// File name prefix used to generate Documents instances from source. + /// + protected static string DefaultFilePathPrefix = "Test"; + /// + /// Project name of + /// + protected static string TestProjectName = "TestProject"; + + protected Solution Solution { get; set; } + + /// + /// Given classes in the form of strings, and an IDiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document. + /// + /// Classes in the form of strings + /// The analyzer to be run on the sources + /// Additional diagnostics to enable at Info level + /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location + protected Task GetDiagnosticsAsync(string[] sources, DiagnosticAnalyzer analyzer, string[] additionalEnabledDiagnostics) + { + return GetDiagnosticsAsync(GetDocuments(sources), analyzer, additionalEnabledDiagnostics); + } + + /// + /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it. + /// The returned diagnostics are then ordered by location in the source document. + /// + /// The Documents that the analyzer will be run on + /// The analyzer to run on the documents + /// Additional diagnostics to enable at Info level + /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location + protected async Task GetDiagnosticsAsync(Document[] documents, DiagnosticAnalyzer analyzer, string[] additionalEnabledDiagnostics) + { + var projects = new HashSet(); + foreach (var document in documents) + { + projects.Add(document.Project); + } + + var diagnostics = new List(); + foreach (var project in projects) + { + var compilation = await project.GetCompilationAsync(); + + // Enable any additional diagnostics + var options = compilation.Options; + if (additionalEnabledDiagnostics.Length > 0) + { + options = compilation.Options + .WithSpecificDiagnosticOptions( + additionalEnabledDiagnostics.ToDictionary(s => s, s => ReportDiagnostic.Info)); + } + + var compilationWithAnalyzers = compilation + .WithOptions(options) + .WithAnalyzers(ImmutableArray.Create(analyzer)); + + var diags = await compilationWithAnalyzers.GetAllDiagnosticsAsync(); + + foreach (var diag in diags) + { + _testOutputHelper?.WriteLine("Diagnostics: " + diag); + } + + Assert.DoesNotContain(diags, d => d.Id == "AD0001"); + + // Filter out non-error diagnostics not produced by our analyzer + // We want to KEEP errors because we might have written bad code. But sometimes we leave warnings in to make the + // test code more convenient + diags = diags.Where(d => d.Severity == DiagnosticSeverity.Error || analyzer.SupportedDiagnostics.Any(s => s.Id.Equals(d.Id))).ToImmutableArray(); + + foreach (var diag in diags) + { + if (diag.Location == Location.None || diag.Location.IsInMetadata) + { + diagnostics.Add(diag); + } + else + { + foreach (var document in documents) + { + var tree = await document.GetSyntaxTreeAsync(); + if (tree == diag.Location.SourceTree) + { + diagnostics.Add(diag); + } + } + } + } + } + + return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + } + + /// + /// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it. + /// + /// Classes in the form of strings + /// An array of Documents produced from the sources. + private Document[] GetDocuments(string[] sources) + { + var project = CreateProject(sources); + var documents = project.Documents.ToArray(); + + Debug.Assert(sources.Length == documents.Length); + + return documents; + } + + /// + /// Create a project using the inputted strings as sources. + /// + /// Classes in the form of strings + /// A Project created out of the Documents created from the source strings + protected Project CreateProject(params string[] sources) + { + var fileNamePrefix = DefaultFilePathPrefix; + + var projectId = ProjectId.CreateNewId(debugName: TestProjectName); + + Solution = Solution ?? new AdhocWorkspace().CurrentSolution; + + Solution = Solution.AddProject(projectId, TestProjectName, TestProjectName, LanguageNames.CSharp) + .WithProjectCompilationOptions(projectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + foreach (var defaultCompileLibrary in DependencyContext.Load(GetType().Assembly).CompileLibraries) + { + foreach (var resolveReferencePath in defaultCompileLibrary.ResolveReferencePaths(new AppLocalResolver())) + { + Solution = Solution.AddMetadataReference(projectId, MetadataReference.CreateFromFile(resolveReferencePath)); + } + } + + var count = 0; + foreach (var source in sources) + { + var newFileName = fileNamePrefix + count; + + _testOutputHelper?.WriteLine("Adding file: " + newFileName + Environment.NewLine + source); + + var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); + Solution = Solution.AddDocument(documentId, newFileName, SourceText.From(source)); + count++; + } + return Solution.GetProject(projectId); + } + + // Required to resolve compilation assemblies inside unit tests + private class AppLocalResolver : ICompilationAssemblyResolver + { + public bool TryResolveAssemblyPaths(CompilationLibrary library, List assemblies) + { + foreach (var assembly in library.Assemblies) + { + var dll = Path.Combine(Directory.GetCurrentDirectory(), "refs", Path.GetFileName(assembly)); + if (File.Exists(dll)) + { + assemblies.Add(dll); + continue; + } + + dll = Path.Combine(Directory.GetCurrentDirectory(), Path.GetFileName(assembly)); + if (File.Exists(dll)) + { + assemblies.Add(dll); + } + } + + return assemblies.Count > 0; + } + } + } +} diff --git a/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/Microsoft.AspNetCore.Analyzer.Testing.csproj b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/Microsoft.AspNetCore.Analyzer.Testing.csproj new file mode 100644 index 0000000000..bea1900716 --- /dev/null +++ b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/Microsoft.AspNetCore.Analyzer.Testing.csproj @@ -0,0 +1,36 @@ + + + + Helpers for writing tests for Roslyn analyzers. + netstandard2.0 + $(PackageTags);testing + true + + false + + true + true + + + + + + + + + + + + + + + + + diff --git a/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/TestSource.cs b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/TestSource.cs new file mode 100644 index 0000000000..ef70152faa --- /dev/null +++ b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/TestSource.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.AspNetCore.Analyzer.Testing +{ + public class TestSource + { + private const string MarkerStart = "/*MM"; + private const string MarkerEnd = "*/"; + + public IDictionary MarkerLocations { get; } + = new Dictionary(StringComparer.Ordinal); + + public DiagnosticLocation DefaultMarkerLocation { get; private set; } + + public string Source { get; private set; } + + public static TestSource Read(string rawSource) + { + var testInput = new TestSource(); + var lines = rawSource.Split(new[] { "\n", "\r\n" }, StringSplitOptions.None); + for (var i = 0; i < lines.Length; i++) + { + var line = lines[i]; + var markerStartIndex = line.IndexOf(MarkerStart, StringComparison.Ordinal); + if (markerStartIndex != -1) + { + var markerEndIndex = line.IndexOf(MarkerEnd, markerStartIndex, StringComparison.Ordinal); + var markerName = line.Substring(markerStartIndex + 2, markerEndIndex - markerStartIndex - 2); + var markerLocation = new DiagnosticLocation(i + 1, markerStartIndex + 1); + if (testInput.DefaultMarkerLocation == null) + { + testInput.DefaultMarkerLocation = markerLocation; + } + + testInput.MarkerLocations.Add(markerName, markerLocation); + line = line.Substring(0, markerStartIndex) + line.Substring(markerEndIndex + MarkerEnd.Length); + } + + lines[i] = line; + } + + testInput.Source = string.Join(Environment.NewLine, lines); + return testInput; + } + } +} 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/Analyzers/test/Microsoft.AspNetCore.Components.Analyzers.Tests.csproj b/src/Components/Analyzers/test/Microsoft.AspNetCore.Components.Analyzers.Tests.csproj index 80b92842f4..4d1ad9ba52 100644 --- a/src/Components/Analyzers/test/Microsoft.AspNetCore.Components.Analyzers.Tests.csproj +++ b/src/Components/Analyzers/test/Microsoft.AspNetCore.Components.Analyzers.Tests.csproj @@ -2,17 +2,17 @@ $(DefaultNetCoreTargetFramework) - + true + - 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/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index 77e0b06faa..5ecddb3ed6 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -2810,8 +2810,7 @@ namespace Microsoft.AspNetCore.Components.Test Assert.Equal(10, component.OnAfterRenderCallCount); } - [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/7487")] + [Fact] public async Task CanTriggerEventHandlerDisposedInEarlierPendingBatchAsync() { // This represents the scenario where the same event handler is being triggered 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/Directory.Build.props b/src/Framework/Directory.Build.props index 2a8b3bf3c0..97b1b157ff 100644 --- a/src/Framework/Directory.Build.props +++ b/src/Framework/Directory.Build.props @@ -6,6 +6,7 @@ PlatformManifest.txt $(ArtifactsObjDir)$(PlatformManifestFileName) + true diff --git a/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj b/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj index f695e4d3ee..4f60448f35 100644 --- a/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj +++ b/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj @@ -53,6 +53,10 @@ This package is an internal implementation of the .NET Core SDK and is not meant $(AspNetCoreMajorVersion).$(AspNetCoreMinorVersion).0 $(ReferencePackSharedFxVersion)-$(VersionSuffix) + + + $(PkgMicrosoft_Extensions_Internal_Transport)\ref\$(TargetFramework)\ + @@ -109,14 +113,14 @@ This package is an internal implementation of the .NET Core SDK and is not meant BeforeTargets="_GetPackageFiles" DependsOnTargets="ResolveReferences;FindReferenceAssembliesForReferences"> - <_AvailableExtensionsRefAssemblies Include="$(MicrosoftInternalExtensionsRefsPath)\*.dll" /> + <_AvailableExtensionsRefAssemblies Include="$(RuntimeExtensionsReferenceDirectory)*.dll" /> - + @@ -131,7 +135,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant - + <_ReferencedExtensionsRefAssembliesToExclude Include="@(_ReferencedExtensionsRefAssemblies)" Exclude="@(_DuplicatedExtensionsRefAssemblies)" /> @@ -147,10 +151,10 @@ This package is an internal implementation of the .NET Core SDK and is not meant @(ReferencePathWithRefAssemblies->WithMetadataValue('ReferenceGrouping', 'Microsoft.NETCore.App'));" /> + Include="@(_SelectedExtensionsRefAssemblies->'$(RuntimeExtensionsReferenceDirectory)%(FileName)%(Extension)')" /> - + @@ -165,7 +169,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant Outputs="$(TargetDir)$(PackageConflictManifestFileName)"> - <_AspNetCoreAppPackageOverrides Include="@(AspNetCoreReferenceAssemblyPath->'%(NuGetPackageId)|%(NuGetPackageVersion)')" Condition="!Exists('$(MicrosoftInternalExtensionsRefsPath)%(AspNetCoreReferenceAssemblyPath.NuGetPackageId).dll') AND '%(AspNetCoreReferenceAssemblyPath.NuGetPackageId)' != 'Microsoft.NETCore.App' AND '%(AspNetCoreReferenceAssemblyPath.NuGetPackageId)' != 'Microsoft.Internal.Extensions.Refs' AND '%(AspNetCoreReferenceAssemblyPath.NuGetSourceType)' == 'Package' " /> + <_AspNetCoreAppPackageOverrides Include="@(AspNetCoreReferenceAssemblyPath->'%(NuGetPackageId)|%(NuGetPackageVersion)')" Condition="!Exists('$(RuntimeExtensionsReferenceDirectory)%(AspNetCoreReferenceAssemblyPath.NuGetPackageId).dll') AND '%(AspNetCoreReferenceAssemblyPath.NuGetPackageId)' != 'Microsoft.NETCore.App' AND '%(AspNetCoreReferenceAssemblyPath.NuGetPackageId)' != 'Microsoft.Extensions.Internal.Transport' AND '%(AspNetCoreReferenceAssemblyPath.NuGetSourceType)' == 'Package' " /> <_AspNetCoreAppPackageOverrides Include="@(_SelectedExtensionsRefAssemblies->'%(FileName)|$(MicrosoftInternalExtensionsRefsPackageOverrideVersion)')" /> 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/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj index d6b79361e7..4f83bbda83 100644 --- a/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj +++ b/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj @@ -9,6 +9,7 @@ + @@ -16,6 +17,7 @@ + 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/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj index a737e01d45..44e1d8d95f 100644 --- a/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj +++ b/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj @@ -28,6 +28,7 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder + 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/Hosting/test/FunctionalTests/ShutdownTests.cs b/src/Hosting/test/FunctionalTests/ShutdownTests.cs index 96ba91bcaa..58abe91b33 100644 --- a/src/Hosting/test/FunctionalTests/ShutdownTests.cs +++ b/src/Hosting/test/FunctionalTests/ShutdownTests.cs @@ -32,6 +32,7 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests await ExecuteShutdownTest(nameof(ShutdownTestRun), "Run"); } + [QuarantinedTest] [ConditionalFact] [OSSkipCondition(OperatingSystems.Windows)] [OSSkipCondition(OperatingSystems.MacOSX)] @@ -133,7 +134,7 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests private static void WaitForExitOrKill(Process process) { - process.WaitForExit(1000); + process.WaitForExit(5 * 1000); if (!process.HasExited) { process.Kill(); 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.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp.cs b/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp.cs index 850f4c3569..1ece9f3acd 100644 --- a/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp.cs +++ b/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp.cs @@ -97,6 +97,14 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure } namespace Microsoft.AspNetCore.Http { + public partial class BadHttpRequestException : System.IO.IOException + { + public BadHttpRequestException(string message) { } + public BadHttpRequestException(string message, System.Exception innerException) { } + public BadHttpRequestException(string message, int statusCode) { } + public BadHttpRequestException(string message, int statusCode, System.Exception innerException) { } + public int StatusCode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } + } public abstract partial class ConnectionInfo { protected ConnectionInfo() { } diff --git a/src/Http/Http.Abstractions/src/BadHttpRequestException.cs b/src/Http/Http.Abstractions/src/BadHttpRequestException.cs new file mode 100644 index 0000000000..1e8a0bf096 --- /dev/null +++ b/src/Http/Http.Abstractions/src/BadHttpRequestException.cs @@ -0,0 +1,60 @@ +using System; +using System.IO; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Represents an HTTP request error + /// + public class BadHttpRequestException : IOException + { + /// + /// Initializes a new instance of the class. + /// + /// The message to associate with this exception. + /// The HTTP status code to associate with this exception. + public BadHttpRequestException(string message, int statusCode) + : base(message) + { + StatusCode = statusCode; + } + + /// + /// Initializes a new instance of the class with the set to 400 Bad Request. + /// + /// The message to associate with this exception + public BadHttpRequestException(string message) + : base(message) + { + StatusCode = StatusCodes.Status400BadRequest; + } + + /// + /// Initializes a new instance of the class. + /// + /// The message to associate with this exception. + /// The HTTP status code to associate with this exception. + /// The inner exception to associate with this exception + public BadHttpRequestException(string message, int statusCode, Exception innerException) + : base(message, innerException) + { + StatusCode = statusCode; + } + + /// + /// Initializes a new instance of the class with the set to 400 Bad Request. + /// + /// The message to associate with this exception + /// The inner exception to associate with this exception + public BadHttpRequestException(string message, Exception innerException) + : base(message, innerException) + { + StatusCode = StatusCodes.Status400BadRequest; + } + + /// + /// Gets the HTTP status code for this exception. + /// + public int StatusCode { get; } + } +} 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/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs index 099fdf73e5..dda0233d77 100644 --- a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs +++ b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs @@ -23,7 +23,6 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer X509KeyStorageFlags.DefaultKeySet); [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")] [FrameworkSkipCondition(RuntimeFrameworks.CLR)] public void Configure_AddsDevelopmentKeyFromConfiguration() { @@ -64,7 +63,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer } [ConditionalFact] - [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 Configure_LoadsPfxCertificateCredentialFromConfiguration() { // Arrange @@ -94,7 +93,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer } [ConditionalFact] - [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 Configure_LoadsCertificateStoreCertificateCredentialFromConfiguration() { try diff --git a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs index c5d543dfe9..893be873ab 100644 --- a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs +++ b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration } [ConditionalFact] - [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 static void LoadFromStoreCert_SkipsCertificatesNotYetValid() { try @@ -82,7 +82,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration } [ConditionalFact] - [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 static void LoadFromStoreCert_PrefersCertificatesCloserToExpirationDate() { try @@ -105,7 +105,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration } [ConditionalFact] - [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 static void LoadFromStoreCert_SkipsExpiredCertificates() { try diff --git a/src/Identity/Core/ref/Microsoft.AspNetCore.Identity.netcoreapp.cs b/src/Identity/Core/ref/Microsoft.AspNetCore.Identity.netcoreapp.cs index 0ccafd325b..61b1891d26 100644 --- a/src/Identity/Core/ref/Microsoft.AspNetCore.Identity.netcoreapp.cs +++ b/src/Identity/Core/ref/Microsoft.AspNetCore.Identity.netcoreapp.cs @@ -188,6 +188,8 @@ namespace Microsoft.Extensions.DependencyInjection public static Microsoft.AspNetCore.Identity.IdentityBuilder AddIdentity(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) where TUser : class where TRole : class { throw null; } public static Microsoft.AspNetCore.Identity.IdentityBuilder AddIdentity(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action setupAction) where TUser : class where TRole : class { throw null; } public static Microsoft.Extensions.DependencyInjection.IServiceCollection ConfigureApplicationCookie(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IServiceCollection ConfigureApplicationCookie(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure) where TService : class { throw null; } public static Microsoft.Extensions.DependencyInjection.IServiceCollection ConfigureExternalCookie(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IServiceCollection ConfigureExternalCookie(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure) where TService : class { throw null; } } } diff --git a/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs b/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs index 45e3d567eb..ffbc9f563e 100644 --- a/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs +++ b/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs @@ -110,6 +110,21 @@ namespace Microsoft.Extensions.DependencyInjection public static IServiceCollection ConfigureApplicationCookie(this IServiceCollection services, Action configure) => services.Configure(IdentityConstants.ApplicationScheme, configure); + /// + /// Configures the application cookie. + /// + /// TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly. + /// The services available in the application. + /// An action to configure the . + /// The services. + public static IServiceCollection ConfigureApplicationCookie(this IServiceCollection services, Action configure) where TService : class + { + services.AddOptions(IdentityConstants.ApplicationScheme) + .Configure(configure); + + return services; + } + /// /// Configure the external cookie. /// @@ -118,5 +133,20 @@ namespace Microsoft.Extensions.DependencyInjection /// The services. public static IServiceCollection ConfigureExternalCookie(this IServiceCollection services, Action configure) => services.Configure(IdentityConstants.ExternalScheme, configure); + + /// + /// Configure the external cookie. + /// + /// TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly. + /// The services available in the application. + /// An action to configure the . + /// The services. + public static IServiceCollection ConfigureExternalCookie(this IServiceCollection services, Action configure) where TService : class + { + services.AddOptions(IdentityConstants.ExternalScheme) + .Configure(configure); + + return services; + } } } diff --git a/src/Identity/Extensions.Core/ref/Microsoft.Extensions.Identity.Core.csproj b/src/Identity/Extensions.Core/ref/Microsoft.Extensions.Identity.Core.csproj index 1ddecfe522..846f73feee 100644 --- a/src/Identity/Extensions.Core/ref/Microsoft.Extensions.Identity.Core.csproj +++ b/src/Identity/Extensions.Core/ref/Microsoft.Extensions.Identity.Core.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj b/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj index 7555dda1fe..5f6681e261 100644 --- a/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj +++ b/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj @@ -13,7 +13,8 @@ - + + 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/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/README.md b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/README.md index 77fc8404c4..511b1088fc 100644 --- a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/README.md +++ b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/README.md @@ -2,17 +2,3 @@ Microsoft IIS Common -------------------------------- The repository contains common resources shared by IIS Out-Of-Band (OOB) products. - -### Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.microsoft.com. - -When you submit a pull request, a CLA-bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -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. diff --git a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/README.md b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/README.md index 22e594aa6c..5a4135df83 100644 --- a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/README.md +++ b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/README.md @@ -2,17 +2,3 @@ Microsoft IIS Setup -------------------------------- The repository contains setup resources shared by IIS Out-Of-Band (OOB) products. - -### Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.microsoft.com. - -When you submit a pull request, a CLA-bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -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. 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/Middleware/Session/samples/SessionSample.csproj b/src/Middleware/Session/samples/SessionSample.csproj index c1d8559a47..3beef6fa7b 100644 --- a/src/Middleware/Session/samples/SessionSample.csproj +++ b/src/Middleware/Session/samples/SessionSample.csproj @@ -9,8 +9,6 @@ - - diff --git a/src/Middleware/Session/samples/Startup.cs b/src/Middleware/Session/samples/Startup.cs index c776d8e2ff..9bc4265daf 100644 --- a/src/Middleware/Session/samples/Startup.cs +++ b/src/Middleware/Session/samples/Startup.cs @@ -21,7 +21,8 @@ namespace SessionSample // Uncomment the following line to use the in-memory implementation of IDistributedCache services.AddDistributedMemoryCache(); - // Uncomment the following line to use the Microsoft SQL Server implementation of IDistributedCache. + // Uncomment the following line to use the Microsoft SQL Server implementation of IDistributedCache + // and add a PackageReference to Microsoft.Extensions.Caching.SqlServer in the .csrpoj. // Note that this would require setting up the session state database. //services.AddDistributedSqlServerCache(o => //{ @@ -30,7 +31,8 @@ namespace SessionSample // o.TableName = "Sessions"; //}); - // Uncomment the following line to use the Redis implementation of IDistributedCache. + // Uncomment the following line to use the Redis implementation of IDistributedCache + // and add a PackageReference to Microsoft.Extensions.Caching.StackExchangeRedis in the .csrpoj. // This will override any previously registered IDistributedCache service. //services.AddStackExchangeRedisCache(o => //{ 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/Mvc/Mvc.Analyzers/test/Mvc.Analyzers.Test.csproj b/src/Mvc/Mvc.Analyzers/test/Mvc.Analyzers.Test.csproj index 4335457466..bcbc066c95 100644 --- a/src/Mvc/Mvc.Analyzers/test/Mvc.Analyzers.Test.csproj +++ b/src/Mvc/Mvc.Analyzers/test/Mvc.Analyzers.Test.csproj @@ -15,8 +15,8 @@ + - diff --git a/src/Mvc/Mvc.Api.Analyzers/test/Mvc.Api.Analyzers.Test.csproj b/src/Mvc/Mvc.Api.Analyzers/test/Mvc.Api.Analyzers.Test.csproj index 179e80e92b..a37838a6bf 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/Mvc.Api.Analyzers.Test.csproj +++ b/src/Mvc/Mvc.Api.Analyzers/test/Mvc.Api.Analyzers.Test.csproj @@ -3,6 +3,7 @@ $(DefaultNetCoreTargetFramework) Microsoft.AspNetCore.Mvc.Api.Analyzers + true @@ -13,8 +14,8 @@ + - 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..a198f9b799 100644 --- a/src/ProjectTemplates/test/BlazorServerTemplateTest.cs +++ b/src/ProjectTemplates/test/BlazorServerTemplateTest.cs @@ -81,10 +81,11 @@ namespace Templates.Test } } - [ConditionalTheory] + [ConditionalTheory(Skip = "See: https://github.com/dotnet/aspnetcore/issues/20520")] [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/GrpcTemplateTest.cs b/src/ProjectTemplates/test/GrpcTemplateTest.cs index 707ffb9034..371c0022d3 100644 --- a/src/ProjectTemplates/test/GrpcTemplateTest.cs +++ b/src/ProjectTemplates/test/GrpcTemplateTest.cs @@ -25,7 +25,7 @@ namespace Templates.Test public ProjectFactoryFixture ProjectFactory { get; } public ITestOutputHelper Output { get; } - [ConditionalFact(Skip = "This test run for over an hour")] + [ConditionalFact] [SkipOnHelix("Not supported queues", Queues = "Windows.7.Amd64;Windows.7.Amd64.Open;OSX.1014.Amd64;OSX.1014.Amd64.Open")] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/19716")] public async Task GrpcTemplate() 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/ProjectTemplates/test/IdentityUIPackageTest.cs b/src/ProjectTemplates/test/IdentityUIPackageTest.cs index da766229a6..b89c19b61a 100644 --- a/src/ProjectTemplates/test/IdentityUIPackageTest.cs +++ b/src/ProjectTemplates/test/IdentityUIPackageTest.cs @@ -118,7 +118,7 @@ namespace Templates.Test "Identity/lib/jquery-validation-unobtrusive/LICENSE.txt", }; - [ConditionalTheory(Skip = "This test run for over an hour")] + [ConditionalTheory] [MemberData(nameof(MSBuildIdentityUIPackageOptions))] [SkipOnHelix("cert failure", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/19716")] diff --git a/src/ProjectTemplates/test/MvcTemplateTest.cs b/src/ProjectTemplates/test/MvcTemplateTest.cs index d881ffcdd0..ff60a16bd8 100644 --- a/src/ProjectTemplates/test/MvcTemplateTest.cs +++ b/src/ProjectTemplates/test/MvcTemplateTest.cs @@ -104,7 +104,7 @@ namespace Templates.Test } } - [ConditionalTheory(Skip = "This test run for over an hour")] + [ConditionalTheory] [InlineData(true)] [InlineData(false)] [SkipOnHelix("cert failure", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] diff --git a/src/ProjectTemplates/test/RazorPagesTemplateTest.cs b/src/ProjectTemplates/test/RazorPagesTemplateTest.cs index f7727fbb5a..10370a3573 100644 --- a/src/ProjectTemplates/test/RazorPagesTemplateTest.cs +++ b/src/ProjectTemplates/test/RazorPagesTemplateTest.cs @@ -94,7 +94,7 @@ namespace Templates.Test } } - [ConditionalTheory(Skip = "This test run for over an hour")] + [ConditionalTheory] [InlineData(false)] [InlineData(true)] [SkipOnHelix("cert failure", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] diff --git a/src/ProjectTemplates/test/SpaTemplateTest/ReactTemplateTest.cs b/src/ProjectTemplates/test/SpaTemplateTest/ReactTemplateTest.cs index d4a1ff1756..9e591ff67d 100644 --- a/src/ProjectTemplates/test/SpaTemplateTest/ReactTemplateTest.cs +++ b/src/ProjectTemplates/test/SpaTemplateTest/ReactTemplateTest.cs @@ -23,7 +23,7 @@ namespace Templates.Test.SpaTemplateTest => SpaTemplateImplAsync("reactnoauth", "react", useLocalDb: false, usesAuth: false); [QuarantinedTest] - [ConditionalFact(Skip="This test run for over an hour")] + [ConditionalFact] [SkipOnHelix("selenium")] public Task ReactTemplate_IndividualAuth_NetCore() => SpaTemplateImplAsync("reactindividual", "react", useLocalDb: false, usesAuth: true); diff --git a/src/ProjectTemplates/test/WorkerTemplateTest.cs b/src/ProjectTemplates/test/WorkerTemplateTest.cs index 09f444811c..1bc2d8d5b3 100644 --- a/src/ProjectTemplates/test/WorkerTemplateTest.cs +++ b/src/ProjectTemplates/test/WorkerTemplateTest.cs @@ -21,7 +21,7 @@ namespace Templates.Test public ProjectFactoryFixture ProjectFactory { get; } public ITestOutputHelper Output { get; } - [Fact(Skip = "This test run for over an hour")] + [Fact] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/19716")] public async Task WorkerTemplateAsync() { 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/Certificate/src/CertificateAuthenticationExtensions.cs b/src/Security/Authentication/Certificate/src/CertificateAuthenticationExtensions.cs index d49f2c274b..4926a21382 100644 --- a/src/Security/Authentication/Certificate/src/CertificateAuthenticationExtensions.cs +++ b/src/Security/Authentication/Certificate/src/CertificateAuthenticationExtensions.cs @@ -28,7 +28,7 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The . public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, string authenticationScheme) - => builder.AddCertificate(authenticationScheme, configureOptions: null); + => builder.AddCertificate(authenticationScheme, configureOptions: (Action)null); /// /// Adds certificate authentication. @@ -39,6 +39,16 @@ namespace Microsoft.Extensions.DependencyInjection public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, Action configureOptions) => builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme, configureOptions); + /// + /// Adds certificate authentication. + /// + /// TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly. + /// The . + /// + /// The . + public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, Action configureOptions) where TService : class + => builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme, configureOptions); + /// /// Adds certificate authentication. /// @@ -50,6 +60,32 @@ namespace Microsoft.Extensions.DependencyInjection this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) - => builder.AddScheme(authenticationScheme, configureOptions); + { + Action configureOptionsWithServices; + if (configureOptions == null) + { + configureOptionsWithServices = null; + } + else + { + configureOptionsWithServices = (options, _) => configureOptions(options); + } + + return builder.AddCertificate(authenticationScheme, configureOptionsWithServices); + } + + /// + /// Adds certificate authentication. + /// + /// TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly. + /// The . + /// + /// + /// The . + public static AuthenticationBuilder AddCertificate( + this AuthenticationBuilder builder, + string authenticationScheme, + Action configureOptions) where TService : class + => builder.AddScheme(authenticationScheme, configureOptions); } } diff --git a/src/Security/Authentication/Cookies/ref/Microsoft.AspNetCore.Authentication.Cookies.netcoreapp.cs b/src/Security/Authentication/Cookies/ref/Microsoft.AspNetCore.Authentication.Cookies.netcoreapp.cs index f3d730a1f9..714bdd2ebd 100644 --- a/src/Security/Authentication/Cookies/ref/Microsoft.AspNetCore.Authentication.Cookies.netcoreapp.cs +++ b/src/Security/Authentication/Cookies/ref/Microsoft.AspNetCore.Authentication.Cookies.netcoreapp.cs @@ -126,5 +126,8 @@ namespace Microsoft.Extensions.DependencyInjection public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCookie(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme) { throw null; } public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCookie(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, System.Action configureOptions) { throw null; } public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCookie(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, string displayName, System.Action configureOptions) { throw null; } + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCookie(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, System.Action configureOptions) where TService : class { throw null; } + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCookie(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, System.Action configureOptions) where TService : class { throw null; } + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCookie(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, string displayName, System.Action configureOptions) where TService : class { throw null; } } } diff --git a/src/Security/Authentication/Cookies/samples/CookieSessionSample/Startup.cs b/src/Security/Authentication/Cookies/samples/CookieSessionSample/Startup.cs index f7b8f2bb88..c538866d7e 100644 --- a/src/Security/Authentication/Cookies/samples/CookieSessionSample/Startup.cs +++ b/src/Security/Authentication/Cookies/samples/CookieSessionSample/Startup.cs @@ -14,12 +14,14 @@ namespace CookieSessionSample { public void ConfigureServices(IServiceCollection services) { + services.AddSingleton(); + // This can be removed after https://github.com/aspnet/IISIntegration/issues/371 services.AddAuthentication(options => { options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; - }).AddCookie(o => o.SessionStore = new MemoryCacheTicketStore()); + }).AddCookie((o, ticketStore) => o.SessionStore = ticketStore); } public void Configure(IApplicationBuilder app) diff --git a/src/Security/Authentication/Cookies/src/CookieExtensions.cs b/src/Security/Authentication/Cookies/src/CookieExtensions.cs index 7763e6a624..a67a708149 100644 --- a/src/Security/Authentication/Cookies/src/CookieExtensions.cs +++ b/src/Security/Authentication/Cookies/src/CookieExtensions.cs @@ -15,19 +15,40 @@ namespace Microsoft.Extensions.DependencyInjection => builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme); public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme) - => builder.AddCookie(authenticationScheme, configureOptions: null); + => builder.AddCookie(authenticationScheme, configureOptions: (Action)null); - public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, Action configureOptions) + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, Action configureOptions) where TService : class => builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, configureOptions); public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) => builder.AddCookie(authenticationScheme, displayName: null, configureOptions: configureOptions); + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) where TService : class + => builder.AddCookie(authenticationScheme, displayName: null, configureOptions: configureOptions); + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + Action configureOptionsWithServices; + if (configureOptions == null) + { + configureOptionsWithServices = null; + } + else + { + configureOptionsWithServices = (options, _) => configureOptions(options); + } + + return builder.AddCookie(authenticationScheme, displayName, configureOptionsWithServices); + } + + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) where TService : class { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, PostConfigureCookieAuthenticationOptions>()); builder.Services.AddOptions(authenticationScheme).Validate(o => o.Cookie.Expiration == null, "Cookie.Expiration is ignored, use ExpireTimeSpan instead."); - return builder.AddScheme(authenticationScheme, displayName, configureOptions); + return builder.AddScheme(authenticationScheme, displayName, configureOptions); } } } diff --git a/src/Security/Authentication/Core/ref/Microsoft.AspNetCore.Authentication.netcoreapp.cs b/src/Security/Authentication/Core/ref/Microsoft.AspNetCore.Authentication.netcoreapp.cs index 1a1d21a774..1cf17c4f5f 100644 --- a/src/Security/Authentication/Core/ref/Microsoft.AspNetCore.Authentication.netcoreapp.cs +++ b/src/Security/Authentication/Core/ref/Microsoft.AspNetCore.Authentication.netcoreapp.cs @@ -16,9 +16,13 @@ namespace Microsoft.AspNetCore.Authentication public AuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection services) { } public virtual Microsoft.Extensions.DependencyInjection.IServiceCollection Services { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public virtual Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddPolicyScheme(string authenticationScheme, string displayName, System.Action configureOptions) { throw null; } + public virtual Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddPolicyScheme(string authenticationScheme, string displayName, System.Action configureOptions) where TService : class { throw null; } public virtual Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddRemoteScheme(string authenticationScheme, string displayName, System.Action configureOptions) where TOptions : Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions, new() where THandler : Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler { throw null; } + public virtual Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddRemoteScheme(string authenticationScheme, string displayName, System.Action configureOptions) where TOptions : Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions, new() where THandler : Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler where TService : class { throw null; } public virtual Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddScheme(string authenticationScheme, System.Action configureOptions) where TOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions, new() where THandler : Microsoft.AspNetCore.Authentication.AuthenticationHandler { throw null; } public virtual Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddScheme(string authenticationScheme, string displayName, System.Action configureOptions) where TOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions, new() where THandler : Microsoft.AspNetCore.Authentication.AuthenticationHandler { throw null; } + public virtual Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddScheme(string authenticationScheme, System.Action configureOptions) where TOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions, new() where THandler : Microsoft.AspNetCore.Authentication.AuthenticationHandler where TService : class { throw null; } + public virtual Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddScheme(string authenticationScheme, string displayName, System.Action configureOptions) where TOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions, new() where THandler : Microsoft.AspNetCore.Authentication.AuthenticationHandler where TService : class { throw null; } } public abstract partial class AuthenticationHandler : Microsoft.AspNetCore.Authentication.IAuthenticationHandler where TOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions, new() { diff --git a/src/Security/Authentication/Core/src/AuthenticationBuilder.cs b/src/Security/Authentication/Core/src/AuthenticationBuilder.cs index d4efd0c847..829fe007d7 100644 --- a/src/Security/Authentication/Core/src/AuthenticationBuilder.cs +++ b/src/Security/Authentication/Core/src/AuthenticationBuilder.cs @@ -25,25 +25,31 @@ namespace Microsoft.AspNetCore.Authentication /// public virtual IServiceCollection Services { get; } - private AuthenticationBuilder AddSchemeHelper(string authenticationScheme, string displayName, Action configureOptions) + private AuthenticationBuilder AddSchemeHelper(string authenticationScheme, string displayName, Action configureOptions) where TService : class where TOptions : AuthenticationSchemeOptions, new() where THandler : class, IAuthenticationHandler { Services.Configure(o => { - o.AddScheme(authenticationScheme, scheme => { + o.AddScheme(authenticationScheme, scheme => + { scheme.HandlerType = typeof(THandler); scheme.DisplayName = displayName; }); }); + + var optionsBuilder = Services.AddOptions(authenticationScheme) + .Validate(o => + { + o.Validate(authenticationScheme); + return true; + }); + if (configureOptions != null) { - Services.Configure(authenticationScheme, configureOptions); + optionsBuilder.Configure(configureOptions); } - Services.AddOptions(authenticationScheme).Validate(o => { - o.Validate(authenticationScheme); - return true; - }); + Services.AddTransient(); return this; } @@ -60,7 +66,22 @@ namespace Microsoft.AspNetCore.Authentication public virtual AuthenticationBuilder AddScheme(string authenticationScheme, string displayName, Action configureOptions) where TOptions : AuthenticationSchemeOptions, new() where THandler : AuthenticationHandler - => AddSchemeHelper(authenticationScheme, displayName, configureOptions); + => AddSchemeHelper(authenticationScheme, displayName, MapConfiguration(configureOptions)); + + /// + /// Adds a which can be used by . + /// + /// The type to configure the handler."/>. + /// The used to handle this scheme. + /// TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly. + /// The name of this scheme. + /// The display name of this scheme. + /// Used to configure the scheme options. + /// The builder. + public virtual AuthenticationBuilder AddScheme(string authenticationScheme, string displayName, Action configureOptions) where TService : class + where TOptions : AuthenticationSchemeOptions, new() + where THandler : AuthenticationHandler + => AddSchemeHelper(authenticationScheme, displayName, configureOptions); /// /// Adds a which can be used by . @@ -75,6 +96,20 @@ namespace Microsoft.AspNetCore.Authentication where THandler : AuthenticationHandler => AddScheme(authenticationScheme, displayName: null, configureOptions: configureOptions); + /// + /// Adds a which can be used by . + /// + /// The type to configure the handler."/>. + /// The used to handle this scheme. + /// TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly. + /// The name of this scheme. + /// Used to configure the scheme options. + /// The builder. + public virtual AuthenticationBuilder AddScheme(string authenticationScheme, Action configureOptions) where TService : class + where TOptions : AuthenticationSchemeOptions, new() + where THandler : AuthenticationHandler + => AddScheme(authenticationScheme, displayName: null, configureOptions: configureOptions); + /// /// Adds a based that supports remote authentication /// which can be used by . @@ -93,6 +128,25 @@ namespace Microsoft.AspNetCore.Authentication return AddScheme(authenticationScheme, displayName, configureOptions: configureOptions); } + /// + /// Adds a based that supports remote authentication + /// which can be used by . + /// + /// The type to configure the handler."/>. + /// The used to handle this scheme. + /// TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly. + /// The name of this scheme. + /// The display name of this scheme. + /// Used to configure the scheme options. + /// The builder. + public virtual AuthenticationBuilder AddRemoteScheme(string authenticationScheme, string displayName, Action configureOptions) where TService : class + where TOptions : RemoteAuthenticationOptions, new() + where THandler : RemoteAuthenticationHandler + { + Services.TryAddEnumerable(ServiceDescriptor.Singleton, EnsureSignInScheme>()); + return AddScheme(authenticationScheme, displayName, configureOptions: configureOptions); + } + /// /// Adds a based authentication handler which can be used to /// redirect to other authentication schemes. @@ -102,7 +156,30 @@ namespace Microsoft.AspNetCore.Authentication /// Used to configure the scheme options. /// The builder. public virtual AuthenticationBuilder AddPolicyScheme(string authenticationScheme, string displayName, Action configureOptions) - => AddSchemeHelper(authenticationScheme, displayName, configureOptions); + => AddSchemeHelper(authenticationScheme, displayName, MapConfiguration(configureOptions)); + + /// + /// Adds a based authentication handler which can be used to + /// redirect to other authentication schemes. + /// + /// The name of this scheme. + /// The display name of this scheme. + /// Used to configure the scheme options. + /// The builder. + public virtual AuthenticationBuilder AddPolicyScheme(string authenticationScheme, string displayName, Action configureOptions) where TService : class + => AddSchemeHelper(authenticationScheme, displayName, configureOptions); + + private Action MapConfiguration(Action configureOptions) + { + if (configureOptions == null) + { + return null; + } + else + { + return (options, _) => configureOptions(options); + } + } // Used to ensure that there's always a default sign in scheme that's not itself private class EnsureSignInScheme : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions diff --git a/src/Security/Authentication/Facebook/src/FacebookExtensions.cs b/src/Security/Authentication/Facebook/src/FacebookExtensions.cs index 2273724a42..b32b35c6c1 100644 --- a/src/Security/Authentication/Facebook/src/FacebookExtensions.cs +++ b/src/Security/Authentication/Facebook/src/FacebookExtensions.cs @@ -15,10 +15,31 @@ namespace Microsoft.Extensions.DependencyInjection public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, Action configureOptions) => builder.AddFacebook(FacebookDefaults.AuthenticationScheme, configureOptions); + public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, Action configureOptions) where TService : class + => builder.AddFacebook(FacebookDefaults.AuthenticationScheme, configureOptions); + public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) => builder.AddFacebook(authenticationScheme, FacebookDefaults.DisplayName, configureOptions); + public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) where TService : class + => builder.AddFacebook(authenticationScheme, FacebookDefaults.DisplayName, configureOptions); + public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) - => builder.AddOAuth(authenticationScheme, displayName, configureOptions); + { + Action configureOptionsWithServices; + if (configureOptions == null) + { + configureOptionsWithServices = null; + } + else + { + configureOptionsWithServices = (options, _) => configureOptions(options); + } + + return builder.AddFacebook(authenticationScheme, displayName, configureOptionsWithServices); + } + + public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) where TService : class + => builder.AddOAuth(authenticationScheme, displayName, configureOptions); } } diff --git a/src/Security/Authentication/Google/src/GoogleExtensions.cs b/src/Security/Authentication/Google/src/GoogleExtensions.cs index 95547014ca..88add9610d 100644 --- a/src/Security/Authentication/Google/src/GoogleExtensions.cs +++ b/src/Security/Authentication/Google/src/GoogleExtensions.cs @@ -15,10 +15,31 @@ namespace Microsoft.Extensions.DependencyInjection public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, Action configureOptions) => builder.AddGoogle(GoogleDefaults.AuthenticationScheme, configureOptions); + public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, Action configureOptions) where TService : class + => builder.AddGoogle(GoogleDefaults.AuthenticationScheme, configureOptions); + public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) => builder.AddGoogle(authenticationScheme, GoogleDefaults.DisplayName, configureOptions); + public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) where TService : class + => builder.AddGoogle(authenticationScheme, GoogleDefaults.DisplayName, configureOptions); + public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) - => builder.AddOAuth(authenticationScheme, displayName, configureOptions); + { + Action configureOptionsWithServices; + if (configureOptions == null) + { + configureOptionsWithServices = null; + } + else + { + configureOptionsWithServices = (options, _) => configureOptions(options); + } + + return builder.AddGoogle(authenticationScheme, displayName, configureOptionsWithServices); + } + + public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) where TService : class + => builder.AddOAuth(authenticationScheme, displayName, configureOptions); } } diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs index 334407c0da..a5aa46a451 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs @@ -17,13 +17,34 @@ namespace Microsoft.Extensions.DependencyInjection public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, Action configureOptions) => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, configureOptions); + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, Action configureOptions) where TService : class + => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, configureOptions); + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) => builder.AddJwtBearer(authenticationScheme, displayName: null, configureOptions: configureOptions); + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) where TService : class + => builder.AddJwtBearer(authenticationScheme, displayName: null, configureOptions: configureOptions); + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + Action configureOptionsWithServices; + if (configureOptions == null) + { + configureOptionsWithServices = null; + } + else + { + configureOptionsWithServices = (options, _) => configureOptions(options); + } + + return builder.AddJwtBearer(authenticationScheme, displayName, configureOptionsWithServices); + } + + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) where TService : class { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, JwtBearerPostConfigureOptions>()); - return builder.AddScheme(authenticationScheme, displayName, configureOptions); + return builder.AddScheme(authenticationScheme, displayName, configureOptions); } } } diff --git a/src/Security/Authentication/MicrosoftAccount/src/MicrosoftAccountExtensions.cs b/src/Security/Authentication/MicrosoftAccount/src/MicrosoftAccountExtensions.cs index 7f24e5af77..0c59ce3504 100644 --- a/src/Security/Authentication/MicrosoftAccount/src/MicrosoftAccountExtensions.cs +++ b/src/Security/Authentication/MicrosoftAccount/src/MicrosoftAccountExtensions.cs @@ -15,10 +15,31 @@ namespace Microsoft.Extensions.DependencyInjection public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, Action configureOptions) => builder.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, configureOptions); + public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, Action configureOptions) where TService : class + => builder.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, configureOptions); + public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) => builder.AddMicrosoftAccount(authenticationScheme, MicrosoftAccountDefaults.DisplayName, configureOptions); + public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) where TService : class + => builder.AddMicrosoftAccount(authenticationScheme, MicrosoftAccountDefaults.DisplayName, configureOptions); + public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) - => builder.AddOAuth(authenticationScheme, displayName, configureOptions); + { + Action configureOptionsWithServices; + if (configureOptions == null) + { + configureOptionsWithServices = null; + } + else + { + configureOptionsWithServices = (options, _) => configureOptions(options); + } + + return builder.AddMicrosoftAccount(authenticationScheme, displayName, configureOptionsWithServices); + } + + public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) where TService : class + => builder.AddOAuth(authenticationScheme, displayName, configureOptions); } -} \ No newline at end of file +} diff --git a/src/Security/Authentication/Negotiate/src/NegotiateExtensions.cs b/src/Security/Authentication/Negotiate/src/NegotiateExtensions.cs index f5bbf8cbc8..401c3dc839 100644 --- a/src/Security/Authentication/Negotiate/src/NegotiateExtensions.cs +++ b/src/Security/Authentication/Negotiate/src/NegotiateExtensions.cs @@ -31,6 +31,16 @@ namespace Microsoft.Extensions.DependencyInjection public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, Action configureOptions) => builder.AddNegotiate(NegotiateDefaults.AuthenticationScheme, configureOptions); + /// + /// Adds and configures Negotiate authentication. + /// + /// TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly. + /// The . + /// Allows for configuring the authentication handler. + /// The original builder. + public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, Action configureOptions) where TService : class + => builder.AddNegotiate(NegotiateDefaults.AuthenticationScheme, configureOptions); + /// /// Adds and configures Negotiate authentication. /// @@ -41,6 +51,17 @@ namespace Microsoft.Extensions.DependencyInjection public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) => builder.AddNegotiate(authenticationScheme, displayName: null, configureOptions: configureOptions); + /// + /// Adds and configures Negotiate authentication. + /// + /// TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly. + /// The . + /// The scheme name used to identify the authentication handler internally. + /// Allows for configuring the authentication handler. + /// The original builder. + public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) where TService : class + => builder.AddNegotiate(authenticationScheme, displayName: null, configureOptions: configureOptions); + /// /// Adds and configures Negotiate authentication. /// @@ -50,9 +71,33 @@ namespace Microsoft.Extensions.DependencyInjection /// Allows for configuring the authentication handler. /// The original builder. public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + Action configureOptionsWithServices; + if (configureOptions == null) + { + configureOptionsWithServices = null; + } + else + { + configureOptionsWithServices = (options, _) => configureOptions(options); + } + + return builder.AddNegotiate(authenticationScheme, displayName, configureOptionsWithServices); + } + + /// + /// Adds and configures Negotiate authentication. + /// + /// TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly. + /// The . + /// The scheme name used to identify the authentication handler internally. + /// The name displayed to users when selecting an authentication handler. The default is null to prevent this from displaying. + /// Allows for configuring the authentication handler. + /// The original builder. + public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) where TService : class { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, PostConfigureNegotiateOptions>()); - return builder.AddScheme(authenticationScheme, displayName, configureOptions); + return builder.AddScheme(authenticationScheme, displayName, configureOptions); } } } 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/Security/Authentication/OAuth/ref/Microsoft.AspNetCore.Authentication.OAuth.netcoreapp.cs b/src/Security/Authentication/OAuth/ref/Microsoft.AspNetCore.Authentication.OAuth.netcoreapp.cs index 2301daf1ad..94d2fc4d37 100644 --- a/src/Security/Authentication/OAuth/ref/Microsoft.AspNetCore.Authentication.OAuth.netcoreapp.cs +++ b/src/Security/Authentication/OAuth/ref/Microsoft.AspNetCore.Authentication.OAuth.netcoreapp.cs @@ -168,8 +168,12 @@ namespace Microsoft.Extensions.DependencyInjection { public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddOAuth(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, System.Action configureOptions) { throw null; } public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddOAuth(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, string displayName, System.Action configureOptions) { throw null; } + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddOAuth(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, System.Action configureOptions) where TService : class { throw null; } + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddOAuth(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, string displayName, System.Action configureOptions) where TService : class { throw null; } public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddOAuth(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, System.Action configureOptions) where TOptions : Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions, new() where THandler : Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler { throw null; } public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddOAuth(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, string displayName, System.Action configureOptions) where TOptions : Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions, new() where THandler : Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler { throw null; } + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddOAuth(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, System.Action configureOptions) where TOptions : Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions, new() where THandler : Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler where TService : class { throw null; } + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddOAuth(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, string displayName, System.Action configureOptions) where TOptions : Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions, new() where THandler : Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler where TService : class { throw null; } } public partial class OAuthPostConfigureOptions : Microsoft.Extensions.Options.IPostConfigureOptions where TOptions : Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions, new() where THandler : Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler { diff --git a/src/Security/Authentication/OAuth/src/OAuthExtensions.cs b/src/Security/Authentication/OAuth/src/OAuthExtensions.cs index 22c541a0ac..69bb2d73d3 100644 --- a/src/Security/Authentication/OAuth/src/OAuthExtensions.cs +++ b/src/Security/Authentication/OAuth/src/OAuthExtensions.cs @@ -14,20 +14,50 @@ namespace Microsoft.Extensions.DependencyInjection public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) => builder.AddOAuth>(authenticationScheme, configureOptions); + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) where TService : class + => builder.AddOAuth, TService>(authenticationScheme, configureOptions); + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) => builder.AddOAuth>(authenticationScheme, displayName, configureOptions); + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) where TService : class + => builder.AddOAuth, TService>(authenticationScheme, displayName, configureOptions); + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) where TOptions : OAuthOptions, new() where THandler : OAuthHandler => builder.AddOAuth(authenticationScheme, OAuthDefaults.DisplayName, configureOptions); + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + where TOptions : OAuthOptions, new() + where THandler : OAuthHandler + where TService : class + => builder.AddOAuth(authenticationScheme, OAuthDefaults.DisplayName, configureOptions); + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) where TOptions : OAuthOptions, new() where THandler : OAuthHandler + { + Action configureOptionsWithServices; + if (configureOptions == null) + { + configureOptionsWithServices = null; + } + else + { + configureOptionsWithServices = (options, _) => configureOptions(options); + } + + return builder.AddOAuth(authenticationScheme, displayName, configureOptionsWithServices); + } + + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + where TOptions : OAuthOptions, new() + where THandler : OAuthHandler + where TService : class { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, OAuthPostConfigureOptions>()); - return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); } } } diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectExtensions.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectExtensions.cs index f427bebaff..482452bca2 100644 --- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectExtensions.cs +++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectExtensions.cs @@ -17,13 +17,34 @@ namespace Microsoft.Extensions.DependencyInjection public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, Action configureOptions) => builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, configureOptions); + public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, Action configureOptions) where TService : class + => builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, configureOptions); + public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) => builder.AddOpenIdConnect(authenticationScheme, OpenIdConnectDefaults.DisplayName, configureOptions); + public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) where TService : class + => builder.AddOpenIdConnect(authenticationScheme, OpenIdConnectDefaults.DisplayName, configureOptions); + public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + Action configureOptionsWithServices; + if (configureOptions == null) + { + configureOptionsWithServices = null; + } + else + { + configureOptionsWithServices = (options, _) => configureOptions(options); + } + + return builder.AddOpenIdConnect(authenticationScheme, displayName, configureOptionsWithServices); + } + + public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) where TService : class { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdConnectPostConfigureOptions>()); - return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); } } } diff --git a/src/Security/Authentication/Twitter/src/TwitterExtensions.cs b/src/Security/Authentication/Twitter/src/TwitterExtensions.cs index 7243805692..f6f6b93a9e 100644 --- a/src/Security/Authentication/Twitter/src/TwitterExtensions.cs +++ b/src/Security/Authentication/Twitter/src/TwitterExtensions.cs @@ -17,13 +17,34 @@ namespace Microsoft.Extensions.DependencyInjection public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, Action configureOptions) => builder.AddTwitter(TwitterDefaults.AuthenticationScheme, configureOptions); + public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, Action configureOptions) where TService : class + => builder.AddTwitter(TwitterDefaults.AuthenticationScheme, configureOptions); + public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) => builder.AddTwitter(authenticationScheme, TwitterDefaults.DisplayName, configureOptions); + public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) where TService : class + => builder.AddTwitter(authenticationScheme, TwitterDefaults.DisplayName, configureOptions); + public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + Action configureOptionsWithServices; + if (configureOptions == null) + { + configureOptionsWithServices = null; + } + else + { + configureOptionsWithServices = (options, _) => configureOptions(options); + } + + return builder.AddTwitter(authenticationScheme, displayName, configureOptionsWithServices); + } + + public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) where TService : class { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, TwitterPostConfigureOptions>()); - return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); } } } diff --git a/src/Security/Authentication/WsFederation/src/WsFederationExtensions.cs b/src/Security/Authentication/WsFederation/src/WsFederationExtensions.cs index 47091d58d5..6a9ffad239 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationExtensions.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationExtensions.cs @@ -31,6 +31,16 @@ namespace Microsoft.Extensions.DependencyInjection public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, Action configureOptions) => builder.AddWsFederation(WsFederationDefaults.AuthenticationScheme, configureOptions); + /// + /// Registers the using the default authentication scheme, display name, and the given options configuration. + /// + /// TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly. + /// + /// A delegate that configures the . + /// + public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, Action configureOptions) where TService : class + => builder.AddWsFederation(WsFederationDefaults.AuthenticationScheme, configureOptions); + /// /// Registers the using the given authentication scheme, default display name, and the given options configuration. /// @@ -41,6 +51,17 @@ namespace Microsoft.Extensions.DependencyInjection public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) => builder.AddWsFederation(authenticationScheme, WsFederationDefaults.DisplayName, configureOptions); + /// + /// Registers the using the given authentication scheme, default display name, and the given options configuration. + /// + /// TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly. + /// + /// + /// A delegate that configures the . + /// + public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) where TService : class + => builder.AddWsFederation(authenticationScheme, WsFederationDefaults.DisplayName, configureOptions); + /// /// Registers the using the given authentication scheme, display name, and options configuration. /// @@ -50,9 +71,33 @@ namespace Microsoft.Extensions.DependencyInjection /// A delegate that configures the . /// public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + Action configureOptionsWithServices; + if (configureOptions == null) + { + configureOptionsWithServices = null; + } + else + { + configureOptionsWithServices = (options, _) => configureOptions(options); + } + + return builder.AddWsFederation(authenticationScheme, displayName, configureOptionsWithServices); + } + + /// + /// Registers the using the given authentication scheme, display name, and options configuration. + /// + /// TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly. + /// + /// + /// + /// A delegate that configures the . + /// + public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) where TService : class { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, WsFederationPostConfigureOptions>()); - return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); } } } 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/ref/Microsoft.AspNetCore.Server.IIS.netcoreapp.cs b/src/Servers/IIS/IIS/ref/Microsoft.AspNetCore.Server.IIS.netcoreapp.cs index 479bfe8e6c..a32e60734b 100644 --- a/src/Servers/IIS/IIS/ref/Microsoft.AspNetCore.Server.IIS.netcoreapp.cs +++ b/src/Servers/IIS/IIS/ref/Microsoft.AspNetCore.Server.IIS.netcoreapp.cs @@ -21,10 +21,11 @@ namespace Microsoft.AspNetCore.Hosting } namespace Microsoft.AspNetCore.Server.IIS { - public sealed partial class BadHttpRequestException : System.IO.IOException + [System.ObsoleteAttribute("Moved to Microsoft.AspNetCore.Http.BadHttpRequestException")] + public sealed partial class BadHttpRequestException : Microsoft.AspNetCore.Http.BadHttpRequestException { - internal BadHttpRequestException() { } - public int StatusCode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } + internal BadHttpRequestException() : base (default(string), default(int)) { } + public new int StatusCode { get { throw null; } } } public static partial class HttpContextExtensions { diff --git a/src/Servers/IIS/IIS/src/BadHttpRequestException.cs b/src/Servers/IIS/IIS/src/BadHttpRequestException.cs index 527692f89c..2abefae9e9 100644 --- a/src/Servers/IIS/IIS/src/BadHttpRequestException.cs +++ b/src/Servers/IIS/IIS/src/BadHttpRequestException.cs @@ -1,44 +1,24 @@ // 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.IO; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Server.IIS { - public sealed class BadHttpRequestException : IOException + [Obsolete("Moved to Microsoft.AspNetCore.Http.BadHttpRequestException")] + public sealed class BadHttpRequestException : Microsoft.AspNetCore.Http.BadHttpRequestException { - private BadHttpRequestException(string message, int statusCode, RequestRejectionReason reason) - : base(message) + internal BadHttpRequestException(string message, int statusCode, RequestRejectionReason reason) + : base(message, statusCode) { - StatusCode = statusCode; Reason = reason; } - public int StatusCode { get; } + public new int StatusCode { get => base.StatusCode; } internal RequestRejectionReason Reason { get; } - - internal static void Throw(RequestRejectionReason reason) - { - throw GetException(reason); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - internal static BadHttpRequestException GetException(RequestRejectionReason reason) - { - BadHttpRequestException ex; - switch (reason) - { - case RequestRejectionReason.RequestBodyTooLarge: - ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestBodyTooLarge, StatusCodes.Status413PayloadTooLarge, reason); - break; - default: - ex = new BadHttpRequestException(CoreStrings.BadRequest, StatusCodes.Status400BadRequest, reason); - break; - } - return ex; - } } } diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs index 5f831eb01a..60d098bc11 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs @@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core if (_consumedBytes > MaxRequestBodySize) { - BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); + IISBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); } var result = await _bodyInputPipe.Writer.FlushAsync(); diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs index 4f84786cf0..8a28ecc08a 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs @@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _unexpectedError(logger, className, methodName, ex); } - public static void ConnectionBadRequest(ILogger logger, string connectionId, BadHttpRequestException ex) + public static void ConnectionBadRequest(ILogger logger, string connectionId, Microsoft.AspNetCore.Http.BadHttpRequestException ex) { _connectionBadRequest(logger, connectionId, ex.Message, ex); } diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs index 74f0cbae6b..5103ae871d 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -26,6 +26,8 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.IIS.Core { + using BadHttpRequestException = Microsoft.AspNetCore.Http.BadHttpRequestException; + internal abstract partial class IISHttpContext : NativeRequestContext, IThreadPoolWorkItem, IDisposable { private const int MinAllocBufferSize = 2048; @@ -293,7 +295,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core if (RequestHeaders.ContentLength > MaxRequestBodySize) { - BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); + IISBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); } HasStartedConsumingRequestBody = true; diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs index 060f105233..cfdc20b225 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs @@ -11,6 +11,8 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.IIS.Core { + using BadHttpRequestException = Microsoft.AspNetCore.Http.BadHttpRequestException; + internal class IISHttpContextOfT : IISHttpContext { private readonly IHttpApplication _application; diff --git a/src/Servers/IIS/IIS/src/IISBadHttpRequestException.cs b/src/Servers/IIS/IIS/src/IISBadHttpRequestException.cs new file mode 100644 index 0000000000..3ee8ee42aa --- /dev/null +++ b/src/Servers/IIS/IIS/src/IISBadHttpRequestException.cs @@ -0,0 +1,36 @@ +// 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.IO; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Server.IIS +{ + internal static class IISBadHttpRequestException + { + internal static void Throw(RequestRejectionReason reason) + { + throw GetException(reason); + } + + [MethodImpl(MethodImplOptions.NoInlining)] +#pragma warning disable CS0618 // Type or member is obsolete + internal static BadHttpRequestException GetException(RequestRejectionReason reason) + { + BadHttpRequestException ex; + switch (reason) + { + case RequestRejectionReason.RequestBodyTooLarge: + ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestBodyTooLarge, StatusCodes.Status413PayloadTooLarge, reason); + break; + default: + ex = new BadHttpRequestException(CoreStrings.BadRequest, StatusCodes.Status400BadRequest, reason); + break; + } + return ex; + } +#pragma warning restore CS0618 // Type or member is obsolete + } +} 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/IIS.Tests/MaxRequestBodySizeTests.cs b/src/Servers/IIS/IIS/test/IIS.Tests/MaxRequestBodySizeTests.cs index 1f46ed1b4b..0eb92e4ec7 100644 --- a/src/Servers/IIS/IIS/test/IIS.Tests/MaxRequestBodySizeTests.cs +++ b/src/Servers/IIS/IIS/test/IIS.Tests/MaxRequestBodySizeTests.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Server.IIS.FunctionalTests; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; +using BadHttpRequestException = Microsoft.AspNetCore.Http.BadHttpRequestException; namespace IIS.Tests { @@ -24,6 +25,7 @@ namespace IIS.Tests var globalMaxRequestBodySize = 0x100000000; BadHttpRequestException exception = null; + using (var testServer = await TestServer.Create( async ctx => { @@ -60,6 +62,7 @@ namespace IIS.Tests var perRequestMaxRequestBodySize = 0x100; BadHttpRequestException exception = null; + using (var testServer = await TestServer.Create( async ctx => { @@ -71,6 +74,7 @@ namespace IIS.Tests await ctx.Request.Body.ReadAsync(new byte[2000]); } + catch (BadHttpRequestException ex) { exception = ex; @@ -266,6 +270,7 @@ namespace IIS.Tests var maxRequestSize = 0x1000; BadHttpRequestException exception = null; + using (var testServer = await TestServer.Create( async ctx => { @@ -307,13 +312,14 @@ namespace IIS.Tests { BadHttpRequestException requestRejectedEx1 = null; BadHttpRequestException requestRejectedEx2 = null; + using (var testServer = await TestServer.Create( async ctx => { var buffer = new byte[1]; - requestRejectedEx1 = await Assert.ThrowsAsync( + requestRejectedEx1 = await Assert.ThrowsAnyAsync( async () => await ctx.Request.Body.ReadAsync(buffer, 0, 1)); - requestRejectedEx2 = await Assert.ThrowsAsync( + requestRejectedEx2 = await Assert.ThrowsAnyAsync( async () => await ctx.Request.Body.ReadAsync(buffer, 0, 1)); throw requestRejectedEx2; }, LoggerFactory, new IISServerOptions { MaxRequestBodySize = 0 })) 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/IIS/IIS/test/testassets/InProcessNewShimWebSite/InProcessNewShimWebSite.csproj b/src/Servers/IIS/IIS/test/testassets/InProcessNewShimWebSite/InProcessNewShimWebSite.csproj index fabcb2938a..7e319b26df 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessNewShimWebSite/InProcessNewShimWebSite.csproj +++ b/src/Servers/IIS/IIS/test/testassets/InProcessNewShimWebSite/InProcessNewShimWebSite.csproj @@ -7,6 +7,7 @@ InProcessWebSite InProcessNewShimWebSite FORWARDCOMPAT + false 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..7de1a0907e 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 @@ -62,10 +62,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel } namespace Microsoft.AspNetCore.Server.Kestrel.Core { - public sealed partial class BadHttpRequestException : System.IO.IOException + [System.ObsoleteAttribute("Moved to Microsoft.AspNetCore.Http.BadHttpRequestException")] + public sealed partial class BadHttpRequestException : Microsoft.AspNetCore.Http.BadHttpRequestException { - internal BadHttpRequestException() { } - public int StatusCode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } + internal BadHttpRequestException() : base (default(string), default(int)) { } + public new int StatusCode { get { throw null; } } } public partial class Http2Limits { @@ -127,6 +128,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/BadHttpRequestException.cs b/src/Servers/Kestrel/Core/src/BadHttpRequestException.cs index 16f7ab0fce..d68eff9a45 100644 --- a/src/Servers/Kestrel/Core/src/BadHttpRequestException.cs +++ b/src/Servers/Kestrel/Core/src/BadHttpRequestException.cs @@ -1,26 +1,23 @@ // 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.Diagnostics; -using System.IO; -using System.Runtime.CompilerServices; -using Microsoft.AspNetCore.Http; +using System; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Server.Kestrel.Core { - public sealed class BadHttpRequestException : IOException + [Obsolete("Moved to Microsoft.AspNetCore.Http.BadHttpRequestException")] + public sealed class BadHttpRequestException : Microsoft.AspNetCore.Http.BadHttpRequestException { - private BadHttpRequestException(string message, int statusCode, RequestRejectionReason reason) + internal BadHttpRequestException(string message, int statusCode, RequestRejectionReason reason) : this(message, statusCode, reason, null) { } - private BadHttpRequestException(string message, int statusCode, RequestRejectionReason reason, HttpMethod? requiredMethod) - : base(message) + internal BadHttpRequestException(string message, int statusCode, RequestRejectionReason reason, HttpMethod? requiredMethod) + : base(message, statusCode) { - StatusCode = statusCode; Reason = reason; if (requiredMethod.HasValue) @@ -29,151 +26,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } } - public int StatusCode { get; } + public new int StatusCode { get => base.StatusCode; } internal StringValues AllowedHeader { get; } internal RequestRejectionReason Reason { get; } - - [StackTraceHidden] - internal static void Throw(RequestRejectionReason reason) - { - throw GetException(reason); - } - - [StackTraceHidden] - internal static void Throw(RequestRejectionReason reason, HttpMethod method) - => throw GetException(reason, method.ToString().ToUpperInvariant()); - - [MethodImpl(MethodImplOptions.NoInlining)] - internal static BadHttpRequestException GetException(RequestRejectionReason reason) - { - BadHttpRequestException ex; - switch (reason) - { - case RequestRejectionReason.InvalidRequestHeadersNoCRLF: - ex = new BadHttpRequestException(CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF, StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.InvalidRequestLine: - ex = new BadHttpRequestException(CoreStrings.BadRequest_InvalidRequestLine, StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.MalformedRequestInvalidHeaders: - ex = new BadHttpRequestException(CoreStrings.BadRequest_MalformedRequestInvalidHeaders, StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.MultipleContentLengths: - ex = new BadHttpRequestException(CoreStrings.BadRequest_MultipleContentLengths, StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.UnexpectedEndOfRequestContent: - ex = new BadHttpRequestException(CoreStrings.BadRequest_UnexpectedEndOfRequestContent, StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.BadChunkSuffix: - ex = new BadHttpRequestException(CoreStrings.BadRequest_BadChunkSuffix, StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.BadChunkSizeData: - ex = new BadHttpRequestException(CoreStrings.BadRequest_BadChunkSizeData, StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.ChunkedRequestIncomplete: - ex = new BadHttpRequestException(CoreStrings.BadRequest_ChunkedRequestIncomplete, StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.InvalidCharactersInHeaderName: - ex = new BadHttpRequestException(CoreStrings.BadRequest_InvalidCharactersInHeaderName, StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.RequestLineTooLong: - ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestLineTooLong, StatusCodes.Status414UriTooLong, reason); - break; - case RequestRejectionReason.HeadersExceedMaxTotalSize: - ex = new BadHttpRequestException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, StatusCodes.Status431RequestHeaderFieldsTooLarge, reason); - break; - case RequestRejectionReason.TooManyHeaders: - ex = new BadHttpRequestException(CoreStrings.BadRequest_TooManyHeaders, StatusCodes.Status431RequestHeaderFieldsTooLarge, reason); - break; - case RequestRejectionReason.RequestBodyTooLarge: - ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestBodyTooLarge, StatusCodes.Status413PayloadTooLarge, reason); - break; - case RequestRejectionReason.RequestHeadersTimeout: - ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestHeadersTimeout, StatusCodes.Status408RequestTimeout, reason); - break; - case RequestRejectionReason.RequestBodyTimeout: - ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestBodyTimeout, StatusCodes.Status408RequestTimeout, reason); - break; - case RequestRejectionReason.OptionsMethodRequired: - ex = new BadHttpRequestException(CoreStrings.BadRequest_MethodNotAllowed, StatusCodes.Status405MethodNotAllowed, reason, HttpMethod.Options); - break; - case RequestRejectionReason.ConnectMethodRequired: - ex = new BadHttpRequestException(CoreStrings.BadRequest_MethodNotAllowed, StatusCodes.Status405MethodNotAllowed, reason, HttpMethod.Connect); - break; - case RequestRejectionReason.MissingHostHeader: - ex = new BadHttpRequestException(CoreStrings.BadRequest_MissingHostHeader, StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.MultipleHostHeaders: - ex = new BadHttpRequestException(CoreStrings.BadRequest_MultipleHostHeaders, StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.InvalidHostHeader: - ex = new BadHttpRequestException(CoreStrings.BadRequest_InvalidHostHeader, StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.UpgradeRequestCannotHavePayload: - ex = new BadHttpRequestException(CoreStrings.BadRequest_UpgradeRequestCannotHavePayload, StatusCodes.Status400BadRequest, reason); - break; - default: - ex = new BadHttpRequestException(CoreStrings.BadRequest, StatusCodes.Status400BadRequest, reason); - break; - } - return ex; - } - - [StackTraceHidden] - internal static void Throw(RequestRejectionReason reason, string detail) - { - throw GetException(reason, detail); - } - - [StackTraceHidden] - internal static void Throw(RequestRejectionReason reason, StringValues detail) - { - throw GetException(reason, detail.ToString()); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - internal static BadHttpRequestException GetException(RequestRejectionReason reason, string detail) - { - BadHttpRequestException ex; - switch (reason) - { - case RequestRejectionReason.TlsOverHttpError: - ex = new BadHttpRequestException(CoreStrings.HttpParserTlsOverHttpError, StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.InvalidRequestLine: - ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(detail), StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.InvalidRequestTarget: - ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidRequestTarget_Detail(detail), StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.InvalidRequestHeader: - ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(detail), StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.InvalidContentLength: - ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidContentLength_Detail(detail), StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.UnrecognizedHTTPVersion: - ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_UnrecognizedHTTPVersion(detail), StatusCodes.Status505HttpVersionNotsupported, reason); - break; - case RequestRejectionReason.FinalTransferCodingNotChunked: - ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_FinalTransferCodingNotChunked(detail), StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.LengthRequired: - ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_LengthRequired(detail), StatusCodes.Status411LengthRequired, reason); - break; - case RequestRejectionReason.LengthRequiredHttp10: - ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_LengthRequiredHttp10(detail), StatusCodes.Status400BadRequest, reason); - break; - case RequestRejectionReason.InvalidHostHeader: - ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(detail), StatusCodes.Status400BadRequest, reason); - break; - default: - ex = new BadHttpRequestException(CoreStrings.BadRequest, StatusCodes.Status400BadRequest, reason); - break; - } - return ex; - } } } 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/Http1ChunkedEncodingMessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs index 9c87b3697e..2a2c01051d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs @@ -135,7 +135,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (_context.RequestTimedOut) { - BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTimeout); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTimeout); } var readableBuffer = result.Buffer; @@ -373,7 +373,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } // At this point, 10 bytes have been consumed which is enough to parse the max value "7FFFFFFF\r\n". - BadHttpRequestException.Throw(RequestRejectionReason.BadChunkSizeData); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.BadChunkSizeData); } private void ParseExtension(ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined) @@ -469,7 +469,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } else { - BadHttpRequestException.Throw(RequestRejectionReason.BadChunkSuffix); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.BadChunkSuffix); } } @@ -528,7 +528,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http throw new IOException(CoreStrings.BadRequest_BadChunkSizeData, ex); } - BadHttpRequestException.Throw(RequestRejectionReason.BadChunkSizeData); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.BadChunkSizeData); + return -1; // can't happen, but compiler complains } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index 832d48cdce..ef77a1f396 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -204,7 +204,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (!_parser.ParseRequestLine(new Http1ParsingHandler(this), trimmedBuffer, out consumed, out examined)) { // We read the maximum allowed but didn't complete the start line. - BadHttpRequestException.Throw(RequestRejectionReason.RequestLineTooLong); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestLineTooLong); } return true; @@ -261,7 +261,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (!result) { // We read the maximum allowed but didn't complete the headers. - BadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize); } TimeoutControl.CancelTimeout(); @@ -424,7 +424,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // requests (https://tools.ietf.org/html/rfc7231#section-4.3.6). if (method != HttpMethod.Connect) { - BadHttpRequestException.Throw(RequestRejectionReason.ConnectMethodRequired); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.ConnectMethodRequired); } // When making a CONNECT request to establish a tunnel through one or @@ -468,7 +468,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // OPTIONS request (https://tools.ietf.org/html/rfc7231#section-4.3.7). if (method != HttpMethod.Options) { - BadHttpRequestException.Throw(RequestRejectionReason.OptionsMethodRequired); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.OptionsMethodRequired); } RawTarget = Asterisk; @@ -555,11 +555,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return; } - BadHttpRequestException.Throw(RequestRejectionReason.MissingHostHeader); + + KestrelBadHttpRequestException.Throw(RequestRejectionReason.MissingHostHeader); } else if (hostCount > 1) { - BadHttpRequestException.Throw(RequestRejectionReason.MultipleHostHeaders); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.MultipleHostHeaders); } else if (_requestTargetForm != HttpRequestTarget.OriginForm) { @@ -568,7 +569,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } else if (!HttpUtilities.IsHostHeaderValid(hostText)) { - BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); } } @@ -578,7 +579,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (hostText != RawTarget) { - BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); } } else if (_requestTargetForm == HttpRequestTarget.AbsoluteForm) @@ -595,14 +596,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (!_absoluteRequestTarget.IsDefaultPort || hostText != _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port.ToString(CultureInfo.InvariantCulture)) { - BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); } } } if (!HttpUtilities.IsHostHeaderValid(hostText)) { - BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); } } @@ -655,7 +656,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (_requestProcessingStatus == RequestProcessingStatus.ParsingHeaders) { - BadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders); } throw; } @@ -672,10 +673,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http endConnection = true; return true; case RequestProcessingStatus.ParsingRequestLine: - BadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestLine); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestLine); break; case RequestProcessingStatus.ParsingHeaders: - BadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders); break; } } @@ -690,7 +691,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { // In this case, there is an ongoing request but the start line/header parsing has timed out, so send // a 408 response. - BadHttpRequestException.Throw(RequestRejectionReason.RequestHeadersTimeout); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestHeadersTimeout); } endConnection = false; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs index 7ef9a167a4..b0d1bf7846 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs @@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // which is unknown to StartTimingReadAsync. if (_context.RequestTimedOut) { - BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTimeout); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTimeout); } try @@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (_context.RequestTimedOut) { ResetReadingState(); - BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTimeout); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTimeout); } if (_readResult.IsCompleted) @@ -234,7 +234,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (_contentLength > _context.MaxRequestBodySize) { - BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs index c5993cd7cc..282e665db8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs @@ -11,6 +11,8 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { + using BadHttpRequestException = Microsoft.AspNetCore.Http.BadHttpRequestException; + internal abstract class Http1MessageBody : MessageBody { protected readonly Http1Connection _context; @@ -31,7 +33,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // closing the connection without a response as expected. _context.OnInputOrOutputCompleted(); - BadHttpRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent); } public abstract bool TryReadInternal(out ReadResult readResult); @@ -87,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http AdvanceTo(result.Buffer.End); } while (!result.IsCompleted); } - catch (BadHttpRequestException ex) + catch (Microsoft.AspNetCore.Http.BadHttpRequestException ex) { _context.SetBadRequestState(ex); } @@ -130,7 +132,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (headers.HeaderTransferEncoding.Count > 0 || (headers.ContentLength.HasValue && headers.ContentLength.Value != 0)) { - BadHttpRequestException.Throw(RequestRejectionReason.UpgradeRequestCannotHavePayload); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.UpgradeRequestCannotHavePayload); } context.OnTrailersComplete(); // No trailers for these. @@ -150,7 +152,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // status code and then close the connection. if (transferCoding != TransferCoding.Chunked) { - BadHttpRequestException.Throw(RequestRejectionReason.FinalTransferCodingNotChunked, transferEncoding); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.FinalTransferCodingNotChunked, transferEncoding); } // TODO may push more into the wrapper rather than just calling into the message body @@ -175,7 +177,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (context.Method == HttpMethod.Post || context.Method == HttpMethod.Put) { var requestRejectionReason = httpVersion == HttpVersion.Http11 ? RequestRejectionReason.LengthRequired : RequestRejectionReason.LengthRequiredHttp10; - BadHttpRequestException.Throw(requestRejectionReason, context.Method); + KestrelBadHttpRequestException.Throw(requestRejectionReason, context.Method); } context.OnTrailersComplete(); // No trailers for these. diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs index ff905ca079..d34dc047c2 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs @@ -11,6 +11,8 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { + using BadHttpRequestException = Microsoft.AspNetCore.Http.BadHttpRequestException; + public class HttpParser : IHttpParser where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler { private readonly bool _showErrorDetails; @@ -225,7 +227,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http Debug.Assert(readAhead == 0 || readAhead == 2); // Headers don't end in CRLF line. - BadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestHeadersNoCRLF); + + KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestHeadersNoCRLF); } var length = 0; @@ -451,7 +454,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http [MethodImpl(MethodImplOptions.NoInlining)] private unsafe BadHttpRequestException GetInvalidRequestException(RequestRejectionReason reason, byte* detail, int length) - => BadHttpRequestException.GetException( + => KestrelBadHttpRequestException.GetException( reason, _showErrorDetails ? new Span(detail, length).GetAsciiStringEscaped(Constants.MaxExceptionDetailSize) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 539c386efa..cfe97f0f1e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -26,6 +26,8 @@ using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { + using BadHttpRequestException = Microsoft.AspNetCore.Http.BadHttpRequestException; + internal abstract partial class HttpProtocol : IHttpResponseControl { private static readonly byte[] _bytesConnectionClose = Encoding.ASCII.GetBytes("\r\nConnection: close"); @@ -513,7 +515,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _requestHeadersParsed++; if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount) { - BadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); } HttpRequestHeaders.Append(name, value); @@ -525,7 +527,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _requestHeadersParsed++; if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount) { - BadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); } string key = name.GetHeaderName(); @@ -547,6 +549,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 +628,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 +646,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 +761,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); + } } } } @@ -1258,9 +1217,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { SetErrorResponseHeaders(ex.StatusCode); - if (!StringValues.IsNullOrEmpty(ex.AllowedHeader)) +#pragma warning disable CS0618 // Type or member is obsolete + if (ex is Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException kestrelEx && !StringValues.IsNullOrEmpty(kestrelEx.AllowedHeader)) +#pragma warning restore CS0618 // Type or member is obsolete { - HttpResponseHeaders.HeaderAllow = ex.AllowedHeader; + HttpResponseHeaders.HeaderAllow = kestrelEx.AllowedHeader; } } @@ -1314,7 +1275,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http [MethodImpl(MethodImplOptions.NoInlining)] private BadHttpRequestException GetInvalidRequestTargetException(Span target) - => BadHttpRequestException.GetException( + => KestrelBadHttpRequestException.GetException( RequestRejectionReason.InvalidRequestTarget, Log.IsEnabled(LogLevel.Information) ? target.GetAsciiStringEscaped(Constants.MaxExceptionDetailSize) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs index a5e78a4442..95f5c69e79 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs @@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (!HeaderUtilities.TryParseNonNegativeInt64(value, out var parsed)) { - BadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value); } return parsed; @@ -76,14 +76,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (_contentLength.HasValue) { - BadHttpRequestException.Throw(RequestRejectionReason.MultipleContentLengths); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.MultipleContentLengths); } if (!Utf8Parser.TryParse(value, out long parsed, out var consumed) || parsed < 0 || consumed != value.Length) { - BadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetRequestHeaderStringNonNullCharacters(UseLatin1)); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetRequestHeaderStringNonNullCharacters(UseLatin1)); } _contentLength = parsed; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs index e97712f0ae..4f55f3de74 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs @@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (_consumedBytes > _context.MaxRequestBodySize) { - BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); } } 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..3bc267e8c7 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, @@ -150,7 +150,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public void HandleRequestHeadersTimeout() { - Log.ConnectionBadRequest(ConnectionId, BadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout)); + Log.ConnectionBadRequest(ConnectionId, KestrelBadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout)); Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout)); } @@ -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) @@ -1188,7 +1197,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _currentHeadersStream.OnHeader(name, value); } } - catch (BadHttpRequestException bre) + catch (Microsoft.AspNetCore.Http.BadHttpRequestException bre) { throw new Http2ConnectionErrorException(bre.Message, Http2ErrorCode.PROTOCOL_ERROR); } 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/Http2MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2MessageBody.cs index dd62d69cbb..3d1dead1b9 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2MessageBody.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 // Note ContentLength or MaxRequestBodySize may be null if (_context.RequestHeaders.ContentLength > _context.MaxRequestBodySize) { - BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); } } 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/Internal/Http3/Http3ControlStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs index a75d8bc69a..0c89d21c0d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs @@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 public void HandleRequestHeadersTimeout() { - //Log.ConnectionBadRequest(ConnectionId, BadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout)); + //Log.ConnectionBadRequest(ConnectionId, KestrelBadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout)); Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout)); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3MessageBody.cs index 5af5f8df47..77efd60e7b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3MessageBody.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 // Note ContentLength or MaxRequestBodySize may be null if (_context.RequestHeaders.ContentLength > _context.MaxRequestBodySize) { - BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index 6518d00afd..576443150c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -143,7 +143,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 base.OnHeader(name, value); } } - catch (BadHttpRequestException bre) + catch (Microsoft.AspNetCore.Http.BadHttpRequestException bre) { throw new Http3StreamErrorException(bre.Message, Http3ErrorCode.ProtocolError); } @@ -293,7 +293,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 public void HandleRequestHeadersTimeout() { - Log.ConnectionBadRequest(ConnectionId, BadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout)); + Log.ConnectionBadRequest(ConnectionId, KestrelBadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout)); Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout), Http3ErrorCode.RequestRejected); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs index 70fa7f00b3..9e21228c6d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs @@ -107,7 +107,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure // in the string if (!StringUtilities.TryGetAsciiString((byte*)state.ToPointer(), output, buffer.Length)) { - BadHttpRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName); } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs index cd441ae3a2..54a8392fae 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs @@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure void NotAllConnectionsClosedGracefully(); - void ConnectionBadRequest(string connectionId, BadHttpRequestException ex); + void ConnectionBadRequest(string connectionId, Microsoft.AspNetCore.Http.BadHttpRequestException ex); void ApplicationError(string connectionId, string traceIdentifier, Exception ex); diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs index f89732a704..6edaf7b599 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs @@ -175,7 +175,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure _notAllConnectionsClosedGracefully(_logger, null); } - public virtual void ConnectionBadRequest(string connectionId, BadHttpRequestException ex) + public virtual void ConnectionBadRequest(string connectionId, Microsoft.AspNetCore.Http.BadHttpRequestException ex) { _connectionBadRequest(_logger, connectionId, ex.Message, ex); } diff --git a/src/Servers/Kestrel/Core/src/KestralBadHttpRequestException.cs b/src/Servers/Kestrel/Core/src/KestralBadHttpRequestException.cs new file mode 100644 index 0000000000..0921e41e77 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/KestralBadHttpRequestException.cs @@ -0,0 +1,159 @@ +// 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.Diagnostics; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core +{ + internal static class KestrelBadHttpRequestException + { + [StackTraceHidden] + internal static void Throw(RequestRejectionReason reason) + { + throw GetException(reason); + } + + [StackTraceHidden] + internal static void Throw(RequestRejectionReason reason, HttpMethod method) + => throw GetException(reason, method.ToString().ToUpperInvariant()); + + [MethodImpl(MethodImplOptions.NoInlining)] +#pragma warning disable CS0618 // Type or member is obsolete + internal static BadHttpRequestException GetException(RequestRejectionReason reason) + { + BadHttpRequestException ex; + switch (reason) + { + case RequestRejectionReason.InvalidRequestHeadersNoCRLF: + ex = new BadHttpRequestException(CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF, StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.InvalidRequestLine: + ex = new BadHttpRequestException(CoreStrings.BadRequest_InvalidRequestLine, StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.MalformedRequestInvalidHeaders: + ex = new BadHttpRequestException(CoreStrings.BadRequest_MalformedRequestInvalidHeaders, StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.MultipleContentLengths: + ex = new BadHttpRequestException(CoreStrings.BadRequest_MultipleContentLengths, StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.UnexpectedEndOfRequestContent: + ex = new BadHttpRequestException(CoreStrings.BadRequest_UnexpectedEndOfRequestContent, StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.BadChunkSuffix: + ex = new BadHttpRequestException(CoreStrings.BadRequest_BadChunkSuffix, StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.BadChunkSizeData: + ex = new BadHttpRequestException(CoreStrings.BadRequest_BadChunkSizeData, StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.ChunkedRequestIncomplete: + ex = new BadHttpRequestException(CoreStrings.BadRequest_ChunkedRequestIncomplete, StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.InvalidCharactersInHeaderName: + ex = new BadHttpRequestException(CoreStrings.BadRequest_InvalidCharactersInHeaderName, StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.RequestLineTooLong: + ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestLineTooLong, StatusCodes.Status414UriTooLong, reason); + break; + case RequestRejectionReason.HeadersExceedMaxTotalSize: + ex = new BadHttpRequestException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, StatusCodes.Status431RequestHeaderFieldsTooLarge, reason); + break; + case RequestRejectionReason.TooManyHeaders: + ex = new BadHttpRequestException(CoreStrings.BadRequest_TooManyHeaders, StatusCodes.Status431RequestHeaderFieldsTooLarge, reason); + break; + case RequestRejectionReason.RequestBodyTooLarge: + ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestBodyTooLarge, StatusCodes.Status413PayloadTooLarge, reason); + break; + case RequestRejectionReason.RequestHeadersTimeout: + ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestHeadersTimeout, StatusCodes.Status408RequestTimeout, reason); + break; + case RequestRejectionReason.RequestBodyTimeout: + ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestBodyTimeout, StatusCodes.Status408RequestTimeout, reason); + break; + case RequestRejectionReason.OptionsMethodRequired: + ex = new BadHttpRequestException(CoreStrings.BadRequest_MethodNotAllowed, StatusCodes.Status405MethodNotAllowed, reason, HttpMethod.Options); + break; + case RequestRejectionReason.ConnectMethodRequired: + ex = new BadHttpRequestException(CoreStrings.BadRequest_MethodNotAllowed, StatusCodes.Status405MethodNotAllowed, reason, HttpMethod.Connect); + break; + case RequestRejectionReason.MissingHostHeader: + ex = new BadHttpRequestException(CoreStrings.BadRequest_MissingHostHeader, StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.MultipleHostHeaders: + ex = new BadHttpRequestException(CoreStrings.BadRequest_MultipleHostHeaders, StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.InvalidHostHeader: + ex = new BadHttpRequestException(CoreStrings.BadRequest_InvalidHostHeader, StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.UpgradeRequestCannotHavePayload: + ex = new BadHttpRequestException(CoreStrings.BadRequest_UpgradeRequestCannotHavePayload, StatusCodes.Status400BadRequest, reason); + break; + default: + ex = new BadHttpRequestException(CoreStrings.BadRequest, StatusCodes.Status400BadRequest, reason); + break; + } + return ex; + } +#pragma warning restore CS0618 // Type or member is obsolete + + [StackTraceHidden] + internal static void Throw(RequestRejectionReason reason, string detail) + { + throw GetException(reason, detail); + } + + [StackTraceHidden] + internal static void Throw(RequestRejectionReason reason, StringValues detail) + { + throw GetException(reason, detail.ToString()); + } + +#pragma warning disable CS0618 // Type or member is obsolete + [MethodImpl(MethodImplOptions.NoInlining)] + internal static BadHttpRequestException GetException(RequestRejectionReason reason, string detail) + { + BadHttpRequestException ex; + switch (reason) + { + case RequestRejectionReason.TlsOverHttpError: + ex = new BadHttpRequestException(CoreStrings.HttpParserTlsOverHttpError, StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.InvalidRequestLine: + ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(detail), StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.InvalidRequestTarget: + ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidRequestTarget_Detail(detail), StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.InvalidRequestHeader: + ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(detail), StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.InvalidContentLength: + ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidContentLength_Detail(detail), StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.UnrecognizedHTTPVersion: + ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_UnrecognizedHTTPVersion(detail), StatusCodes.Status505HttpVersionNotsupported, reason); + break; + case RequestRejectionReason.FinalTransferCodingNotChunked: + ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_FinalTransferCodingNotChunked(detail), StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.LengthRequired: + ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_LengthRequired(detail), StatusCodes.Status411LengthRequired, reason); + break; + case RequestRejectionReason.LengthRequiredHttp10: + ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_LengthRequiredHttp10(detail), StatusCodes.Status400BadRequest, reason); + break; + case RequestRejectionReason.InvalidHostHeader: + ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(detail), StatusCodes.Status400BadRequest, reason); + break; + default: + ex = new BadHttpRequestException(CoreStrings.BadRequest, StatusCodes.Status400BadRequest, reason); + break; + } + return ex; + } +#pragma warning restore CS0618 // Type or member is obsolete + } +} 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/Http1ConnectionTests.cs b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs index 0992626a80..f60e3d9f91 100644 --- a/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs @@ -127,7 +127,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine}\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); +#pragma warning restore CS0618 // Type or member is obsolete _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, exception.Message); @@ -143,7 +145,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLines}\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); +#pragma warning restore CS0618 // Type or member is obsolete _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(CoreStrings.BadRequest_TooManyHeaders, exception.Message); @@ -438,7 +442,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(requestLineBytes); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); +#pragma warning restore CS0618 // Type or member is obsolete _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(CoreStrings.BadRequest_RequestLineTooLong, exception.Message); @@ -452,7 +458,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"GET {target} HTTP/1.1\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); @@ -466,7 +474,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"GET {target} HTTP/1.1\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); @@ -482,7 +492,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes(requestLine)); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); @@ -498,7 +510,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"GET {target} HTTP/1.1\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); @@ -514,7 +528,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes(requestLine)); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); @@ -529,7 +545,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes(requestLine)); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); @@ -774,7 +792,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"GET /%00 HTTP/1.1\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); @@ -934,7 +954,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { _http1Connection.HttpVersion = "HTTP/1.0"; _http1Connection.RequestHeaders[HeaderNames.Host] = "a=b"; +#pragma warning disable CS0618 // Type or member is obsolete var ex = Assert.Throws(() => _http1Connection.EnsureHostHeaderExists()); +#pragma warning restore CS0618 // Type or member is obsolete Assert.Equal(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("a=b"), ex.Message); } @@ -943,7 +965,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { _http1Connection.HttpVersion = "HTTP/1.1"; _http1Connection.RequestHeaders[HeaderNames.Host] = "a=b"; +#pragma warning disable CS0618 // Type or member is obsolete var ex = Assert.Throws(() => _http1Connection.EnsureHostHeaderExists()); +#pragma warning restore CS0618 // Type or member is obsolete Assert.Equal(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("a=b"), ex.Message); } 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/HttpParserTests.cs b/src/Servers/Kestrel/Core/test/HttpParserTests.cs index 1b2646d16b..b8029aa403 100644 --- a/src/Servers/Kestrel/Core/test/HttpParserTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpParserTests.cs @@ -90,11 +90,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(requestLine)); var requestHandler = new RequestHandler(); +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined)); Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(requestLine.EscapeNonPrintable()), exception.Message); - Assert.Equal(StatusCodes.Status400BadRequest, (exception as BadHttpRequestException).StatusCode); + Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); } [Theory] @@ -112,11 +114,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(requestLine)); var requestHandler = new RequestHandler(); +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined)); Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(method.EscapeNonPrintable() + @" / HTTP/1.1\x0D\x0A"), exception.Message); - Assert.Equal(StatusCodes.Status400BadRequest, (exception as BadHttpRequestException).StatusCode); + Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); } [Theory] @@ -134,11 +138,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(requestLine)); var requestHandler = new RequestHandler(); +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined)); Assert.Equal(CoreStrings.FormatBadRequest_UnrecognizedHTTPVersion(httpVersion), exception.Message); - Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, (exception as BadHttpRequestException).StatusCode); + Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, exception.StatusCode); } [Theory] @@ -328,7 +334,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(rawHeaders)); var requestHandler = new RequestHandler(); +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete { var reader = new SequenceReader(buffer); parser.ParseHeaders(requestHandler, ref reader); @@ -352,25 +360,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes("GET % HTTP/1.1\r\n")); var requestHandler = new RequestHandler(); +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined)); Assert.Equal("Invalid request line: ''", exception.Message); - Assert.Equal(StatusCodes.Status400BadRequest, (exception as BadHttpRequestException).StatusCode); + Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); // Unrecognized HTTP version buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes("GET / HTTP/1.2\r\n")); +#pragma warning disable CS0618 // Type or member is obsolete exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined)); Assert.Equal(CoreStrings.FormatBadRequest_UnrecognizedHTTPVersion(string.Empty), exception.Message); - Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, (exception as BadHttpRequestException).StatusCode); + Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, exception.StatusCode); // Invalid request header buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes("Header: value\n\r\n")); +#pragma warning disable CS0618 // Type or member is obsolete exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete { var reader = new SequenceReader(buffer); parser.ParseHeaders(requestHandler, ref reader); @@ -404,7 +418,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var requestHandler = new RequestHandler(); +#pragma warning disable CS0618 // Type or member is obsolete var badHttpRequestException = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete { parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined); }); diff --git a/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs b/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs index d87f1f814f..098f6fc0ed 100644 --- a/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs @@ -308,7 +308,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests const string key = "\u00141\u00F3d\017c"; var encoding = Encoding.GetEncoding("iso-8859-1"); +#pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws( +#pragma warning restore CS0618 // Type or member is obsolete () => headers.Append(encoding.GetBytes(key), Encoding.ASCII.GetBytes("value"))); Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); } 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/Core/test/MessageBodyTests.cs b/src/Servers/Kestrel/Core/test/MessageBodyTests.cs index b44caa62d4..5ea87c1b6c 100644 --- a/src/Servers/Kestrel/Core/test/MessageBodyTests.cs +++ b/src/Servers/Kestrel/Core/test/MessageBodyTests.cs @@ -221,7 +221,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests input.Add("g"); input.Add("g"); +#pragma warning disable CS0618 // Type or member is obsolete await Assert.ThrowsAsync(() => task); +#pragma warning restore CS0618 // Type or member is obsolete await body.StopAsync(); } @@ -247,7 +249,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests input.Add(i.ToString()); } +#pragma warning disable CS0618 // Type or member is obsolete await Assert.ThrowsAsync(() => task); +#pragma warning restore CS0618 // Type or member is obsolete await body.StopAsync(); } @@ -286,7 +290,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests input.Add("\r"); input.Add("n"); +#pragma warning disable CS0618 // Type or member is obsolete await Assert.ThrowsAsync(() => task); +#pragma warning restore CS0618 // Type or member is obsolete await body.StopAsync(); } @@ -388,7 +394,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests input.Add("012345678\r"); var buffer = new byte[1024]; +#pragma warning disable CS0618 // Type or member is obsolete var ex = await Assert.ThrowsAsync(async () => +#pragma warning restore CS0618 // Type or member is obsolete await stream.ReadAsync(buffer, 0, buffer.Length)); Assert.Equal(CoreStrings.BadRequest_BadChunkSizeData, ex.Message); @@ -536,7 +544,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { using (var input = new TestInput()) { +#pragma warning disable CS0618 // Type or member is obsolete var ex = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete Http1MessageBody.For(HttpVersion.Http11, new HttpRequestHeaders { HeaderTransferEncoding = "chunked, not-chunked" }, input.Http1Connection)); Assert.Equal(StatusCodes.Status400BadRequest, ex.StatusCode); @@ -553,7 +563,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { input.Http1Connection.Method = method; +#pragma warning disable CS0618 // Type or member is obsolete var ex = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete Http1MessageBody.For(HttpVersion.Http11, new HttpRequestHeaders(), input.Http1Connection)); Assert.Equal(StatusCodes.Status411LengthRequired, ex.StatusCode); @@ -570,7 +582,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { input.Http1Connection.Method = method; +#pragma warning disable CS0618 // Type or member is obsolete var ex = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete Http1MessageBody.For(HttpVersion.Http10, new HttpRequestHeaders(), input.Http1Connection)); Assert.Equal(StatusCodes.Status400BadRequest, ex.StatusCode); @@ -733,7 +747,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // Time out on the next read input.Http1Connection.SendTimeoutResponse(); +#pragma warning disable CS0618 // Type or member is obsolete var exception = await Assert.ThrowsAsync(async () => await body.ReadAsync()); +#pragma warning restore CS0618 // Type or member is obsolete Assert.Equal(StatusCodes.Status408RequestTimeout, exception.StatusCode); await body.StopAsync(); @@ -768,7 +784,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests mockLogger.Verify(logger => logger.ConnectionBadRequest( It.IsAny(), +#pragma warning disable CS0618 // Type or member is obsolete It.Is(ex => ex.Reason == RequestRejectionReason.RequestBodyTimeout))); +#pragma warning restore CS0618 // Type or member is obsolete await body.StopAsync(); } @@ -796,7 +814,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var ms = new MemoryStream()) { +#pragma warning disable CS0618 // Type or member is obsolete var exception = await Assert.ThrowsAsync(() => stream.CopyToAsync(ms)); +#pragma warning restore CS0618 // Type or member is obsolete Assert.Equal(StatusCodes.Status408RequestTimeout, exception.StatusCode); } @@ -1206,10 +1226,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests input.Application.Output.Complete(); +#pragma warning disable CS0618 // Type or member is obsolete var ex0 = Assert.Throws(() => reader.TryRead(out var readResult)); var ex1 = Assert.Throws(() => reader.TryRead(out var readResult)); var ex2 = await Assert.ThrowsAsync(() => reader.ReadAsync().AsTask()); var ex3 = await Assert.ThrowsAsync(() => reader.ReadAsync().AsTask()); +#pragma warning restore CS0618 // Type or member is obsolete Assert.Equal(RequestRejectionReason.UnexpectedEndOfRequestContent, ex0.Reason); Assert.Equal(RequestRejectionReason.UnexpectedEndOfRequestContent, ex1.Reason); @@ -1231,10 +1253,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests input.Application.Output.Complete(); +#pragma warning disable CS0618 // Type or member is obsolete var ex0 = Assert.Throws(() => reader.TryRead(out var readResult)); var ex1 = Assert.Throws(() => reader.TryRead(out var readResult)); var ex2 = await Assert.ThrowsAsync(() => reader.ReadAsync().AsTask()); var ex3 = await Assert.ThrowsAsync(() => reader.ReadAsync().AsTask()); +#pragma warning restore CS0618 // Type or member is obsolete Assert.Equal(RequestRejectionReason.UnexpectedEndOfRequestContent, ex0.Reason); Assert.Equal(RequestRejectionReason.UnexpectedEndOfRequestContent, ex1.Reason); 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/Mocks/MockTrace.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/MockTrace.cs index bd93636c83..76a5c9443d 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/MockTrace.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/MockTrace.cs @@ -4,7 +4,6 @@ using System; using System.Net.Http.HPack; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; @@ -16,7 +15,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance public void ApplicationError(string connectionId, string requestId, Exception ex) { } public IDisposable BeginScope(TState state) => null; public void ConnectionAccepted(string connectionId) { } - public void ConnectionBadRequest(string connectionId, BadHttpRequestException ex) { } + public void ConnectionBadRequest(string connectionId, Microsoft.AspNetCore.Http.BadHttpRequestException ex) { } public void ConnectionDisconnect(string connectionId) { } public void ConnectionError(string connectionId, Exception ex) { } public void ConnectionHeadResponseBodyWrite(string connectionId, long count) { } 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/samples/SampleApp/Startup.cs b/src/Servers/Kestrel/samples/SampleApp/Startup.cs index 6467f4468f..1e90bc1a52 100644 --- a/src/Servers/Kestrel/samples/SampleApp/Startup.cs +++ b/src/Servers/Kestrel/samples/SampleApp/Startup.cs @@ -36,7 +36,7 @@ namespace SampleApp { await next.Invoke(); } - catch (BadHttpRequestException ex) when (ex.StatusCode == StatusCodes.Status413RequestEntityTooLarge) { } + catch (Microsoft.AspNetCore.Http.BadHttpRequestException ex) when (ex.StatusCode == StatusCodes.Status413RequestEntityTooLarge) { } }); app.Run(async context => diff --git a/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs b/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs index 85a7643202..363da59c9d 100644 --- a/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs +++ b/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs @@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Testing _trace2.NotAllConnectionsClosedGracefully(); } - public void ConnectionBadRequest(string connectionId, BadHttpRequestException ex) + public void ConnectionBadRequest(string connectionId, Microsoft.AspNetCore.Http.BadHttpRequestException ex) { _trace1.ConnectionBadRequest(connectionId, ex); _trace2.ConnectionBadRequest(connectionId, ex); 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/BadHttpRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs index c45f773dee..f6cbc2f0a9 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Moq; using Xunit; +using BadHttpRequestException = Microsoft.AspNetCore.Http.BadHttpRequestException; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { @@ -191,6 +192,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests private async Task TestBadRequest(string request, string expectedResponseStatusCode, string expectedExceptionMessage, string expectedAllowHeader = null) { BadHttpRequestException loggedException = null; + var mockKestrelTrace = new Mock(); mockKestrelTrace .Setup(trace => trace.IsEnabled(LogLevel.Information)) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs index 72d7c2cc7c..d5b5eb3f21 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Xunit; +using BadHttpRequestException = Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { @@ -842,7 +843,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { var testContext = new TestServiceContext(LoggerFactory); var readStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); +#pragma warning disable CS0618 // Type or member is obsolete var exTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); +#pragma warning restore CS0618 // Type or member is obsolete await using (var server = new TestServer(async httpContext => { @@ -853,7 +856,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { await readTask; } +#pragma warning disable CS0618 // Type or member is obsolete catch (BadHttpRequestException badRequestEx) +#pragma warning restore CS0618 // Type or member is obsolete { exTcs.TrySetResult(badRequestEx); } 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..4e0dd12137 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); @@ -1569,7 +1569,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public async Task MaxRequestBodySize_ContentLengthOver_413() { +#pragma warning disable CS0618 // Type or member is obsolete BadHttpRequestException exception = null; +#pragma warning restore CS0618 // Type or member is obsolete _serviceContext.ServerOptions.Limits.MaxRequestBodySize = 10; var headers = new[] { @@ -1580,7 +1582,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }; await InitializeConnectionAsync(async context => { +#pragma warning disable CS0618 // Type or member is obsolete exception = await Assert.ThrowsAsync(async () => +#pragma warning restore CS0618 // Type or member is obsolete { var buffer = new byte[100]; while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { } @@ -1591,7 +1595,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 +1638,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); @@ -1651,7 +1655,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public async Task MaxRequestBodySize_NoContentLength_Over_413() { +#pragma warning disable CS0618 // Type or member is obsolete BadHttpRequestException exception = null; +#pragma warning restore CS0618 // Type or member is obsolete _serviceContext.ServerOptions.Limits.MaxRequestBodySize = 10; var headers = new[] { @@ -1661,7 +1667,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }; await InitializeConnectionAsync(async context => { +#pragma warning disable CS0618 // Type or member is obsolete exception = await Assert.ThrowsAsync(async () => +#pragma warning restore CS0618 // Type or member is obsolete { var buffer = new byte[100]; while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { } @@ -1674,7 +1682,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); @@ -1699,7 +1707,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [InlineData(false)] public async Task MaxRequestBodySize_AppCanLowerLimit(bool includeContentLength) { +#pragma warning disable CS0618 // Type or member is obsolete BadHttpRequestException exception = null; +#pragma warning restore CS0618 // Type or member is obsolete _serviceContext.ServerOptions.Limits.MaxRequestBodySize = 20; var headers = new[] { @@ -1718,7 +1728,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { Assert.False(context.Features.Get().IsReadOnly); context.Features.Get().MaxRequestBodySize = 17; +#pragma warning disable CS0618 // Type or member is obsolete exception = await Assert.ThrowsAsync(async () => +#pragma warning restore CS0618 // Type or member is obsolete { var buffer = new byte[100]; while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { } @@ -1733,7 +1745,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 +1800,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 +1826,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 +1864,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 +1895,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 +1906,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 +1942,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 +1992,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 +2051,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 +2086,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 +2136,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 +2201,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 +2247,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 +2281,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 +2544,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 +2635,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 +2683,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 +2712,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 +2755,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 +2801,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 +2847,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 +2896,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 +2949,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 +2999,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 +3049,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 +3092,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 +3138,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 +3180,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 +3225,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 +3291,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 +3337,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 +3373,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 +3425,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 +3477,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 +3510,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 +3543,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 +3595,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 +3651,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 +3717,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 +3773,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 +3838,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 +3897,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 +3953,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 +4015,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 +4089,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 +4165,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 +4236,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 +4308,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 +4392,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 +4473,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 +4560,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 +4620,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..de256d5304 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); @@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.RequestHeaders), Times.Once); - await WaitForConnectionErrorAsync( + await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: int.MaxValue, Http2ErrorCode.INTERNAL_ERROR, @@ -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/MaxRequestBodySizeTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs index ef5e141063..4e75c713e6 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; +using BadHttpRequestException = Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { @@ -22,12 +23,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { // 4 GiB var globalMaxRequestBodySize = 0x100000000; +#pragma warning disable CS0618 // Type or member is obsolete BadHttpRequestException requestRejectedEx = null; +#pragma warning restore CS0618 // Type or member is obsolete await using (var server = new TestServer(async context => { var buffer = new byte[1]; +#pragma warning disable CS0618 // Type or member is obsolete requestRejectedEx = await Assert.ThrowsAsync( +#pragma warning restore CS0618 // Type or member is obsolete async () => await context.Request.Body.ReadAsync(buffer, 0, 1)); throw requestRejectedEx; }, @@ -62,7 +67,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests var globalMaxRequestBodySize = 0x200000000; // 4 GiB var perRequestMaxRequestBodySize = 0x100000000; +#pragma warning disable CS0618 // Type or member is obsolete BadHttpRequestException requestRejectedEx = null; +#pragma warning restore CS0618 // Type or member is obsolete await using (var server = new TestServer(async context => { @@ -73,7 +80,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests feature.MaxRequestBodySize = perRequestMaxRequestBodySize; var buffer = new byte[1]; +#pragma warning disable CS0618 // Type or member is obsolete requestRejectedEx = await Assert.ThrowsAsync( +#pragma warning restore CS0618 // Type or member is obsolete async () => await context.Request.Body.ReadAsync(buffer, 0, 1)); throw requestRejectedEx; }, @@ -258,16 +267,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests [Fact] public async Task EveryReadFailsWhenContentLengthHeaderExceedsGlobalLimit() { +#pragma warning disable CS0618 // Type or member is obsolete BadHttpRequestException requestRejectedEx1 = null; BadHttpRequestException requestRejectedEx2 = null; +#pragma warning restore CS0618 // Type or member is obsolete await using (var server = new TestServer(async context => { var buffer = new byte[1]; +#pragma warning disable CS0618 // Type or member is obsolete requestRejectedEx1 = await Assert.ThrowsAsync( async () => await context.Request.Body.ReadAsync(buffer, 0, 1)); requestRejectedEx2 = await Assert.ThrowsAsync( async () => await context.Request.Body.ReadAsync(buffer, 0, 1)); +#pragma warning restore CS0618 // Type or member is obsolete throw requestRejectedEx2; }, new TestServiceContext(LoggerFactory) { ServerOptions = { Limits = { MaxRequestBodySize = 0 } } })) @@ -301,12 +314,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { var chunkedPayload = "5;random chunk extension\r\nHello\r\n6\r\n World\r\n0\r\n\r\n"; var globalMaxRequestBodySize = chunkedPayload.Length - 1; +#pragma warning disable CS0618 // Type or member is obsolete BadHttpRequestException requestRejectedEx = null; +#pragma warning restore CS0618 // Type or member is obsolete await using (var server = new TestServer(async context => { var buffer = new byte[11]; +#pragma warning disable CS0618 // Type or member is obsolete requestRejectedEx = await Assert.ThrowsAsync(async () => +#pragma warning restore CS0618 // Type or member is obsolete { var count = 0; do @@ -389,7 +406,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests var chunkedPayload = "5;random chunk extension\r\nHello\r\n6\r\n World\r\n0\r\n\r\n"; var globalMaxRequestBodySize = chunkedPayload.Length - 1; var firstRequest = true; +#pragma warning disable CS0618 // Type or member is obsolete BadHttpRequestException requestRejectedEx = null; +#pragma warning restore CS0618 // Type or member is obsolete await using (var server = new TestServer(async context => { @@ -411,7 +430,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests } else { +#pragma warning disable CS0618 // Type or member is obsolete requestRejectedEx = await Assert.ThrowsAsync(async () => +#pragma warning restore CS0618 // Type or member is obsolete { do { @@ -457,16 +478,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests [Fact] public async Task EveryReadFailsWhenChunkedPayloadExceedsGlobalLimit() { +#pragma warning disable CS0618 // Type or member is obsolete BadHttpRequestException requestRejectedEx1 = null; BadHttpRequestException requestRejectedEx2 = null; +#pragma warning restore CS0618 // Type or member is obsolete await using (var server = new TestServer(async context => { var buffer = new byte[1]; +#pragma warning disable CS0618 // Type or member is obsolete requestRejectedEx1 = await Assert.ThrowsAsync( async () => await context.Request.Body.ReadAsync(buffer, 0, 1)); requestRejectedEx2 = await Assert.ThrowsAsync( async () => await context.Request.Body.ReadAsync(buffer, 0, 1)); +#pragma warning restore CS0618 // Type or member is obsolete throw requestRejectedEx2; }, new TestServiceContext(LoggerFactory) { ServerOptions = { Limits = { MaxRequestBodySize = 0 } } })) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestBodyTimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestBodyTimeoutTests.cs index f96848bc47..93a0c7ff56 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestBodyTimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestBodyTimeoutTests.cs @@ -163,7 +163,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { await readTask; } - catch (BadHttpRequestException ex) when (ex.StatusCode == 408) + catch (Microsoft.AspNetCore.Http.BadHttpRequestException ex) when (ex.StatusCode == 408) { exceptionSwallowedTcs.SetResult(null); } 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/InMemory.FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs index 0dca63fe5f..4dd609f85b 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs @@ -24,6 +24,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Moq; using Xunit; +using BadHttpRequestException = Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { @@ -288,7 +289,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { await context.Request.Body.ReadAsync(new byte[1], 0, 1); } - catch (BadHttpRequestException) + catch (Microsoft.AspNetCore.Http.BadHttpRequestException) { } }, @@ -403,11 +404,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests [Fact] public async Task NoErrorResponseSentWhenAppSwallowsBadRequestException() { +#pragma warning disable CS0618 // Type or member is obsolete BadHttpRequestException readException = null; +#pragma warning restore CS0618 // Type or member is obsolete await using (var server = new TestServer(async httpContext => { +#pragma warning disable CS0618 // Type or member is obsolete readException = await Assert.ThrowsAsync( +#pragma warning restore CS0618 // Type or member is obsolete async () => await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1)); }, new TestServiceContext(LoggerFactory))) { @@ -430,8 +435,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Assert.NotNull(readException); +#pragma warning disable CS0618 // Type or member is obsolete Assert.Contains(TestSink.Writes, w => w.EventId.Id == 17 && w.LogLevel <= LogLevel.Debug && w.Exception is BadHttpRequestException && ((BadHttpRequestException)w.Exception).StatusCode == StatusCodes.Status400BadRequest); +#pragma warning restore CS0618 // Type or member is obsolete } [Fact] @@ -1265,7 +1272,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1); } - catch (BadHttpRequestException ex) + catch (Microsoft.AspNetCore.Http.BadHttpRequestException ex) { expectedResponse = ex.Message; httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; @@ -1749,8 +1756,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests } } +#pragma warning disable CS0618 // Type or member is obsolete Assert.Contains(TestApplicationErrorLogger.Messages, w => w.EventId.Id == 17 && w.LogLevel <= LogLevel.Debug && w.Exception is BadHttpRequestException && ((BadHttpRequestException)w.Exception).StatusCode == StatusCodes.Status400BadRequest); +#pragma warning restore CS0618 // Type or member is obsolete } [Fact] @@ -1801,8 +1810,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { while (TestApplicationErrorLogger.Messages.TryDequeue(out var message)) { +#pragma warning disable CS0618 // Type or member is obsolete if (message.EventId.Id == 17 && message.LogLevel <= LogLevel.Debug && message.Exception is BadHttpRequestException && ((BadHttpRequestException)message.Exception).StatusCode == StatusCodes.Status400BadRequest) +#pragma warning restore CS0618 // Type or member is obsolete { foundMessage = true; break; @@ -1862,8 +1873,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests } } +#pragma warning disable CS0618 // Type or member is obsolete Assert.Contains(TestApplicationErrorLogger.Messages, w => w.EventId.Id == 17 && w.LogLevel <= LogLevel.Debug && w.Exception is BadHttpRequestException && ((BadHttpRequestException)w.Exception).StatusCode == StatusCodes.Status400BadRequest); +#pragma warning restore CS0618 // Type or member is obsolete } [Fact] @@ -4117,8 +4130,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests if (sendMalformedRequest) { +#pragma warning disable CS0618 // Type or member is obsolete Assert.Contains(testSink.Writes, w => w.EventId.Id == 17 && w.LogLevel <= LogLevel.Debug && w.Exception is BadHttpRequestException && ((BadHttpRequestException)w.Exception).StatusCode == StatusCodes.Status400BadRequest); +#pragma warning restore CS0618 // Type or member is obsolete } else { 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/Shared/test/Shared.Tests/HostFactoryResolverTests.cs b/src/Shared/test/Shared.Tests/HostFactoryResolverTests.cs index a26fb7b133..3301687ee4 100644 --- a/src/Shared/test/Shared.Tests/HostFactoryResolverTests.cs +++ b/src/Shared/test/Shared.Tests/HostFactoryResolverTests.cs @@ -82,10 +82,10 @@ namespace Microsoft.Extensions.Hosting.Tests [Fact] public void CreateHostBuilderPattern_CanFindHostBuilder() { - var factory = HostFactoryResolver.ResolveHostBuilderFactory(typeof(CreateHostBuilderPatternTestSite.Program).Assembly); + var factory = HostFactoryResolver.ResolveHostBuilderFactory(typeof(CreateHostBuilderPatternTestSite.Program).Assembly); Assert.NotNull(factory); - Assert.IsAssignableFrom(factory(Array.Empty())); + Assert.IsAssignableFrom(factory(Array.Empty())); } [Fact] @@ -100,7 +100,7 @@ namespace Microsoft.Extensions.Hosting.Tests [Fact] public void CreateHostBuilderPattern__Invalid_CantFindHostBuilder() { - var factory = HostFactoryResolver.ResolveHostBuilderFactory(typeof(CreateHostBuilderInvalidSignature.Program).Assembly); + var factory = HostFactoryResolver.ResolveHostBuilderFactory(typeof(CreateHostBuilderInvalidSignature.Program).Assembly); Assert.Null(factory); } diff --git a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj index bc6eb225c1..c1db74970e 100644 --- a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj +++ b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) 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/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index 5651ba4622..6114e56a44 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests public ITestOutputHelper Output { get; } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6721")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public void EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates() { try @@ -124,7 +124,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6721")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public void EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates() { // Arrange @@ -154,8 +154,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString()); } - [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6721")] + [Fact] public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateIfVersionIsIncorrect() { _fixture.CleanupCertificates(); @@ -170,8 +169,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests Assert.Empty(httpsCertificateList); } - [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6721")] + [Fact] public void EnsureCreateHttpsCertificate_ReturnsExpiredCertificateForEmptyVersionField() { _fixture.CleanupCertificates(); @@ -188,7 +186,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6721")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public void EnsureCreateHttpsCertificate_ReturnsValidIfVersionIsZero() { _fixture.CleanupCertificates(); @@ -203,7 +201,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests } [ConditionalFact] - [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6721")] + [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] public void EnsureCreateHttpsCertificate_ReturnValidIfCertIsNewer() { _fixture.CleanupCertificates(); 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