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