diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml index 107fcdef9ff3..5e4e2e533b16 100644 --- a/.azure/pipelines/ci.yml +++ b/.azure/pipelines/ci.yml @@ -102,14 +102,14 @@ variables: - name: WindowsArm64InstallersLogArgs value: /bl:artifacts/log/Release/Build.Installers.Arm64.binlog - name: _InternalRuntimeDownloadArgs - value: -RuntimeSourceFeed https://dotnetbuilds.blob.core.windows.net/internal + value: -RuntimeSourceFeed https://ci.dot.net/internal -RuntimeSourceFeedKey $(dotnetbuilds-internal-container-read-token-base64) /p:DotNetAssetRootAccessTokenSuffix='$(dotnetbuilds-internal-container-read-token-base64)' # The code signing doesn't use the aspnet build scripts, so the msbuild parameters have to be passed directly. This # is awkward but necessary because the eng/common/ build scripts don't add the msbuild properties automatically. - name: _InternalRuntimeDownloadCodeSignArgs value: $(_InternalRuntimeDownloadArgs) - /p:DotNetRuntimeSourceFeed=https://dotnetbuilds.blob.core.windows.net/internal + /p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) - group: DotNet-HelixApi-Access - ${{ if notin(variables['Build.Reason'], 'PullRequest') }}: diff --git a/.azure/pipelines/jobs/default-build.yml b/.azure/pipelines/jobs/default-build.yml index fbe60b3e7964..eee89df3733a 100644 --- a/.azure/pipelines/jobs/default-build.yml +++ b/.azure/pipelines/jobs/default-build.yml @@ -109,10 +109,10 @@ jobs: vmImage: macOS-13 ${{ if eq(parameters.agentOs, 'Linux') }}: ${{ if eq(parameters.useHostedUbuntu, true) }}: - vmImage: ubuntu-20.04 + vmImage: ubuntu-22.04 ${{ if eq(parameters.useHostedUbuntu, false) }}: name: $(DncEngPublicBuildPool) - demands: ImageOverride -equals Build.Ubuntu.2004.Amd64.Open + demands: ImageOverride -equals Build.Ubuntu.2204.Amd64.Open ${{ if eq(parameters.agentOs, 'Windows') }}: name: $(DncEngPublicBuildPool) demands: ImageOverride -equals windows.vs2022preview.amd64.open @@ -324,7 +324,7 @@ jobs: os: macOS ${{ if eq(parameters.agentOs, 'Linux') }}: name: $(DncEngInternalBuildPool) - image: 1es-ubuntu-2004 + image: 1es-ubuntu-2204 os: linux ${{ if eq(parameters.agentOs, 'Windows') }}: name: $(DncEngInternalBuildPool) diff --git a/AspNetCore.sln b/AspNetCore.sln index 367d27911f8e..a16a5c63a6e9 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1784,6 +1784,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotReferencedInWasmCodePack EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.WasmRemoteAuthentication", "src\Components\test\testassets\Components.WasmRemoteAuthentication\Components.WasmRemoteAuthentication.csproj", "{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TlsFeaturesObserve", "src\Servers\HttpSys\samples\TlsFeaturesObserve\TlsFeaturesObserve.csproj", "{98C71EC8-1303-F55D-4032-E6728971770E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10753,6 +10755,22 @@ Global {8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x64.Build.0 = Release|Any CPU {8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x86.ActiveCfg = Release|Any CPU {8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x86.Build.0 = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|arm64.ActiveCfg = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|arm64.Build.0 = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|x64.ActiveCfg = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|x64.Build.0 = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|x86.ActiveCfg = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|x86.Build.0 = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|Any CPU.Build.0 = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|arm64.ActiveCfg = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|arm64.Build.0 = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|x64.ActiveCfg = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|x64.Build.0 = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|x86.ActiveCfg = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11634,6 +11652,7 @@ Global {F232B503-D412-45EE-8B31-EFD46B9FA302} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} {433F91E4-E39D-4EB0-B798-2998B3969A2C} = {6126DCE4-9692-4EE2-B240-C65743572995} {8A021D6D-7935-4AB3-BB47-38D4FF9B0D13} = {6126DCE4-9692-4EE2-B240-C65743572995} + {98C71EC8-1303-F55D-4032-E6728971770E} = {49016328-4D32-46E4-A4D2-94686ED38EA2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/NuGet.config b/NuGet.config index a972af091630..05d6d50db75d 100644 --- a/NuGet.config +++ b/NuGet.config @@ -6,10 +6,10 @@ - + - + @@ -30,10 +30,10 @@ - + - + diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props index af5cc98a0d47..af8b9b06b329 100644 --- a/eng/Baseline.Designer.props +++ b/eng/Baseline.Designer.props @@ -2,117 +2,117 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - + - + - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 @@ -120,138 +120,138 @@ - 8.0.13 + 8.0.19 - + - + - + - 8.0.13 + 8.0.19 - + - + - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - + - 8.0.13 + 8.0.19 - - + + - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - - + + - 8.0.13 + 8.0.19 - + - 8.0.13 + 8.0.19 - + - 8.0.13 + 8.0.19 - + - 8.0.13 + 8.0.19 - - + + - 8.0.13 + 8.0.19 - - - + + + - 8.0.13 + 8.0.19 - - + + - 8.0.13 + 8.0.19 - - + + - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - - + + @@ -259,7 +259,7 @@ - 8.0.13 + 8.0.19 @@ -268,51 +268,51 @@ - 8.0.13 + 8.0.19 - + - + - + - + - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - + - + - + - 8.0.13 + 8.0.19 - - + + @@ -322,8 +322,8 @@ - - + + @@ -331,8 +331,8 @@ - - + + @@ -343,58 +343,58 @@ - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - - + + - 8.0.13 + 8.0.19 - + - + - + - 8.0.13 + 8.0.19 - + - + - + - 8.0.13 + 8.0.19 - + - 8.0.13 + 8.0.19 @@ -403,7 +403,7 @@ - 8.0.13 + 8.0.19 @@ -411,71 +411,71 @@ - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - + - + - + - + - 8.0.13 + 8.0.19 - - + + - + - - + + - 8.0.13 + 8.0.19 - - + + - 8.0.13 + 8.0.19 - - + + - 8.0.13 + 8.0.19 @@ -491,27 +491,27 @@ - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - + - 8.0.13 + 8.0.19 @@ -520,23 +520,23 @@ - 8.0.13 + 8.0.19 - + - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 @@ -545,54 +545,54 @@ - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - - + + - - + + - - + + - 8.0.13 + 8.0.19 - - + + - - + + - - + + - - + + @@ -600,83 +600,83 @@ - 8.0.13 + 8.0.19 - + - + - + - + - + - 8.0.13 + 8.0.19 - + - + - + - 8.0.13 + 8.0.19 - + - + - + - 8.0.13 + 8.0.19 - + - + - + - 8.0.13 + 8.0.19 - - - - + + + + - 8.0.13 + 8.0.19 @@ -685,64 +685,64 @@ - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - + - 8.0.13 + 8.0.19 - + - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 @@ -764,7 +764,7 @@ - 8.0.13 + 8.0.19 @@ -786,7 +786,7 @@ - 8.0.13 + 8.0.19 @@ -802,23 +802,23 @@ - 8.0.13 + 8.0.19 - + - + - + @@ -826,24 +826,24 @@ - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - - - + + + - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 @@ -853,7 +853,7 @@ - 8.0.13 + 8.0.19 @@ -862,73 +862,73 @@ - 8.0.13 + 8.0.19 - + - + - + - 8.0.13 + 8.0.19 - + - + - + - 8.0.13 + 8.0.19 - + - + - + - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 @@ -957,11 +957,11 @@ - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 @@ -979,18 +979,18 @@ - 8.0.13 + 8.0.19 - 8.0.13 + 8.0.19 - + - 8.0.13 + 8.0.19 diff --git a/eng/Baseline.xml b/eng/Baseline.xml index 9efbb290ef60..5e40f13f2eca 100644 --- a/eng/Baseline.xml +++ b/eng/Baseline.xml @@ -4,110 +4,110 @@ This file contains a list of all the packages and their versions which were rele Update this list when preparing for a new patch. --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 176de8a62280..346a9afd7c06 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -9,37 +9,37 @@ --> - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0118cb6810a48869bf7494aabd86ef44da5940a3 + a947fe22902f3f0b921f5dafed9f059eaa4d18c6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0118cb6810a48869bf7494aabd86ef44da5940a3 + a947fe22902f3f0b921f5dafed9f059eaa4d18c6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0118cb6810a48869bf7494aabd86ef44da5940a3 + a947fe22902f3f0b921f5dafed9f059eaa4d18c6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0118cb6810a48869bf7494aabd86ef44da5940a3 + a947fe22902f3f0b921f5dafed9f059eaa4d18c6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0118cb6810a48869bf7494aabd86ef44da5940a3 + a947fe22902f3f0b921f5dafed9f059eaa4d18c6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0118cb6810a48869bf7494aabd86ef44da5940a3 + a947fe22902f3f0b921f5dafed9f059eaa4d18c6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0118cb6810a48869bf7494aabd86ef44da5940a3 + a947fe22902f3f0b921f5dafed9f059eaa4d18c6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0118cb6810a48869bf7494aabd86ef44da5940a3 + a947fe22902f3f0b921f5dafed9f059eaa4d18c6 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -121,9 +121,9 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 5535e31a712343a63f5d7d796cd874e563e5ac14 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 50c4cb9fc31c47f03eac865d7bc518af173b74b7 + 574100b692e71fa3426931adf4c1ba42e4ee5213 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -185,13 +185,13 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 5535e31a712343a63f5d7d796cd874e563e5ac14 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 50c4cb9fc31c47f03eac865d7bc518af173b74b7 + 574100b692e71fa3426931adf4c1ba42e4ee5213 - + https://github.com/dotnet/source-build-externals - dc30cd1ec22f198d658e011c14525d4d65873991 + 16bcad1c13be082bd52ce178896d1119a73081a9 @@ -211,9 +211,9 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 50c4cb9fc31c47f03eac865d7bc518af173b74b7 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 50c4cb9fc31c47f03eac865d7bc518af173b74b7 + 77545d6fd5ca79bc08198fd6d8037c14843f14ad https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -223,9 +223,9 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 81cabf2857a01351e5ab578947c7403a5b128ad1 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 05e0f2d2c881def48961d3b83fa11ae84df8e534 + 77545d6fd5ca79bc08198fd6d8037c14843f14ad https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -255,9 +255,9 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 5535e31a712343a63f5d7d796cd874e563e5ac14 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 81cabf2857a01351e5ab578947c7403a5b128ad1 + ef853a71052646a42abf17e888ec6d9a69614ad9 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -275,17 +275,17 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 81cabf2857a01351e5ab578947c7403a5b128ad1 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 50c4cb9fc31c47f03eac865d7bc518af173b74b7 + 574100b692e71fa3426931adf4c1ba42e4ee5213 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 50c4cb9fc31c47f03eac865d7bc518af173b74b7 + 574100b692e71fa3426931adf4c1ba42e4ee5213 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 50c4cb9fc31c47f03eac865d7bc518af173b74b7 + 574100b692e71fa3426931adf4c1ba42e4ee5213 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -316,31 +316,31 @@ Win-x64 is used here because we have picked an arbitrary runtime identifier to flow the version of the latest NETCore.App runtime. All Runtime.$rid packages should have the same version. --> - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 50c4cb9fc31c47f03eac865d7bc518af173b74b7 + 574100b692e71fa3426931adf4c1ba42e4ee5213 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 50c4cb9fc31c47f03eac865d7bc518af173b74b7 + 574100b692e71fa3426931adf4c1ba42e4ee5213 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 50c4cb9fc31c47f03eac865d7bc518af173b74b7 + 574100b692e71fa3426931adf4c1ba42e4ee5213 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 50c4cb9fc31c47f03eac865d7bc518af173b74b7 + 574100b692e71fa3426931adf4c1ba42e4ee5213 https://github.com/dotnet/xdt 9a1c3e1b7f0c8763d4c96e593961a61a72679a7b - + https://github.com/dotnet/source-build-reference-packages - d73fc552386797322e84fa9b2ef5eaa5369de83c + 3827e68e002064268d4be4b2d1d96048f9794808 @@ -368,42 +368,42 @@ - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 50c4cb9fc31c47f03eac865d7bc518af173b74b7 + 574100b692e71fa3426931adf4c1ba42e4ee5213 https://github.com/dotnet/winforms abda8e3bfa78319363526b5a5f86863ec979940e - + https://github.com/dotnet/arcade - a319ada170a54ee87c7a81e3309948e3d3ea7aca + a5ec3fab69ac440afe6764b2e046af6e8fcafa74 - + https://github.com/dotnet/arcade - a319ada170a54ee87c7a81e3309948e3d3ea7aca + a5ec3fab69ac440afe6764b2e046af6e8fcafa74 - + https://github.com/dotnet/arcade - a319ada170a54ee87c7a81e3309948e3d3ea7aca + a5ec3fab69ac440afe6764b2e046af6e8fcafa74 - + https://github.com/dotnet/arcade - a319ada170a54ee87c7a81e3309948e3d3ea7aca + a5ec3fab69ac440afe6764b2e046af6e8fcafa74 - + https://github.com/dotnet/arcade - a319ada170a54ee87c7a81e3309948e3d3ea7aca + a5ec3fab69ac440afe6764b2e046af6e8fcafa74 - + https://github.com/dotnet/extensions - ca03b0c72858567f9b668d90fee32ef2d5d8dd74 + eafdf6e9c40bcd561f38979617405fd2801a46e3 - + https://github.com/dotnet/extensions - ca03b0c72858567f9b668d90fee32ef2d5d8dd74 + eafdf6e9c40bcd561f38979617405fd2801a46e3 https://github.com/nuget/nuget.client diff --git a/eng/Versions.props b/eng/Versions.props index 5d05883df000..dfc636c88f04 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -8,10 +8,10 @@ 8 0 - 15 + 20 - false + true 7.1.2 7.* 8.0.2 - 8.0.15 - 8.0.15 - 8.0.15 - 8.0.15 - 8.0.15 - 8.0.15-servicing.25164.13 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20-servicing.25419.14 8.0.0 8.0.1 8.0.0 @@ -93,7 +93,7 @@ 8.0.0 8.0.0 8.0.0 - 8.0.15-servicing.25164.13 + 8.0.20-servicing.25419.14 8.0.1 8.0.1 8.0.1 @@ -109,15 +109,15 @@ 8.0.0 8.0.2 8.0.0 - 8.0.15-servicing.25164.13 + 8.0.20-servicing.25419.14 8.0.1 8.0.1 8.0.2 - 8.0.1 + 8.0.2 8.0.0-rtm.23520.14 8.0.0 8.0.1 - 8.0.2 + 8.0.3 8.0.1 8.0.0 8.0.1 @@ -125,13 +125,13 @@ 8.0.0 8.0.1 8.0.0 - 8.0.5 + 8.0.6 8.0.0 8.0.0 8.0.0 - 8.0.15-servicing.25164.13 + 8.0.20-servicing.25419.14 - 8.0.15-servicing.25164.13 + 8.0.20-servicing.25419.14 8.0.0 8.0.1 @@ -140,17 +140,17 @@ 8.0.0 8.0.1 - 8.1.0-preview.23604.1 - 8.1.0-preview.23604.1 + 9.0.0-preview.9.24518.1 + 9.0.0-preview.9.24518.1 - 8.0.15 - 8.0.15 - 8.0.15 - 8.0.15 - 8.0.15 - 8.0.15 - 8.0.15 - 8.0.15 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20 + 8.0.20 4.8.0-7.24574.2 4.8.0-7.24574.2 @@ -162,13 +162,13 @@ 6.2.4 6.2.4 - 8.0.0-beta.25111.4 - 8.0.0-beta.25111.4 - 8.0.0-beta.25111.4 + 8.0.0-beta.25407.1 + 8.0.0-beta.25407.1 + 8.0.0-beta.25407.1 - 8.0.0-alpha.1.25104.1 + 8.0.0-alpha.1.25202.2 - 8.0.0-alpha.1.25081.5 + 8.0.0-alpha.1.25316.2 2.0.0-beta-23228-03 @@ -200,7 +200,7 @@ 15.9.3032 6.0.1 - 8.0.5 + $(SystemTextJsonVersion) 4.7.0 5.0.0 diff --git a/eng/common/cross/toolchain.cmake b/eng/common/cross/toolchain.cmake index dafabdcaef00..f93dc440df0e 100644 --- a/eng/common/cross/toolchain.cmake +++ b/eng/common/cross/toolchain.cmake @@ -280,6 +280,8 @@ elseif(TARGET_ARCH_NAME MATCHES "^(arm64|x64)$") add_toolchain_linker_flag("-Wl,--rpath-link=${CROSS_ROOTFS}/usr/lib64") add_toolchain_linker_flag("-Wl,--rpath-link=${TIZEN_TOOLCHAIN_PATH}") endif() +elseif(TARGET_ARCH_NAME STREQUAL "s390x") + add_toolchain_linker_flag("--target=${TOOLCHAIN}") elseif(TARGET_ARCH_NAME STREQUAL "x86") if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/i586-alpine-linux-musl) add_toolchain_linker_flag("--target=${TOOLCHAIN}") @@ -327,6 +329,8 @@ if(TARGET_ARCH_NAME MATCHES "^(arm|armel)$") if(TARGET_ARCH_NAME STREQUAL "armel") add_compile_options(-mfloat-abi=softfp) endif() +elseif(TARGET_ARCH_NAME STREQUAL "s390x") + add_compile_options("--target=${TOOLCHAIN}") elseif(TARGET_ARCH_NAME STREQUAL "x86") if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/i586-alpine-linux-musl) add_compile_options(--target=${TOOLCHAIN}) diff --git a/eng/common/generate-sbom-prep.ps1 b/eng/common/generate-sbom-prep.ps1 index 3e5c1c74a1c5..a0c7d792a76f 100644 --- a/eng/common/generate-sbom-prep.ps1 +++ b/eng/common/generate-sbom-prep.ps1 @@ -4,18 +4,26 @@ Param( . $PSScriptRoot\pipeline-logging-functions.ps1 +# Normally - we'd listen to the manifest path given, but 1ES templates will overwrite if this level gets uploaded directly +# with their own overwriting ours. So we create it as a sub directory of the requested manifest path. +$ArtifactName = "${env:SYSTEM_STAGENAME}_${env:AGENT_JOBNAME}_SBOM" +$SafeArtifactName = $ArtifactName -replace '["/:<>\\|?@*"() ]', '_' +$SbomGenerationDir = Join-Path $ManifestDirPath $SafeArtifactName + +Write-Host "Artifact name before : $ArtifactName" +Write-Host "Artifact name after : $SafeArtifactName" + Write-Host "Creating dir $ManifestDirPath" + # create directory for sbom manifest to be placed -if (!(Test-Path -path $ManifestDirPath)) +if (!(Test-Path -path $SbomGenerationDir)) { - New-Item -ItemType Directory -path $ManifestDirPath - Write-Host "Successfully created directory $ManifestDirPath" + New-Item -ItemType Directory -path $SbomGenerationDir + Write-Host "Successfully created directory $SbomGenerationDir" } else{ Write-PipelineTelemetryError -category 'Build' "Unable to create sbom folder." } Write-Host "Updating artifact name" -$artifact_name = "${env:SYSTEM_STAGENAME}_${env:AGENT_JOBNAME}_SBOM" -replace '["/:<>\\|?@*"() ]', '_' -Write-Host "Artifact name $artifact_name" -Write-Host "##vso[task.setvariable variable=ARTIFACT_NAME]$artifact_name" +Write-Host "##vso[task.setvariable variable=ARTIFACT_NAME]$SafeArtifactName" diff --git a/eng/common/generate-sbom-prep.sh b/eng/common/generate-sbom-prep.sh index d5c76dc827b4..bbb4922151e6 100644 --- a/eng/common/generate-sbom-prep.sh +++ b/eng/common/generate-sbom-prep.sh @@ -14,19 +14,24 @@ done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" . $scriptroot/pipeline-logging-functions.sh +# replace all special characters with _, some builds use special characters like : in Agent.Jobname, that is not a permissible name while uploading artifacts. +artifact_name=$SYSTEM_STAGENAME"_"$AGENT_JOBNAME"_SBOM" +safe_artifact_name="${artifact_name//["/:<>\\|?@*$" ]/_}" + manifest_dir=$1 -if [ ! -d "$manifest_dir" ] ; then - mkdir -p "$manifest_dir" - echo "Sbom directory created." $manifest_dir +# Normally - we'd listen to the manifest path given, but 1ES templates will overwrite if this level gets uploaded directly +# with their own overwriting ours. So we create it as a sub directory of the requested manifest path. +sbom_generation_dir="$manifest_dir/$safe_artifact_name" + +if [ ! -d "$sbom_generation_dir" ] ; then + mkdir -p "$sbom_generation_dir" + echo "Sbom directory created." $sbom_generation_dir else Write-PipelineTelemetryError -category 'Build' "Unable to create sbom folder." fi -artifact_name=$SYSTEM_STAGENAME"_"$AGENT_JOBNAME"_SBOM" echo "Artifact name before : "$artifact_name -# replace all special characters with _, some builds use special characters like : in Agent.Jobname, that is not a permissible name while uploading artifacts. -safe_artifact_name="${artifact_name//["/:<>\\|?@*$" ]/_}" echo "Artifact name after : "$safe_artifact_name export ARTIFACT_NAME=$safe_artifact_name echo "##vso[task.setvariable variable=ARTIFACT_NAME]$safe_artifact_name" diff --git a/eng/common/sdl/packages.config b/eng/common/sdl/packages.config index 4585cfd6bba1..e5f543ea68c2 100644 --- a/eng/common/sdl/packages.config +++ b/eng/common/sdl/packages.config @@ -1,4 +1,4 @@ - + diff --git a/eng/common/templates-official/job/job.yml b/eng/common/templates-official/job/job.yml index 1f035fee73f4..b98f6a6505d3 100644 --- a/eng/common/templates-official/job/job.yml +++ b/eng/common/templates-official/job/job.yml @@ -21,6 +21,7 @@ parameters: # See schema documentation - https://github.com/dotnet/arcade/blob/master/Documentation/AzureDevOps/TemplateSchema.md artifacts: '' enableMicrobuild: false + microbuildUseESRP: true enablePublishBuildArtifacts: false enablePublishBuildAssets: false enablePublishTestResults: false @@ -38,6 +39,7 @@ parameters: enableSbom: true PackageVersion: 7.0.0 BuildDropPath: '$(Build.SourcesDirectory)/artifacts' + ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom jobs: - job: ${{ parameters.name }} @@ -134,6 +136,11 @@ jobs: signType: $(_SignType) zipSources: false feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + ${{ if eq(parameters.microbuildUseESRP, true) }}: + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea + ${{ else }}: + ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca env: TeamName: $(_TeamName) MicroBuildOutputFolderOverride: '$(Agent.TempDirectory)' @@ -261,4 +268,4 @@ jobs: targetPath: '$(Build.SourcesDirectory)\eng\common\BuildConfiguration' artifactName: 'BuildConfiguration' displayName: 'Publish build retry configuration' - continueOnError: true \ No newline at end of file + continueOnError: true diff --git a/eng/common/templates-official/job/source-build.yml b/eng/common/templates-official/job/source-build.yml index f983033bb028..4217d6d8b148 100644 --- a/eng/common/templates-official/job/source-build.yml +++ b/eng/common/templates-official/job/source-build.yml @@ -54,7 +54,7 @@ jobs: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')] - demands: ImageOverride -equals Build.Ubuntu.1804.Amd64.Open + demands: ImageOverride -equals Build.Ubuntu.2204.Amd64.Open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')] diff --git a/eng/common/templates-official/job/source-index-stage1.yml b/eng/common/templates-official/job/source-index-stage1.yml index 60dfb6b2d1c0..fb632b71a250 100644 --- a/eng/common/templates-official/job/source-index-stage1.yml +++ b/eng/common/templates-official/job/source-index-stage1.yml @@ -1,7 +1,7 @@ parameters: runAsPublic: false - sourceIndexUploadPackageVersion: 2.0.0-20240502.12 - sourceIndexProcessBinlogPackageVersion: 1.0.1-20240129.2 + sourceIndexUploadPackageVersion: 2.0.0-20250425.2 + sourceIndexProcessBinlogPackageVersion: 1.0.1-20250425.2 sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json sourceIndexBuildCommand: powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "eng/common/build.ps1 -restore -build -binarylog -ci" preSteps: [] diff --git a/eng/common/templates-official/jobs/source-build.yml b/eng/common/templates-official/jobs/source-build.yml index 5cf6a269c0b6..b9247be1547b 100644 --- a/eng/common/templates-official/jobs/source-build.yml +++ b/eng/common/templates-official/jobs/source-build.yml @@ -14,7 +14,7 @@ parameters: # This is the default platform provided by Arcade, intended for use by a managed-only repo. defaultManagedPlatform: name: 'Managed' - container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream8' + container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream-9-amd64' # Defines the platforms on which to run build jobs. One job is created for each platform, and the # object in this array is sent to the job template as 'platform'. If no platforms are specified, diff --git a/eng/common/templates-official/steps/generate-sbom.yml b/eng/common/templates-official/steps/generate-sbom.yml index 1bf43bf807af..daf0957b68d7 100644 --- a/eng/common/templates-official/steps/generate-sbom.yml +++ b/eng/common/templates-official/steps/generate-sbom.yml @@ -35,7 +35,7 @@ steps: PackageName: ${{ parameters.packageName }} BuildDropPath: ${{ parameters.buildDropPath }} PackageVersion: ${{ parameters.packageVersion }} - ManifestDirPath: ${{ parameters.manifestDirPath }} + ManifestDirPath: ${{ parameters.manifestDirPath }}/$(ARTIFACT_NAME) ${{ if ne(parameters.IgnoreDirectories, '') }}: AdditionalComponentDetectorArgs: '--IgnoreDirectories ${{ parameters.IgnoreDirectories }}' diff --git a/eng/common/templates-official/steps/send-to-helix.yml b/eng/common/templates-official/steps/send-to-helix.yml index 3eb7e2d5f840..22f2501307d4 100644 --- a/eng/common/templates-official/steps/send-to-helix.yml +++ b/eng/common/templates-official/steps/send-to-helix.yml @@ -8,6 +8,7 @@ parameters: HelixConfiguration: '' # optional -- additional property attached to a job HelixPreCommands: '' # optional -- commands to run before Helix work item execution HelixPostCommands: '' # optional -- commands to run after Helix work item execution + HelixProjectArguments: '' # optional -- arguments passed to the build command for helixpublish.proj WorkItemDirectory: '' # optional -- a payload directory to zip up and send to Helix; requires WorkItemCommand; incompatible with XUnitProjects WorkItemCommand: '' # optional -- a command to execute on the payload; requires WorkItemDirectory; incompatible with XUnitProjects WorkItemTimeout: '' # optional -- a timeout in TimeSpan.Parse-ready value (e.g. 00:02:00) for the work item command; requires WorkItemDirectory; incompatible with XUnitProjects @@ -24,12 +25,12 @@ parameters: IsExternal: false # [DEPRECATED] -- doesn't do anything, jobs are external if HelixAccessToken is empty and Creator is set HelixBaseUri: 'https://helix.dot.net/' # optional -- sets the Helix API base URI (allows targeting https://helix.int-dot.net ) Creator: '' # optional -- if the build is external, use this to specify who is sending the job - DisplayNamePrefix: 'Run Tests' # optional -- rename the beginning of the displayName of the steps in AzDO + DisplayNamePrefix: 'Run Tests' # optional -- rename the beginning of the displayName of the steps in AzDO condition: succeeded() # optional -- condition for step to execute; defaults to succeeded() continueOnError: false # optional -- determines whether to continue the build if the step errors; defaults to false steps: - - powershell: 'powershell "$env:BUILD_SOURCESDIRECTORY\eng\common\msbuild.ps1 $env:BUILD_SOURCESDIRECTORY\eng\common\helixpublish.proj /restore /p:TreatWarningsAsErrors=false /t:Test /bl:$env:BUILD_SOURCESDIRECTORY\artifacts\log\$env:BuildConfig\SendToHelix.binlog"' + - powershell: 'powershell "$env:BUILD_SOURCESDIRECTORY\eng\common\msbuild.ps1 $env:BUILD_SOURCESDIRECTORY\eng\common\helixpublish.proj ${{ parameters.HelixProjectArguments }} /restore /p:TreatWarningsAsErrors=false /t:Test /bl:$env:BUILD_SOURCESDIRECTORY\artifacts\log\$env:BuildConfig\SendToHelix.binlog"' displayName: ${{ parameters.DisplayNamePrefix }} (Windows) env: BuildConfig: $(_BuildConfig) @@ -59,7 +60,7 @@ steps: SYSTEM_ACCESSTOKEN: $(System.AccessToken) condition: and(${{ parameters.condition }}, eq(variables['Agent.Os'], 'Windows_NT')) continueOnError: ${{ parameters.continueOnError }} - - script: $BUILD_SOURCESDIRECTORY/eng/common/msbuild.sh $BUILD_SOURCESDIRECTORY/eng/common/helixpublish.proj /restore /p:TreatWarningsAsErrors=false /t:Test /bl:$BUILD_SOURCESDIRECTORY/artifacts/log/$BuildConfig/SendToHelix.binlog + - script: $BUILD_SOURCESDIRECTORY/eng/common/msbuild.sh $BUILD_SOURCESDIRECTORY/eng/common/helixpublish.proj ${{ parameters.HelixProjectArguments }} /restore /p:TreatWarningsAsErrors=false /t:Test /bl:$BUILD_SOURCESDIRECTORY/artifacts/log/$BuildConfig/SendToHelix.binlog displayName: ${{ parameters.DisplayNamePrefix }} (Unix) env: BuildConfig: $(_BuildConfig) diff --git a/eng/common/templates/job/job.yml b/eng/common/templates/job/job.yml index e295031c0985..2856f7de1061 100644 --- a/eng/common/templates/job/job.yml +++ b/eng/common/templates/job/job.yml @@ -134,6 +134,10 @@ jobs: signType: $(_SignType) zipSources: false feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea + ${{ else }}: + ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca env: TeamName: $(_TeamName) continueOnError: ${{ parameters.continueOnError }} diff --git a/eng/common/templates/job/source-build.yml b/eng/common/templates/job/source-build.yml index c0ff472b697b..c48f95d93d91 100644 --- a/eng/common/templates/job/source-build.yml +++ b/eng/common/templates/job/source-build.yml @@ -54,11 +54,11 @@ jobs: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')] - demands: ImageOverride -equals Build.Ubuntu.1804.Amd64.Open + demands: ImageOverride -equals Build.Ubuntu.2204.Amd64.Open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')] - demands: ImageOverride -equals Build.Ubuntu.1804.Amd64 + demands: ImageOverride -equals Build.Ubuntu.2204.Amd64 ${{ if ne(parameters.platform.pool, '') }}: pool: ${{ parameters.platform.pool }} diff --git a/eng/common/templates/job/source-index-stage1.yml b/eng/common/templates/job/source-index-stage1.yml index 0b6bb89dc78a..8538f44bab28 100644 --- a/eng/common/templates/job/source-index-stage1.yml +++ b/eng/common/templates/job/source-index-stage1.yml @@ -1,7 +1,7 @@ parameters: runAsPublic: false - sourceIndexUploadPackageVersion: 2.0.0-20240502.12 - sourceIndexProcessBinlogPackageVersion: 1.0.1-20240129.2 + sourceIndexUploadPackageVersion: 2.0.0-20250425.2 + sourceIndexProcessBinlogPackageVersion: 1.0.1-20250425.2 sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json sourceIndexBuildCommand: powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "eng/common/build.ps1 -restore -build -binarylog -ci" preSteps: [] diff --git a/eng/common/templates/jobs/source-build.yml b/eng/common/templates/jobs/source-build.yml index 5f46bfa895c1..3ec997108107 100644 --- a/eng/common/templates/jobs/source-build.yml +++ b/eng/common/templates/jobs/source-build.yml @@ -14,7 +14,7 @@ parameters: # This is the default platform provided by Arcade, intended for use by a managed-only repo. defaultManagedPlatform: name: 'Managed' - container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream8' + container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream-9-amd64' # Defines the platforms on which to run build jobs. One job is created for each platform, and the # object in this array is sent to the job template as 'platform'. If no platforms are specified, diff --git a/eng/common/templates/steps/send-to-helix.yml b/eng/common/templates/steps/send-to-helix.yml index 3eb7e2d5f840..22f2501307d4 100644 --- a/eng/common/templates/steps/send-to-helix.yml +++ b/eng/common/templates/steps/send-to-helix.yml @@ -8,6 +8,7 @@ parameters: HelixConfiguration: '' # optional -- additional property attached to a job HelixPreCommands: '' # optional -- commands to run before Helix work item execution HelixPostCommands: '' # optional -- commands to run after Helix work item execution + HelixProjectArguments: '' # optional -- arguments passed to the build command for helixpublish.proj WorkItemDirectory: '' # optional -- a payload directory to zip up and send to Helix; requires WorkItemCommand; incompatible with XUnitProjects WorkItemCommand: '' # optional -- a command to execute on the payload; requires WorkItemDirectory; incompatible with XUnitProjects WorkItemTimeout: '' # optional -- a timeout in TimeSpan.Parse-ready value (e.g. 00:02:00) for the work item command; requires WorkItemDirectory; incompatible with XUnitProjects @@ -24,12 +25,12 @@ parameters: IsExternal: false # [DEPRECATED] -- doesn't do anything, jobs are external if HelixAccessToken is empty and Creator is set HelixBaseUri: 'https://helix.dot.net/' # optional -- sets the Helix API base URI (allows targeting https://helix.int-dot.net ) Creator: '' # optional -- if the build is external, use this to specify who is sending the job - DisplayNamePrefix: 'Run Tests' # optional -- rename the beginning of the displayName of the steps in AzDO + DisplayNamePrefix: 'Run Tests' # optional -- rename the beginning of the displayName of the steps in AzDO condition: succeeded() # optional -- condition for step to execute; defaults to succeeded() continueOnError: false # optional -- determines whether to continue the build if the step errors; defaults to false steps: - - powershell: 'powershell "$env:BUILD_SOURCESDIRECTORY\eng\common\msbuild.ps1 $env:BUILD_SOURCESDIRECTORY\eng\common\helixpublish.proj /restore /p:TreatWarningsAsErrors=false /t:Test /bl:$env:BUILD_SOURCESDIRECTORY\artifacts\log\$env:BuildConfig\SendToHelix.binlog"' + - powershell: 'powershell "$env:BUILD_SOURCESDIRECTORY\eng\common\msbuild.ps1 $env:BUILD_SOURCESDIRECTORY\eng\common\helixpublish.proj ${{ parameters.HelixProjectArguments }} /restore /p:TreatWarningsAsErrors=false /t:Test /bl:$env:BUILD_SOURCESDIRECTORY\artifacts\log\$env:BuildConfig\SendToHelix.binlog"' displayName: ${{ parameters.DisplayNamePrefix }} (Windows) env: BuildConfig: $(_BuildConfig) @@ -59,7 +60,7 @@ steps: SYSTEM_ACCESSTOKEN: $(System.AccessToken) condition: and(${{ parameters.condition }}, eq(variables['Agent.Os'], 'Windows_NT')) continueOnError: ${{ parameters.continueOnError }} - - script: $BUILD_SOURCESDIRECTORY/eng/common/msbuild.sh $BUILD_SOURCESDIRECTORY/eng/common/helixpublish.proj /restore /p:TreatWarningsAsErrors=false /t:Test /bl:$BUILD_SOURCESDIRECTORY/artifacts/log/$BuildConfig/SendToHelix.binlog + - script: $BUILD_SOURCESDIRECTORY/eng/common/msbuild.sh $BUILD_SOURCESDIRECTORY/eng/common/helixpublish.proj ${{ parameters.HelixProjectArguments }} /restore /p:TreatWarningsAsErrors=false /t:Test /bl:$BUILD_SOURCESDIRECTORY/artifacts/log/$BuildConfig/SendToHelix.binlog displayName: ${{ parameters.DisplayNamePrefix }} (Unix) env: BuildConfig: $(_BuildConfig) diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index a00577ed17aa..bb048ad125a8 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -42,7 +42,7 @@ [bool]$useInstalledDotNetCli = if (Test-Path variable:useInstalledDotNetCli) { $useInstalledDotNetCli } else { $true } # Enable repos to use a particular version of the on-line dotnet-install scripts. -# default URL: https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.ps1 +# default URL: https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.ps1 [string]$dotnetInstallScriptVersion = if (Test-Path variable:dotnetInstallScriptVersion) { $dotnetInstallScriptVersion } else { 'v1' } # True to use global NuGet cache instead of restoring packages to repository-local directory. @@ -263,7 +263,7 @@ function GetDotNetInstallScript([string] $dotnetRoot) { if (!(Test-Path $installScript)) { Create-Directory $dotnetRoot $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit - $uri = "https://dotnet.microsoft.com/download/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.ps1" + $uri = "https://builds.dotnet.microsoft.com/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.ps1" Retry({ Write-Host "GET $uri" @@ -417,7 +417,7 @@ function InitializeVisualStudioMSBuild([bool]$install, [object]$vsRequirements = # Locate Visual Studio installation or download x-copy msbuild. $vsInfo = LocateVisualStudio $vsRequirements - if ($vsInfo -ne $null) { + if ($vsInfo -ne $null -and $env:ForceUseXCopyMSBuild -eq $null) { # Ensure vsInstallDir has a trailing slash $vsInstallDir = Join-Path $vsInfo.installationPath "\" $vsMajorVersion = $vsInfo.installationVersion.Split('.')[0] diff --git a/eng/common/tools.sh b/eng/common/tools.sh index b9b329ce37ff..68db15430230 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -54,7 +54,7 @@ warn_as_error=${warn_as_error:-true} use_installed_dotnet_cli=${use_installed_dotnet_cli:-true} # Enable repos to use a particular version of the on-line dotnet-install scripts. -# default URL: https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.sh +# default URL: https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.sh dotnetInstallScriptVersion=${dotnetInstallScriptVersion:-'v1'} # True to use global NuGet cache instead of restoring packages to repository-local directory. @@ -297,7 +297,7 @@ function with_retries { function GetDotNetInstallScript { local root=$1 local install_script="$root/dotnet-install.sh" - local install_script_url="https://dotnet.microsoft.com/download/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.sh" + local install_script_url="https://builds.dotnet.microsoft.com/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.sh" if [[ ! -a "$install_script" ]]; then mkdir -p "$root" diff --git a/eng/helix/helix.proj b/eng/helix/helix.proj index 99254551279f..9535e87a6042 100644 --- a/eng/helix/helix.proj +++ b/eng/helix/helix.proj @@ -57,13 +57,13 @@ runtime - - $([System.Environment]::GetEnvironmentVariable('DotNetBuildsInternalReadSasToken')) - $([System.Environment]::GetEnvironmentVariable('DotNetBuildsInternalReadSasToken')) diff --git a/eng/scripts/install-nginx-linux.sh b/eng/scripts/install-nginx-linux.sh index bbfb79c48203..f075a899d1cf 100755 --- a/eng/scripts/install-nginx-linux.sh +++ b/eng/scripts/install-nginx-linux.sh @@ -6,7 +6,7 @@ scriptroot="$( cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" reporoot="$(dirname "$(dirname "$scriptroot")")" nginxinstall="$reporoot/.tools/nginx" -curl -sSL http://nginx.org/download/nginx-1.14.2.tar.gz --retry 5 | tar zxfv - -C /tmp && cd /tmp/nginx-1.14.2/ +curl -sSL http://nginx.org/download/nginx-1.26.3.tar.gz --retry 5 | tar zxfv - -C /tmp && cd /tmp/nginx-1.26.3/ ./configure --prefix=$nginxinstall --with-http_ssl_module --without-http_rewrite_module make make install diff --git a/eng/targets/Helix.Common.props b/eng/targets/Helix.Common.props index 8a0fdf3481d3..1ba8ba99dbe3 100644 --- a/eng/targets/Helix.Common.props +++ b/eng/targets/Helix.Common.props @@ -2,7 +2,7 @@ (AlmaLinux.8.Amd64.Open)Ubuntu.2204.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:almalinux-8-helix-amd64 - (Alpine.318.Amd64.Open)Ubuntu.2204.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:alpine-3.18-helix-amd64 + (Alpine.321.Amd64.Open)azurelinux.3.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:alpine-3.21-helix-amd64 (Debian.12.Amd64.Open)Ubuntu.2204.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-12-helix-amd64 (Fedora.41.Amd64.Open)Ubuntu.2204.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-41-helix (Mariner)Ubuntu.2204.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:cbl-mariner-2.0-helix-amd64 @@ -42,7 +42,7 @@ - + diff --git a/eng/targets/Helix.targets b/eng/targets/Helix.targets index 70e01877befa..0aab28ef20cc 100644 --- a/eng/targets/Helix.targets +++ b/eng/targets/Helix.targets @@ -17,7 +17,7 @@ $(HelixQueueAlmaLinux8); - $(HelixQueueAlpine318); + $(HelixQueueAlpine); $(HelixQueueDebian12); $(HelixQueueFedora40); $(HelixQueueMariner); diff --git a/global.json b/global.json index 9d3930333424..eb59ddf5a90c 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "8.0.113" + "version": "8.0.119" }, "tools": { - "dotnet": "8.0.113", + "dotnet": "8.0.119", "runtimes": { "dotnet/x86": [ "$(MicrosoftNETCoreBrowserDebugHostTransportVersion)" @@ -25,8 +25,8 @@ }, "msbuild-sdks": { "Yarn.MSBuild": "1.22.19", - "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.25111.4", - "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.25111.4" + "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.25407.1", + "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.25407.1" }, "native-tools": { "jdk": "latest" diff --git a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs index 83592f562e1b..b8171c827e41 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs @@ -1287,7 +1287,7 @@ public void CanBindToFormWithFiles() } [Theory] - [InlineData(true)] + // [InlineData(true)] QuarantinedTest: https://github.com/dotnet/aspnetcore/issues/61882 [InlineData(false)] public void CanUseFormWithMethodGet(bool suppressEnhancedNavigation) { diff --git a/src/Framework/App.Runtime/src/Microsoft.AspNetCore.App.Runtime.csproj b/src/Framework/App.Runtime/src/Microsoft.AspNetCore.App.Runtime.csproj index fadf9de6547c..1f8527329959 100644 --- a/src/Framework/App.Runtime/src/Microsoft.AspNetCore.App.Runtime.csproj +++ b/src/Framework/App.Runtime/src/Microsoft.AspNetCore.App.Runtime.csproj @@ -560,7 +560,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant - + diff --git a/src/Http/Headers/test/CookieHeaderValueTest.cs b/src/Http/Headers/test/CookieHeaderValueTest.cs index 6623a8ed13dd..6ad2e962d005 100644 --- a/src/Http/Headers/test/CookieHeaderValueTest.cs +++ b/src/Http/Headers/test/CookieHeaderValueTest.cs @@ -75,7 +75,7 @@ public static TheoryData InvalidCookieValues } } - public static TheoryData, string?[]> ListOfCookieHeaderDataSet + public static TheoryData, string?[]> ListOfStrictCookieHeaderDataSet { get { @@ -94,19 +94,30 @@ public static TheoryData InvalidCookieValues dataset.Add(new[] { header1 }.ToList(), new[] { string1 }); dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 }); - dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ";", " , ", string1 }); dataset.Add(new[] { header2 }.ToList(), new[] { string2 }); dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1, string2 }); - dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1 + ", " + string2 }); dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + "; " + string1 }); dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) }); dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(";", string1, string2, string3, string4) }); return dataset; } } + public static TheoryData, string?[]> ListOfCookieHeaderDataSet + { + get + { + var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3"); + var string1 = "name1=n1=v1&n2=v2&n3=v3"; + + var dataset = new TheoryData, string?[]>(); + dataset.Concat(ListOfStrictCookieHeaderDataSet); + dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ";", " , ", string1 }); + return dataset; + } + } + public static TheoryData?, string?[]> ListWithInvalidCookieHeaderDataSet { get @@ -127,18 +138,19 @@ public static TheoryData InvalidCookieValues dataset.Add(new[] { header1 }.ToList(), new[] { validString1, invalidString1 }); dataset.Add(new[] { header1 }.ToList(), new[] { validString1, null, "", " ", ";", " , ", invalidString1 }); dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1, null, "", " ", ";", " , ", validString1 }); - dataset.Add(new[] { header1 }.ToList(), new[] { validString1 + ", " + invalidString1 }); - dataset.Add(new[] { header2 }.ToList(), new[] { invalidString1 + ", " + validString2 }); + dataset.Add(null, new[] { validString1 + ", " }); + dataset.Add(null, new[] { invalidString1 + ", " + validString2 }); dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1 + "; " + validString1 }); dataset.Add(new[] { header2 }.ToList(), new[] { validString2 + "; " + invalidString1 }); dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { invalidString1, validString1, validString2, validString3 }); dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, invalidString1, validString2, validString3 }); dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, validString2, invalidString1, validString3 }); dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, validString2, validString3, invalidString1 }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", invalidString1, validString1, validString2, validString3) }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, invalidString1, validString2, validString3) }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, validString2, invalidString1, validString3) }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, validString2, validString3, invalidString1) }); + dataset.Add(null, new[] { string.Join(",", invalidString1, validString1, validString2, validString3) }); + dataset.Add(null, new[] { string.Join(",", validString1, invalidString1, validString2, validString3) }); + dataset.Add(null, new[] { string.Join(",", validString1, validString2, invalidString1, validString3) }); + dataset.Add(null, new[] { string.Join(",", validString1, validString2, validString3, invalidString1) }); + dataset.Add(null, new[] { string.Join(",", validString1, validString2, validString3) }); dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", invalidString1, validString1, validString2, validString3) }); dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, invalidString1, validString2, validString3) }); dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, validString2, invalidString1, validString3) }); @@ -248,7 +260,7 @@ public void CookieHeaderValue_ParseList_AcceptsValidValues(IList cookies, string[] input) { var results = CookieHeaderValue.ParseStrictList(input); @@ -267,7 +279,7 @@ public void CookieHeaderValue_TryParseList_AcceptsValidValues(IList cookies, string[] input) { var result = CookieHeaderValue.TryParseStrictList(input, out var results); diff --git a/src/Http/Http/test/RequestCookiesCollectionTests.cs b/src/Http/Http/test/RequestCookiesCollectionTests.cs index fa3fb6d67f74..4efca2c3c2d6 100644 --- a/src/Http/Http/test/RequestCookiesCollectionTests.cs +++ b/src/Http/Http/test/RequestCookiesCollectionTests.cs @@ -33,15 +33,22 @@ public void ParseManyCookies() [Theory] [InlineData(",", null)] [InlineData(";", null)] - [InlineData("er=dd,cc,bb", new[] { "dd" })] - [InlineData("er=dd,err=cc,errr=bb", new[] { "dd", "cc", "bb" })] - [InlineData("errorcookie=dd,:(\"sa;", new[] { "dd" })] + [InlineData("er=dd,cc,bb", null)] + [InlineData("er=dd,err=cc,errr=bb", null)] + [InlineData("errorcookie=dd,:(\"sa;", null)] [InlineData("s;", null)] + [InlineData("a@a=a;", null)] + [InlineData("a@ a=a;", null)] + [InlineData("a a=a;", null)] + [InlineData(",a=a;", null)] + [InlineData(",a=a", null)] + [InlineData("a=a;,b=b", new []{ "a" })] // valid cookie followed by invalid cookie + [InlineData(",a=a;b=b", new[] { "b" })] // invalid cookie followed by valid cookie public void ParseInvalidCookies(string cookieToParse, string[] expectedCookieValues) { var cookies = RequestCookieCollection.Parse(new StringValues(new[] { cookieToParse })); - if(expectedCookieValues == null) + if (expectedCookieValues == null) { Assert.Equal(0, cookies.Count); return; diff --git a/src/Http/Shared/CookieHeaderParserShared.cs b/src/Http/Shared/CookieHeaderParserShared.cs index e558ec1e4dc4..32f2e9b33b90 100644 --- a/src/Http/Shared/CookieHeaderParserShared.cs +++ b/src/Http/Shared/CookieHeaderParserShared.cs @@ -83,6 +83,17 @@ public static bool TryParseValue(StringSegment value, ref int index, bool suppor if (!TryGetCookieLength(value, ref current, out parsedName, out parsedValue)) { + var separatorIndex = value.IndexOf(';', current); + if (separatorIndex > 0) + { + // Skip the invalid values and keep trying. + index = separatorIndex; + } + else + { + // No more separators, so we're done. + index = value.Length; + } return false; } @@ -91,6 +102,17 @@ public static bool TryParseValue(StringSegment value, ref int index, bool suppor // If we support multiple values and we've not reached the end of the string, then we must have a separator. if ((separatorFound && !supportsMultipleValues) || (!separatorFound && (current < value.Length))) { + var separatorIndex = value.IndexOf(';', current); + if (separatorIndex > 0) + { + // Skip the invalid values and keep trying. + index = separatorIndex; + } + else + { + // No more separators, so we're done. + index = value.Length; + } return false; } @@ -106,7 +128,7 @@ private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int sta separatorFound = false; var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); - if ((current == input.Length) || (input[current] != ',' && input[current] != ';')) + if (current == input.Length || input[current] != ';') { return current; } @@ -119,8 +141,8 @@ private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int sta if (skipEmptyValues) { - // Most headers only split on ',', but cookies primarily split on ';' - while ((current < input.Length) && ((input[current] == ',') || (input[current] == ';'))) + // Cookies are split on ';' + while (current < input.Length && input[current] == ';') { current++; // skip delimiter. current = current + HttpRuleParser.GetWhitespaceLength(input, current); @@ -130,6 +152,18 @@ private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int sta return current; } + /* + * https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1 + * cookie-pair = cookie-name "=" cookie-value + * cookie-name = token + * token = 1* + separators = "(" | ")" | "<" | ">" | "@" + | "," | ";" | ":" | "\" | <"> + | "/" | "[" | "]" | "?" | "=" + | "{" | "}" | SP | HT + CTL = + */ // name=value; name="value" internal static bool TryGetCookieLength(StringSegment input, ref int offset, [NotNullWhen(true)] out StringSegment? parsedName, [NotNullWhen(true)] out StringSegment? parsedValue) { diff --git a/src/Identity/EntityFrameworkCore/test/EF.Test/UserStoreTest.cs b/src/Identity/EntityFrameworkCore/test/EF.Test/UserStoreTest.cs index 22ab9e8be5f9..4f0e347ae937 100644 --- a/src/Identity/EntityFrameworkCore/test/EF.Test/UserStoreTest.cs +++ b/src/Identity/EntityFrameworkCore/test/EF.Test/UserStoreTest.cs @@ -144,6 +144,9 @@ await Assert.ThrowsAsync("user", await Assert.ThrowsAsync("user", async () => await store.GetTwoFactorEnabledAsync(null)); await Assert.ThrowsAsync("user", async () => await store.SetTwoFactorEnabledAsync(null, true)); + await Assert.ThrowsAsync("user", async () => await store.RedeemCodeAsync(user: null, code: "fake", default)); + await Assert.ThrowsAsync("code", async () => await store.RedeemCodeAsync(new IdentityUser("fake"), code: null, default)); + await Assert.ThrowsAsync("code", async () => await store.RedeemCodeAsync(new IdentityUser("fake"), code: "", default)); await Assert.ThrowsAsync("user", async () => await store.GetAccessFailedCountAsync(null)); await Assert.ThrowsAsync("user", async () => await store.GetLockoutEnabledAsync(null)); await Assert.ThrowsAsync("user", async () => await store.SetLockoutEnabledAsync(null, false)); diff --git a/src/Identity/Extensions.Stores/src/UserStoreBase.cs b/src/Identity/Extensions.Stores/src/UserStoreBase.cs index c45dd197e4a2..804ebcbad7dc 100644 --- a/src/Identity/Extensions.Stores/src/UserStoreBase.cs +++ b/src/Identity/Extensions.Stores/src/UserStoreBase.cs @@ -969,7 +969,7 @@ public virtual async Task RedeemCodeAsync(TUser user, string code, Cancell ThrowIfDisposed(); ArgumentNullThrowHelper.ThrowIfNull(user); - ArgumentNullThrowHelper.ThrowIfNull(code); + ArgumentNullThrowHelper.ThrowIfNullOrEmpty(code); var mergedCodes = await GetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, cancellationToken).ConfigureAwait(false) ?? ""; var splitCodes = mergedCodes.Split(';'); diff --git a/src/Installers/Windows/WindowsHostingBundle/Product.targets b/src/Installers/Windows/WindowsHostingBundle/Product.targets index 3e805f35bb3a..c1dc097445d4 100644 --- a/src/Installers/Windows/WindowsHostingBundle/Product.targets +++ b/src/Installers/Windows/WindowsHostingBundle/Product.targets @@ -83,7 +83,7 @@ --> - + diff --git a/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs b/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs index 178a12012a40..be5e4da152bd 100644 --- a/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs +++ b/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs @@ -21,6 +21,7 @@ public class ForwardedHeadersMiddleware private readonly ForwardedHeadersOptions _options; private readonly RequestDelegate _next; private readonly ILogger _logger; + private readonly bool _ignoreUnknownProxiesWithoutFor; private bool _allowAllHosts; private IList? _allowedHosts; @@ -63,6 +64,18 @@ public ForwardedHeadersMiddleware(RequestDelegate next, ILoggerFactory loggerFac _logger = loggerFactory.CreateLogger(); _next = next; + if (AppContext.TryGetSwitch("Microsoft.AspNetCore.HttpOverrides.IgnoreUnknownProxiesWithoutFor", out var enabled) + && enabled) + { + _ignoreUnknownProxiesWithoutFor = true; + } + + if (Environment.GetEnvironmentVariable("MICROSOFT_ASPNETCORE_HTTPOVERRIDES_IGNORE_UNKNOWN_PROXIES_WITHOUT_FOR") is string env + && (env.Equals("true", StringComparison.OrdinalIgnoreCase) || env.Equals("1"))) + { + _ignoreUnknownProxiesWithoutFor = true; + } + PreProcessHosts(); static void EnsureOptionNotNullorWhitespace(string value, string propertyName) @@ -228,19 +241,24 @@ public void ApplyForwarders(HttpContext context) for (; entriesConsumed < sets.Length; entriesConsumed++) { var set = sets[entriesConsumed]; - if (checkFor) + // Opt-out of breaking change behavior where we now always check KnownProxies and KnownNetworks + // It used to be guarded by the ForwardedHeaders.XForwardedFor flag, but now we always check it. + if (!_ignoreUnknownProxiesWithoutFor || checkFor) { // For the first instance, allow remoteIp to be null for servers that don't support it natively. if (currentValues.RemoteIpAndPort != null && checkKnownIps && !CheckKnownAddress(currentValues.RemoteIpAndPort.Address)) { // Stop at the first unknown remote IP, but still apply changes processed so far. - if (_logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Warning)) { - _logger.LogDebug(1, "Unknown proxy: {RemoteIpAndPort}", currentValues.RemoteIpAndPort); + _logger.LogWarning(1, "Unknown proxy: {RemoteIpAndPort}", currentValues.RemoteIpAndPort); } break; } + } + if (checkFor) + { if (IPEndPoint.TryParse(set.IpAndPortText, out var parsedEndPoint)) { applyChanges = true; diff --git a/src/Middleware/HttpOverrides/test/ForwardedHeadersMiddlewareTest.cs b/src/Middleware/HttpOverrides/test/ForwardedHeadersMiddlewareTest.cs index aa33a191e7b7..627ad96a3cd6 100644 --- a/src/Middleware/HttpOverrides/test/ForwardedHeadersMiddlewareTest.cs +++ b/src/Middleware/HttpOverrides/test/ForwardedHeadersMiddlewareTest.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; +using Microsoft.DotNet.RemoteExecutor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -962,6 +963,201 @@ public async Task AllOptionsDisabledRequestDoesntChange() Assert.Equal(PathString.Empty, context.Request.PathBase); } + [Theory] + [InlineData(ForwardedHeaders.XForwardedFor, false)] + [InlineData(ForwardedHeaders.XForwardedFor, true)] + [InlineData(ForwardedHeaders.XForwardedHost, false)] + [InlineData(ForwardedHeaders.XForwardedHost, true)] + [InlineData(ForwardedHeaders.XForwardedProto, false)] + [InlineData(ForwardedHeaders.XForwardedProto, true)] + [InlineData(ForwardedHeaders.XForwardedPrefix, false)] + [InlineData(ForwardedHeaders.XForwardedPrefix, true)] + public async Task IgnoreXForwardedHeadersFromUnknownProxy(ForwardedHeaders forwardedHeaders, bool unknownProxy) + { + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + var options = new ForwardedHeadersOptions + { + ForwardedHeaders = forwardedHeaders + }; + if (!unknownProxy) + { + var proxy = IPAddress.Parse("10.0.0.1"); + options.KnownProxies.Add(proxy); + } + app.UseForwardedHeaders(options); + }); + }).Build(); + + await host.StartAsync(); + + var server = host.GetTestServer(); + + var context = await server.SendAsync(c => + { + c.Request.Headers["X-Forwarded-For"] = "11.111.111.11"; + c.Request.Headers["X-Forwarded-Host"] = "testhost"; + c.Request.Headers["X-Forwarded-Proto"] = "Protocol"; + c.Request.Headers["X-Forwarded-Prefix"] = "/pathbase"; + c.Connection.RemoteIpAddress = IPAddress.Parse("10.0.0.1"); + c.Connection.RemotePort = 99; + }); + + if (unknownProxy) + { + Assert.Equal("10.0.0.1", context.Connection.RemoteIpAddress.ToString()); + Assert.Equal("localhost", context.Request.Host.ToString()); + Assert.Equal("http", context.Request.Scheme); + Assert.Equal(PathString.Empty, context.Request.PathBase); + } + else + { + if (forwardedHeaders.HasFlag(ForwardedHeaders.XForwardedFor)) + { + Assert.Equal("11.111.111.11", context.Connection.RemoteIpAddress.ToString()); + } + if (forwardedHeaders.HasFlag(ForwardedHeaders.XForwardedHost)) + { + Assert.Equal("testhost", context.Request.Host.ToString()); + } + if (forwardedHeaders.HasFlag(ForwardedHeaders.XForwardedProto)) + { + Assert.Equal("Protocol", context.Request.Scheme); + } + if (forwardedHeaders.HasFlag(ForwardedHeaders.XForwardedPrefix)) + { + Assert.Equal("/pathbase", context.Request.PathBase); + } + } + } + + [Theory] + [InlineData(ForwardedHeaders.XForwardedFor)] + [InlineData(ForwardedHeaders.XForwardedHost)] + [InlineData(ForwardedHeaders.XForwardedProto)] + [InlineData(ForwardedHeaders.XForwardedPrefix)] + public void AppContextDoesNotValidateUnknownProxyWithoutForwardedFor(ForwardedHeaders forwardedHeaders) + { + RemoteExecutor.Invoke(static async (forwardedHeadersName) => + { + Assert.True(Enum.TryParse(forwardedHeadersName, out var forwardedHeaders)); + AppContext.SetSwitch("Microsoft.AspNetCore.HttpOverrides.IgnoreUnknownProxiesWithoutFor", true); + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + var options = new ForwardedHeadersOptions + { + ForwardedHeaders = forwardedHeaders + }; + app.UseForwardedHeaders(options); + }); + }).Build(); + + await host.StartAsync(); + + var server = host.GetTestServer(); + + var context = await server.SendAsync(c => + { + c.Request.Headers["X-Forwarded-For"] = "11.111.111.11"; + c.Request.Headers["X-Forwarded-Host"] = "testhost"; + c.Request.Headers["X-Forwarded-Proto"] = "Protocol"; + c.Request.Headers["X-Forwarded-Prefix"] = "/pathbase"; + c.Connection.RemoteIpAddress = IPAddress.Parse("10.0.0.1"); + c.Connection.RemotePort = 99; + }); + + if (forwardedHeaders.HasFlag(ForwardedHeaders.XForwardedFor)) + { + // X-Forwarded-For ignored since 10.0.0.1 isn't in KnownProxies + Assert.Equal("10.0.0.1", context.Connection.RemoteIpAddress.ToString()); + } + if (forwardedHeaders.HasFlag(ForwardedHeaders.XForwardedHost)) + { + Assert.Equal("testhost", context.Request.Host.ToString()); + } + if (forwardedHeaders.HasFlag(ForwardedHeaders.XForwardedProto)) + { + Assert.Equal("Protocol", context.Request.Scheme); + } + if (forwardedHeaders.HasFlag(ForwardedHeaders.XForwardedPrefix)) + { + Assert.Equal("/pathbase", context.Request.PathBase); + } + return RemoteExecutor.SuccessExitCode; + }, forwardedHeaders.ToString()).Dispose(); + } + + [Theory] + [InlineData(ForwardedHeaders.XForwardedFor)] + [InlineData(ForwardedHeaders.XForwardedHost)] + [InlineData(ForwardedHeaders.XForwardedProto)] + [InlineData(ForwardedHeaders.XForwardedPrefix)] + public void EnvVariableDoesNotValidateUnknownProxyWithoutForwardedFor(ForwardedHeaders forwardedHeaders) + { + RemoteExecutor.Invoke(static async (forwardedHeadersName) => + { + Assert.True(Enum.TryParse(forwardedHeadersName, out var forwardedHeaders)); + Environment.SetEnvironmentVariable("MICROSOFT_ASPNETCORE_HTTPOVERRIDES_IGNORE_UNKNOWN_PROXIES_WITHOUT_FOR", "true"); + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + var options = new ForwardedHeadersOptions + { + ForwardedHeaders = forwardedHeaders + }; + app.UseForwardedHeaders(options); + }); + }).Build(); + + await host.StartAsync(); + + var server = host.GetTestServer(); + + var context = await server.SendAsync(c => + { + c.Request.Headers["X-Forwarded-For"] = "11.111.111.11"; + c.Request.Headers["X-Forwarded-Host"] = "testhost"; + c.Request.Headers["X-Forwarded-Proto"] = "Protocol"; + c.Request.Headers["X-Forwarded-Prefix"] = "/pathbase"; + c.Connection.RemoteIpAddress = IPAddress.Parse("10.0.0.1"); + c.Connection.RemotePort = 99; + }); + + if (forwardedHeaders.HasFlag(ForwardedHeaders.XForwardedFor)) + { + // X-Forwarded-For ignored since 10.0.0.1 isn't in KnownProxies + Assert.Equal("10.0.0.1", context.Connection.RemoteIpAddress.ToString()); + } + if (forwardedHeaders.HasFlag(ForwardedHeaders.XForwardedHost)) + { + Assert.Equal("testhost", context.Request.Host.ToString()); + } + if (forwardedHeaders.HasFlag(ForwardedHeaders.XForwardedProto)) + { + Assert.Equal("Protocol", context.Request.Scheme); + } + if (forwardedHeaders.HasFlag(ForwardedHeaders.XForwardedPrefix)) + { + Assert.Equal("/pathbase", context.Request.PathBase); + } + return RemoteExecutor.SuccessExitCode; + }, forwardedHeaders.ToString()).Dispose(); + } + [Fact] public async Task PartiallyEnabledForwardsPartiallyChangesRequest() { diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index a75546b6793f..38a38069b32c 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Linq; using Microsoft.Extensions.Caching.Memory; namespace Microsoft.AspNetCore.OutputCaching.Memory; @@ -9,7 +10,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Memory; internal sealed class MemoryOutputCacheStore : IOutputCacheStore { private readonly MemoryCache _cache; - private readonly Dictionary> _taggedEntries = new(); + private readonly Dictionary> _taggedEntries = []; private readonly object _tagsLock = new(); internal MemoryOutputCacheStore(MemoryCache cache) @@ -20,7 +21,7 @@ internal MemoryOutputCacheStore(MemoryCache cache) } // For testing - internal Dictionary> TaggedEntries => _taggedEntries; + internal Dictionary> TaggedEntries => _taggedEntries.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(t => t.Key).ToHashSet()); public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken) { @@ -30,7 +31,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken { if (_taggedEntries.TryGetValue(tag, out var keys)) { - if (keys != null && keys.Count > 0) + if (keys is { Count: > 0 }) { // If MemoryCache changed to run eviction callbacks inline in Remove, iterating over keys could throw // To prevent allocating a copy of the keys we check if the eviction callback ran, @@ -40,7 +41,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken while (i > 0) { var oldCount = keys.Count; - foreach (var key in keys) + foreach (var (key, _) in keys) { _cache.Remove(key); i--; @@ -74,6 +75,8 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(value); + var entryId = Guid.NewGuid(); + if (tags != null) { // Lock with SetEntry() to prevent EvictByTagAsync() from trying to remove a tag whose entry hasn't been added yet. @@ -90,27 +93,27 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val if (!_taggedEntries.TryGetValue(tag, out var keys)) { - keys = new HashSet(); + keys = new HashSet(); _taggedEntries[tag] = keys; } Debug.Assert(keys != null); - keys.Add(key); + keys.Add(new TaggedEntry(key, entryId)); } - SetEntry(key, value, tags, validFor); + SetEntry(key, value, tags, validFor, entryId); } } else { - SetEntry(key, value, tags, validFor); + SetEntry(key, value, tags, validFor, entryId); } return ValueTask.CompletedTask; } - void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFor) + private void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFor, Guid entryId) { Debug.Assert(key != null); @@ -120,22 +123,25 @@ void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFor) Size = value.Length }; - if (tags != null && tags.Length > 0) + if (tags is { Length: > 0 }) { // Remove cache keys from tag lists when the entry is evicted - options.RegisterPostEvictionCallback(RemoveFromTags, tags); + options.RegisterPostEvictionCallback(RemoveFromTags, (tags, entryId)); } _cache.Set(key, value, options); } - void RemoveFromTags(object key, object? value, EvictionReason reason, object? state) + private void RemoveFromTags(object key, object? value, EvictionReason reason, object? state) { - var tags = state as string[]; + Debug.Assert(state != null); + + var (tags, entryId) = ((string[] Tags, Guid EntryId))state; Debug.Assert(tags != null); Debug.Assert(tags.Length > 0); Debug.Assert(key is string); + Debug.Assert(entryId != Guid.Empty); lock (_tagsLock) { @@ -143,7 +149,7 @@ void RemoveFromTags(object key, object? value, EvictionReason reason, object? st { if (_taggedEntries.TryGetValue(tag, out var tagged)) { - tagged.Remove((string)key); + tagged.Remove(new TaggedEntry((string)key, entryId)); // Remove the collection if there is no more keys in it if (tagged.Count == 0) @@ -154,4 +160,6 @@ void RemoveFromTags(object key, object? value, EvictionReason reason, object? st } } } + + private record TaggedEntry(string Key, Guid EntryId); } diff --git a/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs index e8c809911add..c1ad1d708f4b 100644 --- a/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs +++ b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs @@ -197,6 +197,43 @@ public async Task ExpiredEntries_AreRemovedFromTags() Assert.Single(tag2s); } + [Fact] + public async Task ReplacedEntries_AreNotRemovedFromTags() + { + var testClock = new TestMemoryOptionsClock { UtcNow = DateTimeOffset.UtcNow }; + var cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 1000, Clock = testClock, ExpirationScanFrequency = TimeSpan.FromMilliseconds(1) }); + var store = new MemoryOutputCacheStore(cache); + var value = "abc"u8.ToArray(); + + await store.SetAsync("a", value, new[] { "tag1", "tag2" }, TimeSpan.FromMilliseconds(5), default); + await store.SetAsync("a", value, new[] { "tag1" }, TimeSpan.FromMilliseconds(20), default); + + testClock.Advance(TimeSpan.FromMilliseconds(10)); + + // Trigger background expiration by accessing the cache. + _ = cache.Get("a"); + + var resulta = await store.GetAsync("a", default); + + Assert.NotNull(resulta); + + HashSet tag1s, tag2s; + + // Wait for the tag2 HashSet to be removed by the background expiration thread. + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + + while (store.TaggedEntries.TryGetValue("tag2", out tag2s) && !cts.IsCancellationRequested) + { + await Task.Yield(); + } + + store.TaggedEntries.TryGetValue("tag1", out tag1s); + + Assert.Null(tag2s); + Assert.Single(tag1s); + } + [Theory] [InlineData(null)] public async Task Store_Throws_OnInvalidTag(string tag) diff --git a/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs b/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs index 8f8873057027..de74d6e5bf7c 100644 --- a/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs +++ b/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs @@ -135,21 +135,35 @@ private async Task ValidateCertificateAsync(X509Certificate2 } var chainPolicy = BuildChainPolicy(clientCertificate, isCertificateSelfSigned); - using var chain = new X509Chain + var chain = new X509Chain { ChainPolicy = chainPolicy }; - var certificateIsValid = chain.Build(clientCertificate); - if (!certificateIsValid) + try + { + var certificateIsValid = chain.Build(clientCertificate); + if (!certificateIsValid) + { + var chainErrors = new List(chain.ChainStatus.Length); + foreach (var validationFailure in chain.ChainStatus) + { + chainErrors.Add($"{validationFailure.Status} {validationFailure.StatusInformation}"); + } + Logger.CertificateFailedValidation(clientCertificate.Subject, chainErrors); + return AuthenticateResults.InvalidClientCertificate; + } + } + finally { - var chainErrors = new List(chain.ChainStatus.Length); - foreach (var validationFailure in chain.ChainStatus) + // Disposing the chain does not dispose the elements we potentially built. + // Do the full walk manually to dispose. + for (var i = 0; i < chain.ChainElements.Count; i++) { - chainErrors.Add($"{validationFailure.Status} {validationFailure.StatusInformation}"); + chain.ChainElements[i].Certificate.Dispose(); } - Logger.CertificateFailedValidation(clientCertificate.Subject, chainErrors); - return AuthenticateResults.InvalidClientCertificate; + + chain.Dispose(); } var certificateValidatedContext = new CertificateValidatedContext(Context, Scheme, Options) diff --git a/src/Servers/HttpSys/HttpSysServer.slnf b/src/Servers/HttpSys/HttpSysServer.slnf index 4e0193ae8f4d..2b9ed68e48f0 100644 --- a/src/Servers/HttpSys/HttpSysServer.slnf +++ b/src/Servers/HttpSys/HttpSysServer.slnf @@ -37,6 +37,7 @@ "src\\Servers\\HttpSys\\samples\\QueueSharing\\QueueSharing.csproj", "src\\Servers\\HttpSys\\samples\\SelfHostServer\\SelfHostServer.csproj", "src\\Servers\\HttpSys\\samples\\TestClient\\TestClient.csproj", + "src\\Servers\\HttpSys\\samples\\TlsFeaturesObserve\\TlsFeaturesObserve.csproj", "src\\Servers\\HttpSys\\src\\Microsoft.AspNetCore.Server.HttpSys.csproj", "src\\Servers\\HttpSys\\test\\FunctionalTests\\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj", "src\\Servers\\HttpSys\\test\\NonHelixTests\\Microsoft.AspNetCore.Server.HttpSys.NonHelixTests.csproj", @@ -53,4 +54,4 @@ "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} \ No newline at end of file +} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs new file mode 100644 index 000000000000..3865ecd59451 --- /dev/null +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Runtime.InteropServices; + +namespace TlsFeaturesObserve.HttpSys; + +internal static class HttpSysConfigurator +{ + const uint HTTP_INITIALIZE_CONFIG = 0x00000002; + const uint ERROR_ALREADY_EXISTS = 183; + + static readonly HTTPAPI_VERSION HttpApiVersion = new HTTPAPI_VERSION(1, 0); + + internal static void ConfigureCacheTlsClientHello() + { + // Arbitrarily chosen port, but must match the port used in the web server. Via UrlPrefixes or launchsettings. + var ipPort = new IPEndPoint(new IPAddress([0, 0, 0, 0]), 6000); + var certThumbprint = "" /* your cert thumbprint here */; + var appId = Guid.NewGuid(); + var sslCertStoreName = "My"; + + CallHttpApi(() => SetConfiguration(ipPort, certThumbprint, appId, sslCertStoreName)); + } + + static void SetConfiguration(IPEndPoint ipPort, string certThumbprint, Guid appId, string sslCertStoreName) + { + var sockAddrHandle = CreateSockaddrStructure(ipPort); + var pIpPort = sockAddrHandle.AddrOfPinnedObject(); + var httpServiceConfigSslKey = new HTTP_SERVICE_CONFIG_SSL_KEY(pIpPort); + + var hash = GetHash(certThumbprint); + var handleHash = GCHandle.Alloc(hash, GCHandleType.Pinned); + var configSslParam = new HTTP_SERVICE_CONFIG_SSL_PARAM + { + AppId = appId, + DefaultFlags = 0x00008000 /* HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO */, + DefaultRevocationFreshnessTime = 0, + DefaultRevocationUrlRetrievalTimeout = 15, + pSslCertStoreName = sslCertStoreName, + pSslHash = handleHash.AddrOfPinnedObject(), + SslHashLength = hash.Length, + pDefaultSslCtlIdentifier = null, + pDefaultSslCtlStoreName = sslCertStoreName + }; + + var configSslSet = new HTTP_SERVICE_CONFIG_SSL_SET + { + ParamDesc = configSslParam, + KeyDesc = httpServiceConfigSslKey + }; + + var pInputConfigInfo = Marshal.AllocCoTaskMem( + Marshal.SizeOf(typeof(HTTP_SERVICE_CONFIG_SSL_SET))); + Marshal.StructureToPtr(configSslSet, pInputConfigInfo, false); + + var status = HttpSetServiceConfiguration(nint.Zero, + HTTP_SERVICE_CONFIG_ID.HttpServiceConfigSSLCertInfo, + pInputConfigInfo, + Marshal.SizeOf(configSslSet), + nint.Zero); + + if (status == ERROR_ALREADY_EXISTS || status == 0) // already present or success + { + Console.WriteLine($"HttpServiceConfiguration is correct"); + } + else + { + Console.WriteLine("Failed to HttpSetServiceConfiguration: " + status); + } + } + + static byte[] GetHash(string thumbprint) + { + var length = thumbprint.Length; + var bytes = new byte[length / 2]; + for (var i = 0; i < length; i += 2) + { + bytes[i / 2] = Convert.ToByte(thumbprint.Substring(i, 2), 16); + } + + return bytes; + } + + static GCHandle CreateSockaddrStructure(IPEndPoint ipEndPoint) + { + var socketAddress = ipEndPoint.Serialize(); + + // use an array of bytes instead of the sockaddr structure + var sockAddrStructureBytes = new byte[socketAddress.Size]; + var sockAddrHandle = GCHandle.Alloc(sockAddrStructureBytes, GCHandleType.Pinned); + for (var i = 0; i < socketAddress.Size; ++i) + { + sockAddrStructureBytes[i] = socketAddress[i]; + } + return sockAddrHandle; + } + + static void CallHttpApi(Action body) + { + const uint flags = HTTP_INITIALIZE_CONFIG; + var retVal = HttpInitialize(HttpApiVersion, flags, IntPtr.Zero); + body(); + } + +// disabled warning since it is just a sample +#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time + [DllImport("httpapi.dll", SetLastError = true)] + private static extern uint HttpInitialize( + HTTPAPI_VERSION version, + uint flags, + IntPtr pReserved); + + [DllImport("httpapi.dll", SetLastError = true)] + public static extern uint HttpSetServiceConfiguration( + nint serviceIntPtr, + HTTP_SERVICE_CONFIG_ID configId, + nint pConfigInformation, + int configInformationLength, + nint pOverlapped); +#pragma warning restore SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time +} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/Native.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/Native.cs new file mode 100644 index 000000000000..b939163d2252 --- /dev/null +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/Native.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. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace TlsFeaturesObserve.HttpSys; + +// Http.Sys types from https://learn.microsoft.com/windows/win32/api/http/ + +[StructLayout(LayoutKind.Sequential, Pack = 2)] +public struct HTTPAPI_VERSION +{ + public ushort HttpApiMajorVersion; + public ushort HttpApiMinorVersion; + + public HTTPAPI_VERSION(ushort majorVersion, ushort minorVersion) + { + HttpApiMajorVersion = majorVersion; + HttpApiMinorVersion = minorVersion; + } +} + +public enum HTTP_SERVICE_CONFIG_ID +{ + HttpServiceConfigIPListenList = 0, + HttpServiceConfigSSLCertInfo, + HttpServiceConfigUrlAclInfo, + HttpServiceConfigMax +} + +[StructLayout(LayoutKind.Sequential)] +public struct HTTP_SERVICE_CONFIG_SSL_SET +{ + public HTTP_SERVICE_CONFIG_SSL_KEY KeyDesc; + public HTTP_SERVICE_CONFIG_SSL_PARAM ParamDesc; +} + +[StructLayout(LayoutKind.Sequential)] +public struct HTTP_SERVICE_CONFIG_SSL_KEY +{ + public IntPtr pIpPort; + + public HTTP_SERVICE_CONFIG_SSL_KEY(IntPtr pIpPort) + { + this.pIpPort = pIpPort; + } +} + +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] +public struct HTTP_SERVICE_CONFIG_SSL_PARAM +{ + public int SslHashLength; + public IntPtr pSslHash; + public Guid AppId; + [MarshalAs(UnmanagedType.LPWStr)] + public string pSslCertStoreName; + public CertCheckModes DefaultCertCheckMode; + public int DefaultRevocationFreshnessTime; + public int DefaultRevocationUrlRetrievalTimeout; + [MarshalAs(UnmanagedType.LPWStr)] + public string pDefaultSslCtlIdentifier; + [MarshalAs(UnmanagedType.LPWStr)] + public string pDefaultSslCtlStoreName; + public uint DefaultFlags; // HTTP_SERVICE_CONFIG_SSL_FLAG +} + +[Flags] +public enum CertCheckModes : uint +{ + /// + /// Enables the client certificate revocation check. + /// + None = 0, + + /// + /// Client certificate is not to be verified for revocation. + /// + DoNotVerifyCertificateRevocation = 1, + + /// + /// Only cached certificate is to be used the revocation check. + /// + VerifyRevocationWithCachedCertificateOnly = 2, + + /// + /// The RevocationFreshnessTime setting is enabled. + /// + EnableRevocationFreshnessTime = 4, + + /// + /// No usage check is to be performed. + /// + NoUsageCheck = 0x10000 +} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs new file mode 100644 index 000000000000..13a31cb59b23 --- /dev/null +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.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. + +using System.Reflection; +using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.HttpSys; +using Microsoft.Extensions.Hosting; +using TlsFeatureObserve; +using TlsFeaturesObserve.HttpSys; + +HttpSysConfigurator.ConfigureCacheTlsClientHello(); +CreateHostBuilder(args).Build().Run(); + +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHost(webBuilder => + { + webBuilder.UseStartup() + .UseHttpSys(options => + { + // If you want to use https locally: https://stackoverflow.com/a/51841893 + options.UrlPrefixes.Add("https://*:6000"); // HTTPS + + options.Authentication.Schemes = AuthenticationSchemes.None; + options.Authentication.AllowAnonymous = true; + + // If you want to resolve a callback API, uncomment. + // Recommended approach is to use the on-demand API to fetch TLS client hello bytes, + // look into Startup.cs for details. + + //var property = typeof(HttpSysOptions).GetProperty("TlsClientHelloBytesCallback", BindingFlags.NonPublic | BindingFlags.Instance); + //var delegateType = property.PropertyType; // Get the exact delegate type + + //// Create a delegate of the correct type + //var callbackDelegate = Delegate.CreateDelegate(delegateType, typeof(Holder).GetMethod(nameof(Holder.ProcessTlsClientHello), BindingFlags.Static | BindingFlags.Public)); + + //property?.SetValue(options, callbackDelegate); + }); + }); + +public static class Holder +{ + public static void ProcessTlsClientHello(IFeatureCollection features, ReadOnlySpan tlsClientHelloBytes) + { + var httpConnectionFeature = features.Get(); + + var myTlsFeature = new MyTlsFeature( + connectionId: httpConnectionFeature.ConnectionId, + tlsClientHelloLength: tlsClientHelloBytes.Length); + + features.Set(myTlsFeature); + } +} + +public interface IMyTlsFeature +{ + string ConnectionId { get; } + int TlsClientHelloLength { get; } +} + +public class MyTlsFeature : IMyTlsFeature +{ + public string ConnectionId { get; } + public int TlsClientHelloLength { get; } + + public MyTlsFeature(string connectionId, int tlsClientHelloLength) + { + ConnectionId = connectionId; + TlsClientHelloLength = tlsClientHelloLength; + } +} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Properties/launchSettings.json b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Properties/launchSettings.json new file mode 100644 index 000000000000..c9d6b5efcb3c --- /dev/null +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "TlsFeaturesObserve": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "nativeDebugging": true + } + } +} \ No newline at end of file diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs new file mode 100644 index 000000000000..4440149c3552 --- /dev/null +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.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. + +using System; +using System.Buffers; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.HttpSys; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace TlsFeatureObserve; + +public class Startup +{ + public void Configure(IApplicationBuilder app) + { + // recommended approach to fetch TLS client hello bytes + // is via on-demand API per request or by building own connection-lifecycle manager + app.Run(async (HttpContext context) => + { + context.Response.ContentType = "text/plain"; + + var httpSysAssembly = typeof(Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions).Assembly; + var httpSysPropertyFeatureType = httpSysAssembly.GetType("Microsoft.AspNetCore.Server.HttpSys.IHttpSysRequestPropertyFeature"); + var httpSysPropertyFeature = context.Features[httpSysPropertyFeatureType]!; + + var method = httpSysPropertyFeature.GetType().GetMethod( + "TryGetTlsClientHello", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic + ); + + // invoke first time to get required size + byte[] bytes = Array.Empty(); + var parameters = new object[] { bytes, 0 }; + var res = (bool)method.Invoke(httpSysPropertyFeature, parameters); + + // fetching out parameter only works by looking into parameters array of objects + var bytesReturned = (int)parameters[1]; + bytes = ArrayPool.Shared.Rent(bytesReturned); + parameters = [bytes, 0]; // correct input now + res = (bool)method.Invoke(httpSysPropertyFeature, parameters); + + // to avoid CS4012 use a method which accepts a byte[] and length, where you can do Span slicing + // error CS4012: Parameters or locals of type 'Span' cannot be declared in async methods or async lambda expressions. + var message = ReadTlsClientHello(bytes, bytesReturned); + await context.Response.WriteAsync(message); + ArrayPool.Shared.Return(bytes); + }); + + static string ReadTlsClientHello(byte[] bytes, int bytesReturned) + { + var tlsClientHelloBytes = bytes.AsSpan(0, bytesReturned); + return $"TlsClientHello bytes: {string.Join(" ", tlsClientHelloBytes.ToArray())}, length={bytesReturned}"; + } + + // middleware compatible with callback API + //app.Run(async (HttpContext context) => + //{ + // context.Response.ContentType = "text/plain"; + + // var tlsFeature = context.Features.Get(); + // await context.Response.WriteAsync("TlsClientHello` data: " + $"connectionId={tlsFeature?.ConnectionId}; length={tlsFeature?.TlsClientHelloLength}"); + //}); + } +} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj b/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj new file mode 100644 index 000000000000..57b6cef72608 --- /dev/null +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj @@ -0,0 +1,15 @@ + + + + $(DefaultNetCoreTargetFramework) + Exe + true + latest + + + + + + + + diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 2b7924491d32..7fecff3c848d 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; +using Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.HttpSys; @@ -37,6 +38,7 @@ internal sealed partial class HttpSysListener : IDisposable private readonly UrlGroup _urlGroup; private readonly RequestQueue _requestQueue; private readonly DisconnectListener _disconnectListener; + private readonly TlsListener? _tlsListener; private readonly object _internalLock; @@ -69,12 +71,14 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) try { _serverSession = new ServerSession(); - _requestQueue = new RequestQueue(options.RequestQueueName, options.RequestQueueMode, Logger); - _urlGroup = new UrlGroup(_serverSession, _requestQueue, Logger); _disconnectListener = new DisconnectListener(_requestQueue, Logger); + if (options.TlsClientHelloBytesCallback is not null) + { + _tlsListener = new TlsListener(Logger, options.TlsClientHelloBytesCallback); + } } catch (Exception exception) { @@ -82,6 +86,7 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) _requestQueue?.Dispose(); _urlGroup?.Dispose(); _serverSession?.Dispose(); + _tlsListener?.Dispose(); Log.HttpSysListenerCtorError(Logger, exception); throw; } @@ -96,20 +101,10 @@ internal enum State internal ILogger Logger { get; private set; } - internal UrlGroup UrlGroup - { - get { return _urlGroup; } - } - - internal RequestQueue RequestQueue - { - get { return _requestQueue; } - } - - internal DisconnectListener DisconnectListener - { - get { return _disconnectListener; } - } + internal UrlGroup UrlGroup => _urlGroup; + internal RequestQueue RequestQueue => _requestQueue; + internal TlsListener? TlsListener => _tlsListener; + internal DisconnectListener DisconnectListener => _disconnectListener; public HttpSysOptions Options { get; } @@ -262,6 +257,7 @@ private void DisposeInternal() Debug.Assert(!_serverSession.Id.IsInvalid, "ServerSessionHandle is invalid in CloseV2Config"); _serverSession.Dispose(); + _tlsListener?.Dispose(); } /// diff --git a/src/Servers/HttpSys/src/HttpSysOptions.cs b/src/Servers/HttpSys/src/HttpSysOptions.cs index 87fb1ba6d176..dfed2d7beced 100644 --- a/src/Servers/HttpSys/src/HttpSysOptions.cs +++ b/src/Servers/HttpSys/src/HttpSysOptions.cs @@ -242,10 +242,23 @@ public Http503VerbosityLevel Http503Verbosity /// Configures request headers to use encoding. /// /// - /// Defaults to `false`, in which case will be used. />. + /// Defaults to false, in which case will be used. />. /// public bool UseLatin1RequestHeaders { get; set; } + /// + /// A callback to be invoked to get the TLS client hello bytes. + /// Null by default. + /// + /// + /// Works only if HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO flag is set on http.sys service configuration. + /// See + /// and + /// + internal TlsClientHelloCallback? TlsClientHelloBytesCallback { get; set; } + + internal delegate void TlsClientHelloCallback(IFeatureCollection features, ReadOnlySpan clientHelloBytes); + // Not called when attaching to an existing queue. internal void Apply(UrlGroup urlGroup, RequestQueue? requestQueue) { diff --git a/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs b/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs new file mode 100644 index 000000000000..16a20abaea8d --- /dev/null +++ b/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.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. + +namespace Microsoft.AspNetCore.Server.HttpSys; + +/// +/// Provides API to read HTTP_REQUEST_PROPERTY value from the HTTP.SYS request. +/// +/// +// internal for backport +internal interface IHttpSysRequestPropertyFeature +{ + /// + /// Reads the TLS client hello from HTTP.SYS + /// + /// Where the raw bytes of the TLS Client Hello message are written. + /// + /// Returns the number of bytes written to . + /// Or can return the size of the buffer needed if wasn't large enough. + /// + /// + /// Works only if HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO flag is set on http.sys service configuration. + /// See + /// and + ///

+ /// If you don't want to guess the required size before first invocation, + /// you should first call with set to empty size, so that you can retrieve the required buffer size from , + /// then allocate that amount of memory and retry the query. + ///
+ /// + /// True, if fetching TLS client hello was successful, false if size is not large enough. + /// If unsuccessful for other reason throws an exception. + /// + /// Any HttpSys error except for ERROR_INSUFFICIENT_BUFFER or ERROR_MORE_DATA. + /// If HttpSys does not support querying the TLS Client Hello. + // has byte[] (not Span) for reflection-based invocation + bool TryGetTlsClientHello(byte[] tlsClientHelloBytesDestination, out int bytesReturned); +} diff --git a/src/Servers/HttpSys/src/LoggerEventIds.cs b/src/Servers/HttpSys/src/LoggerEventIds.cs index 5bc0b6b65ed6..e6d745f506be 100644 --- a/src/Servers/HttpSys/src/LoggerEventIds.cs +++ b/src/Servers/HttpSys/src/LoggerEventIds.cs @@ -59,4 +59,5 @@ internal static class LoggerEventIds public const int AcceptCancelExpectationMismatch = 52; public const int AcceptObserveExpectationMismatch = 53; public const int RequestParsingError = 54; + public const int TlsListenerError = 55; } diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index 575ebc259d25..efb42c87c5b5 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -122,13 +122,30 @@ internal static HTTP_API_VERSION ApiVersion } internal static SafeLibraryHandle? HttpApiModule { get; private set; } - internal static HttpGetRequestPropertyInvoker? HttpGetRequestProperty { get; private set; } - internal static HttpSetRequestPropertyInvoker? HttpSetRequestProperty { get; private set; } - [MemberNotNullWhen(true, nameof(HttpSetRequestProperty))] + private static HttpGetRequestPropertyInvoker? HttpGetRequestInvoker { get; set; } + private static HttpSetRequestPropertyInvoker? HttpSetRequestInvoker { get; set; } + + internal static bool HttpGetRequestPropertySupported => HttpGetRequestInvoker is not null; + internal static bool HttpSetRequestPropertySupported => HttpSetRequestInvoker is not null; + + internal static unsafe uint HttpGetRequestProperty(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, + void* qualifier, uint qualifierSize, void* output, uint outputSize, uint* bytesReturned, IntPtr overlapped) + { + return HttpGetRequestInvoker!(requestQueueHandle, requestId, propertyId, qualifier, qualifierSize, output, outputSize, bytesReturned, overlapped); + } + + internal static unsafe uint HttpSetRequestProperty(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, + void* input, uint inputSize, IntPtr overlapped) + { + return HttpSetRequestInvoker!(requestQueueHandle, requestId, propertyId, input, inputSize, overlapped); + } + + [MemberNotNullWhen(true, nameof(HttpSetRequestInvoker))] internal static bool SupportsTrailers { get; private set; } - [MemberNotNullWhen(true, nameof(HttpSetRequestProperty))] + [MemberNotNullWhen(true, nameof(HttpSetRequestInvoker))] internal static bool SupportsReset { get; private set; } internal static bool SupportsDelegation { get; private set; } + internal static bool SupportsClientHello { get; private set; } static HttpApi() { @@ -147,11 +164,12 @@ private static void InitHttpApi(ushort majorVersion, ushort minorVersion) if (supported) { HttpApiModule = SafeLibraryHandle.Open(HTTPAPI); - HttpGetRequestProperty = HttpApiModule.GetProcAddress("HttpQueryRequestProperty", throwIfNotFound: false); - HttpSetRequestProperty = HttpApiModule.GetProcAddress("HttpSetRequestProperty", throwIfNotFound: false); - SupportsReset = HttpSetRequestProperty != null; + HttpGetRequestInvoker = HttpApiModule.GetProcAddress("HttpQueryRequestProperty", throwIfNotFound: false); + HttpSetRequestInvoker = HttpApiModule.GetProcAddress("HttpSetRequestProperty", throwIfNotFound: false); + SupportsReset = HttpSetRequestPropertySupported; SupportsTrailers = IsFeatureSupported(HTTP_FEATURE_ID.HttpFeatureResponseTrailers); SupportsDelegation = IsFeatureSupported(HTTP_FEATURE_ID.HttpFeatureDelegateEx); + SupportsClientHello = IsFeatureSupported((HTTP_FEATURE_ID)11 /* HTTP_FEATURE_ID.HttpFeatureCacheTlsClientHello */) && HttpGetRequestPropertySupported; } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 6029f8269f53..9aa93adb508d 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -9,10 +9,12 @@ using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; +using static Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions; namespace Microsoft.AspNetCore.Server.HttpSys; @@ -362,6 +364,9 @@ private void GetTlsHandshakeResults() SniHostName = sni.Hostname; } + internal bool GetAndInvokeTlsClientHelloCallback(IFeatureCollection features, TlsClientHelloCallback tlsClientHelloBytesCallback) + => RequestContext.GetAndInvokeTlsClientHelloMessageBytesCallback(features, tlsClientHelloBytesCallback); + public X509Certificate2? ClientCertificate { get diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs index e1931dc0fc6b..337afcb09451 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs @@ -36,6 +36,7 @@ internal partial class RequestContext : IHttpResponseTrailersFeature, IHttpResetFeature, IHttpSysRequestDelegationFeature, + IHttpSysRequestPropertyFeature, IConnectionLifetimeNotificationFeature { private IFeatureCollection? _features; @@ -751,4 +752,9 @@ void IConnectionLifetimeNotificationFeature.RequestClose() Response.Headers[HeaderNames.Connection] = "close"; } } + + public bool TryGetTlsClientHello(byte[] tlsClientHelloBytesDestination, out int bytesReturned) + { + return TryGetTlsClientHelloMessageBytes(tlsClientHelloBytesDestination.AsSpan(), out bytesReturned); + } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs index 41b1cf480d5a..d7766698bc41 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs @@ -20,5 +20,8 @@ private static partial class Log [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to parse request.", EventName = "RequestParsingError")] public static partial void RequestParsingError(ILogger logger, Exception exception); + + [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello; RequestId: {RequestId}; Win32 Error code: {Win32Error}", EventName = "TlsClientHelloRetrieveError")] + public static partial void TlsClientHelloRetrieveError(ILogger logger, ulong requestId, uint win32Error); } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index e9f277b6a990..124a9e8dcd02 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -1,13 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Diagnostics; using System.Runtime.InteropServices; using System.Security.Authentication.ExtendedProtection; using System.Security.Principal; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; +using static Microsoft.AspNetCore.HttpSys.Internal.HttpApiTypes; +using static Microsoft.AspNetCore.HttpSys.Internal.UnsafeNclNativeMethods; +using static Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions; namespace Microsoft.AspNetCore.Server.HttpSys; @@ -234,29 +239,171 @@ internal void ForceCancelRequest() } } - internal unsafe HttpApiTypes.HTTP_REQUEST_PROPERTY_SNI GetClientSni() + /// + /// Attempts to get the client hello message bytes from the http.sys. + /// If successful writes the bytes into , and shows how many bytes were written in . + /// If not successful because is not large enough, returns false and shows a size of required in . + /// If not successful for other reason - throws exception with message/errorCode. + /// + internal unsafe bool TryGetTlsClientHelloMessageBytes( + Span destination, + out int bytesReturned) + { + bytesReturned = default; + if (!HttpApi.SupportsClientHello) + { + // not supported, so we just return and don't invoke the callback + throw new InvalidOperationException("Windows HTTP Server API does not support HTTP_FEATURE_ID.HttpFeatureCacheTlsClientHello or HttpQueryRequestProperty. See HTTP_FEATURE_ID for details."); + } + + uint statusCode; + var requestId = PinsReleased ? Request.RequestId : RequestId; + + uint bytesReturnedValue = 0; + uint* bytesReturnedPointer = &bytesReturnedValue; + + fixed (byte* pBuffer = destination) + { + statusCode = HttpApi.HttpGetRequestProperty( + requestQueueHandle: Server.RequestQueue.Handle, + requestId, + propertyId: (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, + qualifier: null, + qualifierSize: 0, + output: pBuffer, + outputSize: (uint)destination.Length, + bytesReturned: bytesReturnedPointer, + overlapped: IntPtr.Zero); + + bytesReturned = checked((int)bytesReturnedValue); + + if (statusCode is ErrorCodes.ERROR_SUCCESS) + { + return true; + } + + // if buffer supplied is too small, `bytesReturned` has proper size + if (statusCode is ErrorCodes.ERROR_MORE_DATA or ErrorCodes.ERROR_INSUFFICIENT_BUFFER) + { + return false; + } + } + + Log.TlsClientHelloRetrieveError(Logger, requestId, statusCode); + throw new HttpSysException((int)statusCode); + } + + /// + /// Attempts to get the client hello message bytes from HTTP.sys and calls the user provided callback. + /// If not successful, will return false. + /// + internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureCollection features, TlsClientHelloCallback tlsClientHelloBytesCallback) { - if (HttpApi.HttpGetRequestProperty != null) + if (!HttpApi.SupportsClientHello) + { + // not supported, so we just return and don't invoke the callback + return false; + } + + uint bytesReturnedValue = 0; + uint* bytesReturned = &bytesReturnedValue; + uint statusCode; + + var requestId = PinsReleased ? Request.RequestId : RequestId; + + // we will try with some "random" buffer size + var buffer = ArrayPool.Shared.Rent(512); + try { - var buffer = new byte[HttpApiTypes.SniPropertySizeInBytes]; fixed (byte* pBuffer = buffer) { - var statusCode = HttpApi.HttpGetRequestProperty( - Server.RequestQueue.Handle, - RequestId, - HttpApiTypes.HTTP_REQUEST_PROPERTY.HttpRequestPropertySni, + statusCode = HttpApi.HttpGetRequestProperty( + requestQueueHandle: Server.RequestQueue.Handle, + requestId, + propertyId: (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, qualifier: null, qualifierSize: 0, - (void*)pBuffer, - (uint)buffer.Length, - bytesReturned: null, - IntPtr.Zero); + output: pBuffer, + outputSize: (uint)buffer.Length, + bytesReturned: bytesReturned, + overlapped: IntPtr.Zero); + + if (statusCode is ErrorCodes.ERROR_SUCCESS) + { + tlsClientHelloBytesCallback(features, buffer.AsSpan(0, (int)bytesReturnedValue)); + return true; + } + } + } + finally + { + ArrayPool.Shared.Return(buffer, clearArray: true); + } - if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + // if buffer supplied is too small, `bytesReturned` will have proper size + // so retry should succeed with the properly allocated buffer + if (statusCode is ErrorCodes.ERROR_MORE_DATA or ErrorCodes.ERROR_INSUFFICIENT_BUFFER) + { + try + { + var correctSize = (int)bytesReturnedValue; + buffer = ArrayPool.Shared.Rent(correctSize); + + fixed (byte* pBuffer = buffer) { - return Marshal.PtrToStructure((IntPtr)pBuffer); + statusCode = HttpApi.HttpGetRequestProperty( + requestQueueHandle: Server.RequestQueue.Handle, + requestId, + propertyId: (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, + qualifier: null, + qualifierSize: 0, + output: pBuffer, + outputSize: (uint)buffer.Length, + bytesReturned: bytesReturned, + overlapped: IntPtr.Zero); + + if (statusCode is ErrorCodes.ERROR_SUCCESS) + { + tlsClientHelloBytesCallback(features, buffer.AsSpan(0, correctSize)); + return true; + } } } + finally + { + ArrayPool.Shared.Return(buffer, clearArray: true); + } + } + + Log.TlsClientHelloRetrieveError(Logger, requestId, statusCode); + return false; + } + + internal unsafe HTTP_REQUEST_PROPERTY_SNI GetClientSni() + { + if (!HttpApi.HttpGetRequestPropertySupported) + { + return default; + } + + var buffer = new byte[HttpApiTypes.SniPropertySizeInBytes]; + fixed (byte* pBuffer = buffer) + { + var statusCode = HttpApi.HttpGetRequestProperty( + Server.RequestQueue.Handle, + RequestId, + HTTP_REQUEST_PROPERTY.HttpRequestPropertySni, + qualifier: null, + qualifierSize: 0, + pBuffer, + (uint)buffer.Length, + bytesReturned: null, + IntPtr.Zero); + + if (statusCode == ErrorCodes.ERROR_SUCCESS) + { + return Marshal.PtrToStructure((IntPtr)pBuffer); + } } return default; diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs index 2a1d06a06d26..399f1292d60d 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs @@ -48,6 +48,12 @@ public override async Task ExecuteAsync() context = application.CreateContext(Features); try { + if (Server.Options.TlsClientHelloBytesCallback is not null && Server.TlsListener is not null + && Request.IsHttps) + { + Server.TlsListener.InvokeTlsClientHelloCallback(Request.RawConnectionId, Features, Request.GetAndInvokeTlsClientHelloCallback); + } + await application.ProcessRequestAsync(context); await CompleteAsync(); } diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs new file mode 100644 index 000000000000..20ffe5c74b6f --- /dev/null +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; + +internal sealed partial class TlsListener : IDisposable +{ + private static partial class Log + { + [LoggerMessage(LoggerEventIds.TlsListenerError, LogLevel.Error, "Error during closed connection cleanup.", EventName = "TlsListenerCleanupClosedConnectionError")] + public static partial void CleanupClosedConnectionError(ILogger logger, Exception exception); + } +} diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs new file mode 100644 index 000000000000..731ecea05f6e --- /dev/null +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Collections.ObjectModel; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using static Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions; + +namespace Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; + +internal sealed partial class TlsListener : IDisposable +{ + private readonly ConcurrentDictionary _connectionTimestamps = new(); + private readonly TlsClientHelloCallback _tlsClientHelloBytesCallback; + private readonly ILogger _logger; + + private readonly PeriodicTimer _cleanupTimer; + private readonly Task _cleanupTask; + private readonly TimeProvider _timeProvider; + + private readonly TimeSpan ConnectionIdleTime = TimeSpan.FromMinutes(5); + private readonly TimeSpan CleanupDelay = TimeSpan.FromSeconds(10); + internal readonly int CacheSizeLimit = 1_000_000; + + // Internal for testing purposes + internal ReadOnlyDictionary ConnectionTimeStamps => _connectionTimestamps.AsReadOnly(); + + internal TlsListener(ILogger logger, TlsClientHelloCallback tlsClientHelloBytesCallback, TimeProvider? timeProvider = null) + { + if (AppContext.GetData("Microsoft.AspNetCore.Server.HttpSys.TlsListener.CacheSizeLimit") is int limit) + { + CacheSizeLimit = limit; + } + + if (AppContext.GetData("Microsoft.AspNetCore.Server.HttpSys.TlsListener.ConnectionIdleTime") is int idleTime) + { + ConnectionIdleTime = TimeSpan.FromSeconds(idleTime); + } + + if (AppContext.GetData("Microsoft.AspNetCore.Server.HttpSys.TlsListener.CleanupDelay") is int cleanupDelay) + { + CleanupDelay = TimeSpan.FromSeconds(cleanupDelay); + } + + _logger = logger; + _tlsClientHelloBytesCallback = tlsClientHelloBytesCallback; + + _timeProvider = timeProvider ?? TimeProvider.System; + _cleanupTimer = new PeriodicTimer(CleanupDelay, _timeProvider); + _cleanupTask = CleanupLoopAsync(); + } + + // Method looks weird because we want it to be testable by not directly requiring a Request object + internal void InvokeTlsClientHelloCallback(ulong connectionId, IFeatureCollection features, + Func invokeTlsClientHelloCallback) + { + if (!_connectionTimestamps.TryAdd(connectionId, _timeProvider.GetUtcNow())) + { + // update TTL + _connectionTimestamps[connectionId] = _timeProvider.GetUtcNow(); + return; + } + + _ = invokeTlsClientHelloCallback(features, _tlsClientHelloBytesCallback); + } + + internal async Task CleanupLoopAsync() + { + while (await _cleanupTimer.WaitForNextTickAsync()) + { + try + { + var now = _timeProvider.GetUtcNow(); + + // Remove idle connections + foreach (var kvp in _connectionTimestamps) + { + if (now - kvp.Value >= ConnectionIdleTime) + { + _connectionTimestamps.TryRemove(kvp.Key, out _); + } + } + + // Evict oldest items if above CacheSizeLimit + var currentCount = _connectionTimestamps.Count; + if (currentCount > CacheSizeLimit) + { + var excessCount = currentCount - CacheSizeLimit; + + // Find the oldest items in a single pass + var oldestTimestamps = new SortedSet>(TimeComparer.Instance); + + foreach (var kvp in _connectionTimestamps) + { + if (oldestTimestamps.Count < excessCount) + { + oldestTimestamps.Add(new KeyValuePair(kvp.Key, kvp.Value)); + } + else if (kvp.Value < oldestTimestamps.Max.Value) + { + oldestTimestamps.Remove(oldestTimestamps.Max); + oldestTimestamps.Add(new KeyValuePair(kvp.Key, kvp.Value)); + } + } + + // Remove the oldest keys + foreach (var item in oldestTimestamps) + { + _connectionTimestamps.TryRemove(item.Key, out _); + } + } + } + catch (Exception ex) + { + Log.CleanupClosedConnectionError(_logger, ex); + } + } + } + + public void Dispose() + { + _cleanupTimer.Dispose(); + _cleanupTask.Wait(); + } + + private sealed class TimeComparer : IComparer> + { + public static TimeComparer Instance { get; } = new TimeComparer(); + + public int Compare(KeyValuePair x, KeyValuePair y) + { + // Compare timestamps first + int timestampComparison = x.Value.CompareTo(y.Value); + if (timestampComparison != 0) + { + return timestampComparison; + } + + // Use the key as a tiebreaker to ensure uniqueness + return x.Key.CompareTo(y.Key); + } + } +} diff --git a/src/Servers/HttpSys/src/StandardFeatureCollection.cs b/src/Servers/HttpSys/src/StandardFeatureCollection.cs index dda57166921e..1c7d078d8253 100644 --- a/src/Servers/HttpSys/src/StandardFeatureCollection.cs +++ b/src/Servers/HttpSys/src/StandardFeatureCollection.cs @@ -27,6 +27,7 @@ internal sealed class StandardFeatureCollection : IFeatureCollection { typeof(IHttpBodyControlFeature), _identityFunc }, { typeof(IHttpSysRequestInfoFeature), _identityFunc }, { typeof(IHttpSysRequestTimingFeature), _identityFunc }, + { typeof(IHttpSysRequestPropertyFeature), _identityFunc }, { typeof(IHttpResponseTrailersFeature), ctx => ctx.GetResponseTrailersFeature() }, { typeof(IHttpResetFeature), ctx => ctx.GetResetFeature() }, { typeof(IConnectionLifetimeNotificationFeature), ctx => ctx.GetConnectionLifetimeNotificationFeature() }, diff --git a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs index 0ccb71964b74..91af51fd8b56 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs @@ -234,6 +234,28 @@ public async Task Https_ITlsHandshakeFeature_MatchesIHttpSysExtensionInfoFeature } } + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)] + public async Task Https_SetsIHttpSysRequestPropertyFeature() + { + using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext => + { + try + { + var requestPropertyFeature = httpContext.Features.Get(); + Assert.NotNull(requestPropertyFeature); + } + catch (Exception ex) + { + await httpContext.Response.WriteAsync(ex.ToString()); + } + }, LoggerFactory)) + { + string response = await SendRequestAsync(address); + Assert.Equal(string.Empty, response); + } + } + [ConditionalFact] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)] public async Task Https_SetsIHttpSysRequestTimingFeature() diff --git a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj index 08276e6a23fd..56f300b89198 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj +++ b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj @@ -32,6 +32,7 @@ +
diff --git a/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs b/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs new file mode 100644 index 000000000000..00c3ac024d32 --- /dev/null +++ b/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Time.Testing; +using Moq; +using static Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions; + +namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests; + +public class TlsListenerTests +{ + [Fact] + public void AddsAndUpdatesConnectionTimestamps() + { + // Arrange + var logger = Mock.Of(); + var timeProvider = new FakeTimeProvider(); + var callbackInvoked = false; + var tlsListener = new TlsListener(logger, (_, __) => { callbackInvoked = true; }, timeProvider); + + var features = Mock.Of(); + + // Act + tlsListener.InvokeTlsClientHelloCallback(connectionId: 1UL, features, + invokeTlsClientHelloCallback: (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); + + var originalTime = timeProvider.GetUtcNow(); + + // Assert + Assert.True(callbackInvoked); + Assert.Equal(originalTime, Assert.Single(tlsListener.ConnectionTimeStamps).Value); + + timeProvider.Advance(TimeSpan.FromSeconds(1)); + callbackInvoked = false; + // Update the timestamp + tlsListener.InvokeTlsClientHelloCallback(connectionId: 1UL, features, + invokeTlsClientHelloCallback: (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); + + // Callback should not be invoked again and the timestamp should be updated + Assert.False(callbackInvoked); + Assert.Equal(timeProvider.GetUtcNow(), Assert.Single(tlsListener.ConnectionTimeStamps).Value); + Assert.NotEqual(originalTime, timeProvider.GetUtcNow()); + } + + [Fact] + public async Task RemovesIdleConnections() + { + // Arrange + var logger = Mock.Of(); + var timeProvider = new FakeTimeProvider(); + using var tlsListener = new TlsListener(logger, (_, __) => { }, timeProvider); + + var features = Mock.Of(); + + bool InvokeCallback(IFeatureCollection f, TlsClientHelloCallback cb) + { + cb(f, ReadOnlySpan.Empty); + return true; + } + + // Act + tlsListener.InvokeTlsClientHelloCallback(connectionId: 1UL, features, InvokeCallback); + + // 1 less minute than the idle time cleanup + timeProvider.Advance(TimeSpan.FromMinutes(4)); + Assert.Single(tlsListener.ConnectionTimeStamps); + + tlsListener.InvokeTlsClientHelloCallback(connectionId: 2UL, features, InvokeCallback); + Assert.Equal(2, tlsListener.ConnectionTimeStamps.Count); + + // With the previous 4 minutes, this should be 5 minutes and remove the first connection + timeProvider.Advance(TimeSpan.FromMinutes(1)); + + var timeout = TimeSpan.FromSeconds(5); + while (timeout > TimeSpan.Zero) + { + // Wait for the cleanup loop to run + if (tlsListener.ConnectionTimeStamps.Count == 1) + { + break; + } + timeout -= TimeSpan.FromMilliseconds(100); + await Task.Delay(100); + } + + // Assert + Assert.Single(tlsListener.ConnectionTimeStamps); + Assert.Contains(2UL, tlsListener.ConnectionTimeStamps.Keys); + } + + [Fact] + public async Task EvictsOldestConnectionsWhenExceedingCacheSizeLimit() + { + // Arrange + var logger = Mock.Of(); + var timeProvider = new FakeTimeProvider(); + var tlsListener = new TlsListener(logger, (_, __) => { }, timeProvider); + var features = Mock.Of(); + + ulong i = 0; + for (; i < (ulong)tlsListener.CacheSizeLimit; i++) + { + tlsListener.InvokeTlsClientHelloCallback(i, features, (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); + } + + timeProvider.Advance(TimeSpan.FromSeconds(5)); + + for (; i < (ulong)tlsListener.CacheSizeLimit + 3; i++) + { + tlsListener.InvokeTlsClientHelloCallback(i, features, (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); + } + + // 'touch' first connection to update its timestamp + tlsListener.InvokeTlsClientHelloCallback(0, features, (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); + + // Make sure the cleanup loop has run to evict items since we're above the cache size limit + timeProvider.Advance(TimeSpan.FromMinutes(1)); + + var timeout = TimeSpan.FromSeconds(5); + while (timeout > TimeSpan.Zero) + { + // Wait for the cleanup loop to run + if (tlsListener.ConnectionTimeStamps.Count == tlsListener.CacheSizeLimit) + { + break; + } + timeout -= TimeSpan.FromMilliseconds(100); + await Task.Delay(100); + } + + Assert.Equal(tlsListener.CacheSizeLimit, tlsListener.ConnectionTimeStamps.Count); + Assert.Contains(0UL, tlsListener.ConnectionTimeStamps.Keys); + // 3 newest connections should be present + Assert.Contains(i - 1, tlsListener.ConnectionTimeStamps.Keys); + Assert.Contains(i - 2, tlsListener.ConnectionTimeStamps.Keys); + Assert.Contains(i - 3, tlsListener.ConnectionTimeStamps.Keys); + } +} diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.cpp b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.cpp index 9b12cd0132b4..8fb960261590 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.cpp @@ -197,7 +197,7 @@ HostFxrResolver::TryGetHostFxrPath( size_t size = MAX_PATH * 2; hostfxrPath.resize(size); - get_hostfxr_parameters params; + get_hostfxr_parameters params{}; params.size = sizeof(get_hostfxr_parameters); params.assembly_path = applicationPath.c_str(); params.dotnet_root = dotnetRoot.c_str(); @@ -393,7 +393,7 @@ HostFxrResolver::GetAbsolutePathToDotnetFromHostfxr(const fs::path& hostfxrPath) // Tries to call where.exe to find the location of dotnet.exe. // Will check that the bitness of dotnet matches the current // worker process bitness. -// Returns true if a valid dotnet was found, else false.R +// Returns true if a valid dotnet was found, else false. // std::optional HostFxrResolver::InvokeWhereToFindDotnet() @@ -415,8 +415,7 @@ HostFxrResolver::InvokeWhereToFindDotnet() DWORD dwExitCode; STRU struDotnetSubstring; STRU struDotnetLocationsString; - DWORD dwNumBytesRead; - DWORD dwBinaryType; + DWORD dwNumBytesRead = 0; INT index = 0; INT prevIndex = 0; std::optional result; @@ -521,14 +520,7 @@ HostFxrResolver::InvokeWhereToFindDotnet() LOG_INFOF(L"Processing entry '%ls'", struDotnetSubstring.QueryStr()); - if (LOG_LAST_ERROR_IF(!GetBinaryTypeW(struDotnetSubstring.QueryStr(), &dwBinaryType))) - { - continue; - } - - LOG_INFOF(L"Binary type %d", dwBinaryType); - - if (fIsCurrentProcess64Bit == (dwBinaryType == SCS_64BIT_BINARY)) + if (fIsCurrentProcess64Bit == IsX64(struDotnetSubstring.QueryStr())) { // The bitness of dotnet matched with the current worker process bitness. return std::make_optional(struDotnetSubstring.QueryStr()); @@ -539,6 +531,62 @@ HostFxrResolver::InvokeWhereToFindDotnet() return result; } +BOOL HostFxrResolver::IsX64(const WCHAR* dotnetPath) +{ + // Errors while reading from the file shouldn't throw unless + // file.exception(bits) is set + std::ifstream file(dotnetPath, std::ios::binary); + if (!file.is_open()) + { + LOG_TRACEF(L"Failed to open file %ls", dotnetPath); + return false; + } + + // Read the DOS header + IMAGE_DOS_HEADER dosHeader{}; + file.read(reinterpret_cast(&dosHeader), sizeof(dosHeader)); + if (dosHeader.e_magic != IMAGE_DOS_SIGNATURE) // 'MZ' + { + LOG_TRACEF(L"%ls is not a valid executable file (missing MZ header).", dotnetPath); + return false; + } + + // Seek to the PE header + file.seekg(dosHeader.e_lfanew, std::ios::beg); + + // Read the PE header + DWORD peSignature{}; + file.read(reinterpret_cast(&peSignature), sizeof(peSignature)); + if (peSignature != IMAGE_NT_SIGNATURE) // 'PE\0\0' + { + LOG_TRACEF(L"%ls is not a valid PE file (missing PE header).", dotnetPath); + return false; + } + + // Read the file header + IMAGE_FILE_HEADER fileHeader{}; + file.read(reinterpret_cast(&fileHeader), sizeof(fileHeader)); + + // Read the optional header magic field + WORD magic{}; + file.read(reinterpret_cast(&magic), sizeof(magic)); + + // Determine the architecture based on the magic value + if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) + { + LOG_INFOF(L"%ls is 32-bit", dotnetPath); + return false; + } + else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) + { + LOG_INFOF(L"%ls is 64-bit", dotnetPath); + return true; + } + + LOG_INFOF(L"%ls is unknown architecture %i", dotnetPath, fileHeader.Machine); + return false; +} + std::optional HostFxrResolver::GetAbsolutePathToDotnetFromProgramFiles() { diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.h index 519f6df52c97..08ec650aec54 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.h @@ -74,6 +74,8 @@ class HostFxrResolver const std::filesystem::path & requestedPath ); + static BOOL IsX64(const WCHAR* dotnetPath); + struct LocalFreeDeleter { void operator ()(_In_ LPWSTR* ptr) const diff --git a/src/Servers/IIS/build/Build.Lib.Settings b/src/Servers/IIS/build/Build.Lib.Settings index 0dcba8c2011a..9327eb363771 100644 --- a/src/Servers/IIS/build/Build.Lib.Settings +++ b/src/Servers/IIS/build/Build.Lib.Settings @@ -9,7 +9,7 @@ - false + true _LIB;%(PreprocessorDefinitions) true diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 1febba391529..6549acb14fda 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation; /// internal sealed partial class UnixCertificateManager : CertificateManager { - private const UnixFileMode DirectoryPermissions = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute; + private const UnixFileMode DirectoryPermissions = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute; /// The name of an environment variable consumed by OpenSSL to locate certificates. private const string OpenSslCertificateDirectoryVariableName = "SSL_CERT_DIR"; @@ -62,18 +62,32 @@ public override TrustLevel GetTrustLevel(X509Certificate2 certificate) // Building the chain will check whether dotnet trusts the cert. We could, instead, // enumerate the Root store and/or look for the file in the OpenSSL directory, but // this tests the real-world behavior. - using var chain = new X509Chain(); - // This is just a heuristic for whether or not we should prompt the user to re-run with `--trust` - // so we don't need to check revocation (which doesn't really make sense for dev certs anyway) - chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; - if (chain.Build(certificate)) + var chain = new X509Chain(); + try { - sawTrustSuccess = true; + // This is just a heuristic for whether or not we should prompt the user to re-run with `--trust` + // so we don't need to check revocation (which doesn't really make sense for dev certs anyway) + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + if (chain.Build(certificate)) + { + sawTrustSuccess = true; + } + else + { + sawTrustFailure = true; + Log.UnixNotTrustedByDotnet(); + } } - else + finally { - sawTrustFailure = true; - Log.UnixNotTrustedByDotnet(); + // Disposing the chain does not dispose the elements we potentially built. + // Do the full walk manually to dispose. + for (var i = 0; i < chain.ChainElements.Count; i++) + { + chain.ChainElements[i].Certificate.Dispose(); + } + + chain.Dispose(); } // Will become the name of the file on disk and the nickname in the NSS DBs @@ -94,7 +108,7 @@ public override TrustLevel GetTrustLevel(X509Certificate2 certificate) var certPath = Path.Combine(sslCertDir, certificateNickname + ".pem"); if (File.Exists(certPath)) { - var candidate = new X509Certificate2(certPath); + using var candidate = new X509Certificate2(certPath); if (AreCertificatesEqual(certificate, candidate)) { foundCert = true; @@ -161,7 +175,7 @@ protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certifi store.Open(OpenFlags.ReadWrite); store.Add(certificate); store.Close(); - }; + } return certificate; } diff --git a/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs b/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs index 7fed60b434b1..9fce59c69c36 100644 --- a/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs +++ b/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs @@ -28,6 +28,7 @@ internal static class ErrorCodes internal const uint ERROR_HANDLE_EOF = 38; internal const uint ERROR_NOT_SUPPORTED = 50; internal const uint ERROR_INVALID_PARAMETER = 87; + internal const uint ERROR_INSUFFICIENT_BUFFER = 122; internal const uint ERROR_INVALID_NAME = 123; internal const uint ERROR_ALREADY_EXISTS = 183; internal const uint ERROR_MORE_DATA = 234; diff --git a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs index b58bad9e800e..ad4f63eaacd7 100644 --- a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs +++ b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs @@ -31,9 +31,11 @@ internal unsafe class NativeRequestContext : IDisposable private MemoryHandle _memoryHandle; private readonly int _bufferAlignment; private readonly bool _permanentlyPinned; - private bool _disposed; private IReadOnlyDictionary>? _requestInfo; + private bool _disposed; + private bool _pinsReleased; + [MemberNotNullWhen(false, nameof(_backingBuffer))] private bool PermanentlyPinned => _permanentlyPinned; @@ -168,6 +170,11 @@ internal uint Size } } + /// + /// Shows whether was already invoked on this native request context + /// + internal bool PinsReleased => _pinsReleased; + // ReleasePins() should be called exactly once. It must be called before Dispose() is called, which means it must be called // before an object (Request) which closes the RequestContext on demand is returned to the application. internal void ReleasePins() @@ -177,6 +184,7 @@ internal void ReleasePins() _memoryHandle.Dispose(); _memoryHandle = default; _nativeRequest = null; + _pinsReleased = true; } public bool TryGetTimestamp(HttpSysRequestTimingType timestampType, out long timestamp) diff --git a/src/Shared/ThrowHelpers/ArgumentNullThrowHelper.cs b/src/Shared/ThrowHelpers/ArgumentNullThrowHelper.cs index fc1d5c847d74..e83e87423745 100644 --- a/src/Shared/ThrowHelpers/ArgumentNullThrowHelper.cs +++ b/src/Shared/ThrowHelpers/ArgumentNullThrowHelper.cs @@ -30,6 +30,29 @@ public static void ThrowIfNull( #endif } + /// Throws an if is null or empty. + /// The argument to validate as non-null and non-empty. + /// The name of the parameter with which corresponds. + public static void ThrowIfNullOrEmpty( +#if INTERNAL_NULLABLE_ATTRIBUTES || NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + [NotNull] +#endif + string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { +#if !NET7_0_OR_GREATER || NETSTANDARD || NETFRAMEWORK + if (argument is null) + { + Throw(paramName); + } + else if (argument.Length == 0) + { + throw new ArgumentException("Must not be null or empty", paramName); + } +#else + ArgumentException.ThrowIfNullOrEmpty(argument, paramName); +#endif + } + #if !NET7_0_OR_GREATER || NETSTANDARD || NETFRAMEWORK #if INTERNAL_NULLABLE_ATTRIBUTES || NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER [DoesNotReturn] diff --git a/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/GsonHubProtocol.java b/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/GsonHubProtocol.java index 042ca484806f..4b0c8848e816 100644 --- a/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/GsonHubProtocol.java +++ b/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/GsonHubProtocol.java @@ -126,7 +126,14 @@ public List parseMessages(ByteBuffer payload, InvocationBinder binde } break; case "headers": - throw new RuntimeException("Headers not implemented yet."); + // Parse headers as Map but don't store for now as it's unused + reader.beginObject(); + while (reader.hasNext()) { + reader.nextName(); // Read the key + reader.nextString(); // Read the value + } + reader.endObject(); + break; default: // Skip unknown property, allows new clients to still work with old protocols reader.skipValue(); diff --git a/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/GsonHubProtocolTest.java b/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/GsonHubProtocolTest.java index d696a74850eb..2ecfd483c7f5 100644 --- a/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/GsonHubProtocolTest.java +++ b/src/SignalR/clients/java/signalr/test/src/main/java/com/microsoft/signalr/GsonHubProtocolTest.java @@ -527,4 +527,98 @@ public void canRegisterTypeAdaptorWithoutAffectingJsonProtocol() { assertEquals(3, (int) invocationMessage.getArguments()[0]); assertEquals("four", invocationMessage.getArguments()[1]); } + + @Test + public void canParseInvocationMessageWithHeaders() { + String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42],\"headers\":{\"a\":\"b\",\"c\":\"d\"}}\u001E"; + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class }, null); + + List messages = hubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(1, messages.size()); + + assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType()); + InvocationMessage invocationMessage = (InvocationMessage) messages.get(0); + + assertEquals("test", invocationMessage.getTarget()); + assertEquals(null, invocationMessage.getInvocationId()); + int messageResult = (int)invocationMessage.getArguments()[0]; + assertEquals(42, messageResult); + // Headers are parsed but not stored, so we just verify the message was processed successfully + } + + @Test + public void canParseInvocationMessageWithEmptyHeaders() { + String stringifiedMessage = "{\"type\":1,\"target\":\"test\",\"arguments\":[42],\"headers\":{}}\u001E"; + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class }, null); + + List messages = hubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(1, messages.size()); + + assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType()); + InvocationMessage invocationMessage = (InvocationMessage) messages.get(0); + + assertEquals("test", invocationMessage.getTarget()); + int messageResult = (int)invocationMessage.getArguments()[0]; + assertEquals(42, messageResult); + } + + @Test + public void canParseCompletionMessageWithHeaders() { + String stringifiedMessage = "{\"type\":3,\"invocationId\":\"1\",\"result\":42,\"headers\":{\"a\":\"b\",\"c\":\"d\"}}\u001E"; + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(null, int.class); + + List messages = hubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(1, messages.size()); + + assertEquals(HubMessageType.COMPLETION, messages.get(0).getMessageType()); + CompletionMessage completionMessage = (CompletionMessage) messages.get(0); + assertEquals("1", completionMessage.getInvocationId()); + assertEquals(42, completionMessage.getResult()); + assertEquals(null, completionMessage.getError()); + } + + @Test + public void canParseStreamItemMessageWithHeaders() { + String stringifiedMessage = "{\"type\":2,\"invocationId\":\"1\",\"item\":\"test-item\",\"headers\":{\"a\":\"b\"}}\u001E"; + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(null, String.class); + + List messages = hubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(1, messages.size()); + + assertEquals(HubMessageType.STREAM_ITEM, messages.get(0).getMessageType()); + StreamItem streamItem = (StreamItem) messages.get(0); + assertEquals("1", streamItem.getInvocationId()); + assertEquals("test-item", streamItem.getItem()); + } + + @Test + public void canParseMessageWithHeadersInDifferentOrder() { + String stringifiedMessage = "{\"headers\":{\"First\":\"value1\",\"Second\":\"value2\"},\"type\":1,\"target\":\"test\",\"arguments\":[42]}\u001E"; + ByteBuffer message = TestUtils.stringToByteBuffer(stringifiedMessage); + TestBinder binder = new TestBinder(new Type[] { int.class }, null); + + List messages = hubProtocol.parseMessages(message, binder); + + assertNotNull(messages); + assertEquals(1, messages.size()); + + assertEquals(HubMessageType.INVOCATION, messages.get(0).getMessageType()); + InvocationMessage invocationMessage = (InvocationMessage) messages.get(0); + + assertEquals("test", invocationMessage.getTarget()); + int messageResult = (int)invocationMessage.getArguments()[0]; + assertEquals(42, messageResult); + } } diff --git a/src/SignalR/common/Shared/MessageBuffer.cs b/src/SignalR/common/Shared/MessageBuffer.cs index 17b9ae170fe0..f08fff86aa40 100644 --- a/src/SignalR/common/Shared/MessageBuffer.cs +++ b/src/SignalR/common/Shared/MessageBuffer.cs @@ -121,15 +121,16 @@ private async Task RunTimer() public ValueTask WriteAsync(SerializedHubMessage hubMessage, CancellationToken cancellationToken) { - return WriteAsyncCore(hubMessage.Message!, hubMessage.GetSerializedMessage(_protocol), cancellationToken); + // Default to HubInvocationMessage as that's the only type we use SerializedHubMessage for currently when Message is null. Should harden this in the future. + return WriteAsyncCore(hubMessage.Message?.GetType() ?? typeof(HubInvocationMessage), hubMessage.GetSerializedMessage(_protocol), cancellationToken); } public ValueTask WriteAsync(HubMessage hubMessage, CancellationToken cancellationToken) { - return WriteAsyncCore(hubMessage, _protocol.GetMessageBytes(hubMessage), cancellationToken); + return WriteAsyncCore(hubMessage.GetType(), _protocol.GetMessageBytes(hubMessage), cancellationToken); } - private async ValueTask WriteAsyncCore(HubMessage hubMessage, ReadOnlyMemory messageBytes, CancellationToken cancellationToken) + private async ValueTask WriteAsyncCore(Type hubMessageType, ReadOnlyMemory messageBytes, CancellationToken cancellationToken) { // TODO: Add backpressure based on message count if (_bufferedByteCount > _bufferLimit) @@ -158,7 +159,7 @@ private async ValueTask WriteAsyncCore(HubMessage hubMessage, ReadO await _writeLock.WaitAsync(cancellationToken: default).ConfigureAwait(false); try { - if (hubMessage is HubInvocationMessage invocationMessage) + if (typeof(HubInvocationMessage).IsAssignableFrom(hubMessageType)) { _totalMessageCount++; _bufferedByteCount += messageBytes.Length; diff --git a/src/SignalR/server/Core/src/SerializedHubMessage.cs b/src/SignalR/server/Core/src/SerializedHubMessage.cs index e355b0329128..9f4327a4cc58 100644 --- a/src/SignalR/server/Core/src/SerializedHubMessage.cs +++ b/src/SignalR/server/Core/src/SerializedHubMessage.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using Microsoft.AspNetCore.SignalR.Protocol; namespace Microsoft.AspNetCore.SignalR; @@ -40,6 +41,8 @@ public SerializedHubMessage(IReadOnlyList messages) /// The hub message for the cache. This will be serialized with an in to get the message's serialized representation. public SerializedHubMessage(HubMessage message) { + // Type currently only used for invocation messages, we should probably refactor it to be explicit about that e.g. new property for message type? + Debug.Assert(message.GetType().IsAssignableTo(typeof(HubInvocationMessage))); Message = message; } diff --git a/src/SignalR/server/SignalR/test/Internal/MessageBufferTests.cs b/src/SignalR/server/SignalR/test/Internal/MessageBufferTests.cs index e6f6248a733b..ac2a7888e942 100644 --- a/src/SignalR/server/SignalR/test/Internal/MessageBufferTests.cs +++ b/src/SignalR/server/SignalR/test/Internal/MessageBufferTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO.Pipelines; +using System.Text.Json; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.SignalR.Internal; @@ -169,6 +170,62 @@ public async Task UnAckedMessageResentOnReconnect() Assert.False(messageBuffer.ShouldProcessMessage(CompletionMessage.WithResult("1", null))); } + // Regression test for https://github.com/dotnet/aspnetcore/issues/55575 + [Fact] + public async Task UnAckedSerializedMessageResentOnReconnect() + { + var protocol = new JsonHubProtocol(); + var connection = new TestConnectionContext(); + var pipes = DuplexPipe.CreateConnectionPair(new PipeOptions(), new PipeOptions()); + connection.Transport = pipes.Transport; + using var messageBuffer = new MessageBuffer(connection, protocol, bufferLimit: 1000, NullLogger.Instance); + + var invocationMessage = new SerializedHubMessage([new SerializedMessage(protocol.Name, + protocol.GetMessageBytes(new InvocationMessage("method1", [1])))]); + await messageBuffer.WriteAsync(invocationMessage, default); + + var res = await pipes.Application.Input.ReadAsync(); + + var buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out var message)); + var parsedMessage = Assert.IsType(message); + Assert.Equal("method1", parsedMessage.Target); + Assert.Equal(1, ((JsonElement)Assert.Single(parsedMessage.Arguments)).GetInt32()); + + pipes.Application.Input.AdvanceTo(buffer.Start); + + DuplexPipe.UpdateConnectionPair(ref pipes, connection); + await messageBuffer.ResendAsync(pipes.Transport.Output); + + Assert.True(messageBuffer.ShouldProcessMessage(PingMessage.Instance)); + Assert.True(messageBuffer.ShouldProcessMessage(CompletionMessage.WithResult("1", null))); + Assert.True(messageBuffer.ShouldProcessMessage(new SequenceMessage(1))); + + res = await pipes.Application.Input.ReadAsync(); + + buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out message)); + var seqMessage = Assert.IsType(message); + Assert.Equal(1, seqMessage.SequenceId); + + pipes.Application.Input.AdvanceTo(buffer.Start); + + res = await pipes.Application.Input.ReadAsync(); + + buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out message)); + parsedMessage = Assert.IsType(message); + Assert.Equal("method1", parsedMessage.Target); + Assert.Equal(1, ((JsonElement)Assert.Single(parsedMessage.Arguments)).GetInt32()); + + pipes.Application.Input.AdvanceTo(buffer.Start); + + messageBuffer.ShouldProcessMessage(new SequenceMessage(1)); + + Assert.True(messageBuffer.ShouldProcessMessage(PingMessage.Instance)); + Assert.False(messageBuffer.ShouldProcessMessage(CompletionMessage.WithResult("1", null))); + } + [Fact] public async Task AckedMessageNotResentOnReconnect() { diff --git a/src/SignalR/server/StackExchangeRedis/test/Docker.cs b/src/SignalR/server/StackExchangeRedis/test/Docker.cs index 76fa6440e672..41315734daea 100644 --- a/src/SignalR/server/StackExchangeRedis/test/Docker.cs +++ b/src/SignalR/server/StackExchangeRedis/test/Docker.cs @@ -16,7 +16,8 @@ public class Docker { private static readonly string _exeSuffix = OperatingSystem.IsWindows() ? ".exe" : string.Empty; - private static readonly string _dockerContainerName = "redisTestContainer"; + private static readonly string _redisImageName = "dotnetdhmirror-f8bzbjakh8cga6ab.azurecr.io/library/redis:7.4"; + private static readonly string _dockerContainerName = "redisTestContainer74"; private static readonly string _dockerMonitorContainerName = _dockerContainerName + "Monitor"; private static readonly Lazy _instance = new Lazy(Create); @@ -112,7 +113,7 @@ void Run() // use static name 'redisTestContainer' so if the container doesn't get removed we don't keep adding more // use redis base docker image // 30 second timeout to allow redis image to be downloaded, should be a rare occurrence, only happening when a new version is released - RunProcessAndThrowIfFailed(_path, $"run --rm -p 6379:6379 --name {_dockerContainerName} -d redis", "redis", logger, TimeSpan.FromMinutes(1)); + RunProcessAndThrowIfFailed(_path, $"run --rm -p 6379:6379 --name {_dockerContainerName} -d {_redisImageName}", "redis", logger, TimeSpan.FromMinutes(1)); } } diff --git a/src/SignalR/server/StackExchangeRedis/test/RedisEndToEnd.cs b/src/SignalR/server/StackExchangeRedis/test/RedisEndToEnd.cs index 74cf30df19a9..c3173eb7b178 100644 --- a/src/SignalR/server/StackExchangeRedis/test/RedisEndToEnd.cs +++ b/src/SignalR/server/StackExchangeRedis/test/RedisEndToEnd.cs @@ -1,17 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +using System.Net.WebSockets; using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.Http.Connections.Client; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.AspNetCore.SignalR.Tests; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Xunit; namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests; @@ -37,6 +35,7 @@ public RedisEndToEndTests(RedisServerFixture serverFixture) [ConditionalTheory] [SkipIfDockerNotPresent] [MemberData(nameof(TransportTypesAndProtocolTypes))] + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/62435")] public async Task HubConnectionCanSendAndReceiveMessages(HttpTransportType transportType, string protocolName) { using (StartVerifiableLog()) @@ -57,6 +56,7 @@ public async Task HubConnectionCanSendAndReceiveMessages(HttpTransportType trans [ConditionalTheory] [SkipIfDockerNotPresent] [MemberData(nameof(TransportTypesAndProtocolTypes))] + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/62435")] public async Task HubConnectionCanSendAndReceiveGroupMessages(HttpTransportType transportType, string protocolName) { using (StartVerifiableLog()) @@ -118,6 +118,7 @@ public async Task CanSendAndReceiveUserMessagesFromMultipleConnectionsWithSameUs [ConditionalTheory] [SkipIfDockerNotPresent] [MemberData(nameof(TransportTypesAndProtocolTypes))] + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/62435")] public async Task CanSendAndReceiveUserMessagesWhenOneConnectionWithUserDisconnects(HttpTransportType transportType, string protocolName) { // Regression test: @@ -147,6 +148,7 @@ public async Task CanSendAndReceiveUserMessagesWhenOneConnectionWithUserDisconne [ConditionalTheory] [SkipIfDockerNotPresent] [MemberData(nameof(TransportTypesAndProtocolTypes))] + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/62435")] public async Task HubConnectionCanSendAndReceiveGroupMessagesGroupNameWithPatternIsTreatedAsLiteral(HttpTransportType transportType, string protocolName) { using (StartVerifiableLog()) @@ -211,7 +213,106 @@ public async Task CanSendAndReceiveUserMessagesUserNameWithPatternIsTreatedAsLit } } - private static HubConnection CreateConnection(string url, HttpTransportType transportType, IHubProtocol protocol, ILoggerFactory loggerFactory, string userName = null) + [ConditionalTheory] + [SkipIfDockerNotPresent] + [InlineData("messagepack")] + [InlineData("json")] + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/62435")] + public async Task StatefulReconnectPreservesMessageFromOtherServer(string protocolName) + { + using (StartVerifiableLog()) + { + var protocol = HubProtocolHelpers.GetHubProtocol(protocolName); + + ClientWebSocket innerWs = null; + WebSocketWrapper ws = null; + TaskCompletionSource reconnectTcs = null; + TaskCompletionSource startedReconnectTcs = null; + + var connection = CreateConnection(_serverFixture.FirstServer.Url + "/stateful", HttpTransportType.WebSockets, protocol, LoggerFactory, + customizeConnection: builder => + { + builder.WithStatefulReconnect(); + builder.Services.Configure(o => + { + // Replace the websocket creation for the first connection so we can make the client think there was an ungraceful closure + // Which will trigger the stateful reconnect flow + o.WebSocketFactory = async (context, token) => + { + if (reconnectTcs is null) + { + reconnectTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + startedReconnectTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + else + { + startedReconnectTcs.SetResult(); + // We only want to wait on the reconnect, not the initial connection attempt + await reconnectTcs.Task.DefaultTimeout(); + } + + innerWs = new ClientWebSocket(); + ws = new WebSocketWrapper(innerWs); + await innerWs.ConnectAsync(context.Uri, token); + + _ = Task.Run(async () => + { + try + { + while (innerWs.State == WebSocketState.Open) + { + var buffer = new byte[1024]; + var res = await innerWs.ReceiveAsync(buffer, default); + ws.SetReceiveResult((res, buffer.AsMemory(0, res.Count))); + } + } + // Log but ignore receive errors, that likely just means the connection closed + catch (Exception ex) + { + Logger.LogInformation(ex, "Error while reading from inner websocket"); + } + }); + + return ws; + }; + }); + }); + var secondConnection = CreateConnection(_serverFixture.SecondServer.Url + "/stateful", HttpTransportType.WebSockets, protocol, LoggerFactory); + + var tcs = new TaskCompletionSource(); + connection.On("SendToAll", message => tcs.TrySetResult(message)); + + var tcs2 = new TaskCompletionSource(); + secondConnection.On("SendToAll", message => tcs2.TrySetResult(message)); + + await connection.StartAsync().DefaultTimeout(); + await secondConnection.StartAsync().DefaultTimeout(); + + // Close first connection before the second connection sends a message to all clients + await ws.CloseOutputAsync(WebSocketCloseStatus.InternalServerError, statusDescription: null, default); + await startedReconnectTcs.Task.DefaultTimeout(); + + // Send to all clients, since both clients are on different servers this means the backplane will be used + // And we want to test that messages are still preserved for stateful reconnect purposes when a client disconnects + // But is on a different server from the original message sender. + await secondConnection.SendAsync("SendToAll", "test message").DefaultTimeout(); + + // Check that second connection still receives the message + Assert.Equal("test message", await tcs2.Task.DefaultTimeout()); + Assert.False(tcs.Task.IsCompleted); + + // allow first connection to reconnect + reconnectTcs.SetResult(); + + // Check that first connection received the message once it reconnected + Assert.Equal("test message", await tcs.Task.DefaultTimeout()); + + await connection.DisposeAsync().DefaultTimeout(); + } + } + + private static HubConnection CreateConnection(string url, HttpTransportType transportType, IHubProtocol protocol, ILoggerFactory loggerFactory, string userName = null, + Action customizeConnection = null) { var hubConnectionBuilder = new HubConnectionBuilder() .WithLoggerFactory(loggerFactory) @@ -225,6 +326,8 @@ private static HubConnection CreateConnection(string url, HttpTransportType tran hubConnectionBuilder.Services.AddSingleton(protocol); + customizeConnection?.Invoke(hubConnectionBuilder); + return hubConnectionBuilder.Build(); } @@ -253,4 +356,67 @@ public static IEnumerable TransportTypesAndProtocolTypes } } } + + internal sealed class WebSocketWrapper : WebSocket + { + private readonly WebSocket _inner; + private TaskCompletionSource<(WebSocketReceiveResult, ReadOnlyMemory)> _receiveTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public WebSocketWrapper(WebSocket inner) + { + _inner = inner; + } + + public override WebSocketCloseStatus? CloseStatus => _inner.CloseStatus; + + public override string CloseStatusDescription => _inner.CloseStatusDescription; + + public override WebSocketState State => _inner.State; + + public override string SubProtocol => _inner.SubProtocol; + + public override void Abort() + { + _inner.Abort(); + } + + public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + return _inner.CloseAsync(closeStatus, statusDescription, cancellationToken); + } + + public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + _receiveTcs.TrySetException(new IOException("force reconnect")); + return Task.CompletedTask; + } + + public override void Dispose() + { + _inner.Dispose(); + } + + public void SetReceiveResult((WebSocketReceiveResult, ReadOnlyMemory) result) + { + _receiveTcs.SetResult(result); + } + + public override async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) + { + var res = await _receiveTcs.Task; + // Handle zero-byte reads + if (buffer.Count == 0) + { + return res.Item1; + } + _receiveTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + res.Item2.CopyTo(buffer); + return res.Item1; + } + + public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + { + return _inner.SendAsync(buffer, messageType, endOfMessage, cancellationToken); + } + } } diff --git a/src/SignalR/server/StackExchangeRedis/test/Startup.cs b/src/SignalR/server/StackExchangeRedis/test/Startup.cs index 3fd461aed98e..1b55bd1cff53 100644 --- a/src/SignalR/server/StackExchangeRedis/test/Startup.cs +++ b/src/SignalR/server/StackExchangeRedis/test/Startup.cs @@ -33,6 +33,7 @@ public void Configure(IApplicationBuilder app) app.UseEndpoints(endpoints => { endpoints.MapHub("/echo"); + endpoints.MapHub("/stateful", o => o.AllowStatefulReconnects = true); }); } diff --git a/src/SignalR/server/StackExchangeRedis/test/StatefulHub.cs b/src/SignalR/server/StackExchangeRedis/test/StatefulHub.cs new file mode 100644 index 000000000000..1efa1d84fcd0 --- /dev/null +++ b/src/SignalR/server/StackExchangeRedis/test/StatefulHub.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests; + +public class StatefulHub : Hub +{ + public Task SendToAll(string message) + { + return Clients.All.SendAsync("SendToAll", message); + } +} diff --git a/src/submodules/googletest b/src/submodules/googletest index 24a9e940d481..373af2e3df71 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit 24a9e940d481f992ba852599c78bb2217362847b +Subproject commit 373af2e3df71599b87a40ce0e37164523849166b