diff --git a/.azure/pipelines/jobs/default-build.yml b/.azure/pipelines/jobs/default-build.yml index 65307d5d0ad7..15cf2272358c 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 ${{ else }}: 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 @@ -327,7 +327,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/NuGet.config b/NuGet.config index 9bc1f58f58ef..cd8902cfb5ec 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,10 +4,10 @@ - + - + @@ -30,10 +30,10 @@ - + - + diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props index 038221fceef4..4ff4b2c148c6 100644 --- a/eng/Baseline.Designer.props +++ b/eng/Baseline.Designer.props @@ -2,117 +2,117 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - - - + + + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 @@ -120,279 +120,279 @@ - 9.0.2 + 9.0.8 - - - + + + - - - + + + - - - + + + - 9.0.2 + 9.0.8 - - - + + + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - - + + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - - + + - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - - + + - 9.0.2 + 9.0.8 - - - - - + + + + + - 9.0.2 + 9.0.8 - - - - - + + + + + - 9.0.2 + 9.0.8 - - + + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - - - - - - + + + + + + - 9.0.2 + 9.0.8 - - - + + + - 9.0.2 + 9.0.8 - - - + + + - + - - - + + + - - - + + + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - + - + - + - 9.0.2 + 9.0.8 - - - - - - + + + + + + - + - - - - - - - + + + + + + + - - - - - - + + + + + + - + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - - + + - 9.0.2 + 9.0.8 - - + + - - + + - - + + - 9.0.2 + 9.0.8 - + - + - + - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 @@ -401,83 +401,83 @@ - 9.0.2 + 9.0.8 - - + + - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - - - + + + - + - - - - + + + + - - - - + + + + - - - - + + + + - 9.0.2 + 9.0.8 - - + + - + - - + + - 9.0.2 + 9.0.8 - - + + - 9.0.2 + 9.0.8 - - + + - 9.0.2 + 9.0.8 @@ -493,510 +493,510 @@ - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - - - + + + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - - + + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - - + + - - + + - - + + - 9.0.2 + 9.0.8 - - - - - - + + + + + + - - - - - + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - 9.0.2 + 9.0.8 - - + + - + - - + + - - - + + + - 9.0.2 + 9.0.8 - + - + - + - 9.0.2 + 9.0.8 - + - + - + - 9.0.2 + 9.0.8 - + - + - + - 9.0.2 + 9.0.8 - - - - + + + + - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - - + + - - + + - - + + - 9.0.2 + 9.0.8 - - - + + + - - - + + + - - - + + + - - - + + + - 9.0.2 + 9.0.8 - - + + - - + + - - + + - 9.0.2 + 9.0.8 - - - - - + + + + + - - - - + + + + - - - - - + + + + + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - - - + + + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - + - + - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - - - + + + - - - + + + - - - + + + - 9.0.2 + 9.0.8 - - - + + + - - - + + + - - - + + + - 9.0.2 + 9.0.8 - - - - + + + + - - - - + + + + - - - - + + + + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - - - + + + - - + + - - - + + + - 9.0.2 + 9.0.8 - 9.0.2 + 9.0.8 - + - 9.0.2 + 9.0.8 - + \ No newline at end of file diff --git a/eng/Baseline.xml b/eng/Baseline.xml index d15a11b1f2fd..71f542dc6973 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. --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/eng/SourceBuildPrebuiltBaseline.xml b/eng/SourceBuildPrebuiltBaseline.xml index 8e86375464ef..2c8d20d3234b 100644 --- a/eng/SourceBuildPrebuiltBaseline.xml +++ b/eng/SourceBuildPrebuiltBaseline.xml @@ -46,6 +46,13 @@ + + + + + + + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 28d773064132..3ebbb1853f12 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -9,325 +9,325 @@ --> - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 9275e9ac55e413546a09551c29d5227d6d009747 + 78871c83aac6c38eb5476c2f34aae98ef65314f5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 9275e9ac55e413546a09551c29d5227d6d009747 + 78871c83aac6c38eb5476c2f34aae98ef65314f5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 9275e9ac55e413546a09551c29d5227d6d009747 + 78871c83aac6c38eb5476c2f34aae98ef65314f5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 9275e9ac55e413546a09551c29d5227d6d009747 + 78871c83aac6c38eb5476c2f34aae98ef65314f5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 9275e9ac55e413546a09551c29d5227d6d009747 + 78871c83aac6c38eb5476c2f34aae98ef65314f5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 9275e9ac55e413546a09551c29d5227d6d009747 + 78871c83aac6c38eb5476c2f34aae98ef65314f5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 9275e9ac55e413546a09551c29d5227d6d009747 + 78871c83aac6c38eb5476c2f34aae98ef65314f5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 9275e9ac55e413546a09551c29d5227d6d009747 + 78871c83aac6c38eb5476c2f34aae98ef65314f5 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 https://github.com/dotnet/xdt @@ -367,9 +367,9 @@ bc1c3011064a493b0ca527df6fb7215e2e5cfa96 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 @@ -380,47 +380,47 @@ - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - f57e6dc747158ab7ade4e62a75a6750d16b771e8 + 893c2ebbd49952ca49e93298148af2d95a61a0a4 https://github.com/dotnet/winforms 9b822fd70005bf5632d12fe76811b97b3dd044e4 - + https://github.com/dotnet/arcade - 5da211e1c42254cb35e7ef3d5a8428fb24853169 + e29823691315ed6b3acff20d5bdf3b0be7628283 - + https://github.com/dotnet/arcade - 5da211e1c42254cb35e7ef3d5a8428fb24853169 + e29823691315ed6b3acff20d5bdf3b0be7628283 - + https://github.com/dotnet/arcade - 5da211e1c42254cb35e7ef3d5a8428fb24853169 + e29823691315ed6b3acff20d5bdf3b0be7628283 - + https://github.com/dotnet/arcade - 5da211e1c42254cb35e7ef3d5a8428fb24853169 + e29823691315ed6b3acff20d5bdf3b0be7628283 - + https://github.com/dotnet/arcade - 5da211e1c42254cb35e7ef3d5a8428fb24853169 + e29823691315ed6b3acff20d5bdf3b0be7628283 - + https://github.com/dotnet/arcade - 5da211e1c42254cb35e7ef3d5a8428fb24853169 + e29823691315ed6b3acff20d5bdf3b0be7628283 - + https://github.com/dotnet/extensions - c221abef4b4f1bf3fcf0bda27490e8b26bb479f4 + ed336d147b46d36edad3e9441398de636b67cf5d - + https://github.com/dotnet/extensions - c221abef4b4f1bf3fcf0bda27490e8b26bb479f4 + ed336d147b46d36edad3e9441398de636b67cf5d https://github.com/nuget/nuget.client diff --git a/eng/Versions.props b/eng/Versions.props index 3e45eded9065..a2d7c6b770e6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -8,10 +8,10 @@ 9 0 - 4 + 9 - false + true 8.0.1 *-* - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4-servicing.25163.5 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4-servicing.25163.5 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4-servicing.25163.5 - 9.0.4-servicing.25163.5 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9-servicing.25419.16 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9-servicing.25419.16 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9-servicing.25419.16 + 9.0.9-servicing.25419.16 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 - 9.0.4-servicing.25163.5 - 9.0.4 + 9.0.9-servicing.25419.16 + 9.0.9 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 - 9.3.0-preview.1.25156.1 - 9.3.0-preview.1.25156.1 + 9.9.0-preview.1.25409.1 + 9.9.0-preview.1.25409.1 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 - 9.0.4 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 + 9.0.9 4.11.0-3.24554.2 4.11.0-3.24554.2 @@ -166,10 +166,10 @@ 6.2.4 6.2.4 - 9.0.0-beta.25111.5 - 9.0.0-beta.25111.5 - 9.0.0-beta.25111.5 - 9.0.0-beta.25111.5 + 9.0.0-beta.25407.2 + 9.0.0-beta.25407.2 + 9.0.0-beta.25407.2 + 9.0.0-beta.25407.2 9.0.0-alpha.1.24575.1 @@ -227,13 +227,12 @@ 6.0.0 1.1.1 - 17.4.0 + 17.8.29 1.2.0 - 17.4.0 - 17.4.0 - 17.4.0 + 17.8.29 + 17.8.29 + 17.8.29 1.2.6 - 17.4.0 (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 e52525e2ebde..ae875b2ea090 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "9.0.103" + "version": "9.0.109" }, "tools": { - "dotnet": "9.0.103", + "dotnet": "9.0.109", "runtimes": { "dotnet/x86": [ "$(MicrosoftNETCoreBrowserDebugHostTransportVersion)" @@ -27,7 +27,7 @@ "jdk": "latest" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25111.5", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25111.5" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25407.2", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25407.2" } } diff --git a/src/Caching/StackExchangeRedis/src/RedisCache.cs b/src/Caching/StackExchangeRedis/src/RedisCache.cs index 4ecbf3628222..95bba7dd0088 100644 --- a/src/Caching/StackExchangeRedis/src/RedisCache.cs +++ b/src/Caching/StackExchangeRedis/src/RedisCache.cs @@ -53,7 +53,7 @@ private static RedisValue[] GetHashFields(bool getData) => getData private long _firstErrorTimeTicks; private long _previousErrorTimeTicks; - internal bool HybridCacheActive { get; set; } + internal virtual bool IsHybridCacheActive() => false; // StackExchange.Redis will also be trying to reconnect internally, // so limit how often we recreate the ConnectionMultiplexer instance @@ -378,7 +378,7 @@ private void TryAddSuffix(IConnectionMultiplexer connection) connection.AddLibraryNameSuffix("aspnet"); connection.AddLibraryNameSuffix("DC"); - if (HybridCacheActive) + if (IsHybridCacheActive()) { connection.AddLibraryNameSuffix("HC"); } diff --git a/src/Caching/StackExchangeRedis/src/RedisCacheImpl.cs b/src/Caching/StackExchangeRedis/src/RedisCacheImpl.cs index 67d262002eb3..0a58d43e136d 100644 --- a/src/Caching/StackExchangeRedis/src/RedisCacheImpl.cs +++ b/src/Caching/StackExchangeRedis/src/RedisCacheImpl.cs @@ -11,19 +11,20 @@ namespace Microsoft.Extensions.Caching.StackExchangeRedis; internal sealed class RedisCacheImpl : RedisCache { + private readonly IServiceProvider _services; + + internal override bool IsHybridCacheActive() + => _services.GetService() is not null; + public RedisCacheImpl(IOptions optionsAccessor, ILogger logger, IServiceProvider services) : base(optionsAccessor, logger) { - HybridCacheActive = IsHybridCacheDefined(services); + _services = services; // important: do not check for HybridCache here due to dependency - creates a cycle } public RedisCacheImpl(IOptions optionsAccessor, IServiceProvider services) : base(optionsAccessor) { - HybridCacheActive = IsHybridCacheDefined(services); + _services = services; // important: do not check for HybridCache here due to dependency - creates a cycle } - - // HybridCache optionally uses IDistributedCache; if we're here, then *we are* the DC - private static bool IsHybridCacheDefined(IServiceProvider services) - => services.GetService() is not null; } diff --git a/src/Caching/StackExchangeRedis/test/CacheServiceExtensionsTests.cs b/src/Caching/StackExchangeRedis/test/CacheServiceExtensionsTests.cs index 1d8ce4c3fd40..71e31d19928a 100644 --- a/src/Caching/StackExchangeRedis/test/CacheServiceExtensionsTests.cs +++ b/src/Caching/StackExchangeRedis/test/CacheServiceExtensionsTests.cs @@ -8,10 +8,12 @@ using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Moq; using Xunit; @@ -142,16 +144,46 @@ public void AddStackExchangeRedisCache_HybridCacheDetected(bool hybridCacheActiv services.AddStackExchangeRedisCache(options => { }); if (hybridCacheActive) { - services.TryAddSingleton(new DummyHybridCache()); + services.AddMemoryCache(); + services.TryAddSingleton(); } using var provider = services.BuildServiceProvider(); var cache = Assert.IsAssignableFrom(provider.GetRequiredService()); - Assert.Equal(hybridCacheActive, cache.HybridCacheActive); + Assert.Equal(hybridCacheActive, cache.IsHybridCacheActive()); } sealed class DummyHybridCache : HybridCache { + // emulate the layout from HybridCache in dotnet/extensions + public DummyHybridCache(IOptions options, IServiceProvider services) + { + if (services is null) + { + throw new ArgumentNullException(nameof(services)); + } + + var l1 = services.GetRequiredService(); + _ = options.Value; + var logger = services.GetService()?.CreateLogger(typeof(HybridCache)) ?? NullLogger.Instance; + // var clock = services.GetService() ?? TimeProvider.System; + var l2 = services.GetService(); // note optional + + // ignore L2 if it is really just the same L1, wrapped + // (note not just an "is" test; if someone has a custom subclass, who knows what it does?) + if (l2 is not null + && l2.GetType() == typeof(MemoryDistributedCache) + && l1.GetType() == typeof(MemoryCache)) + { + l2 = null; + } + + IHybridCacheSerializerFactory[] factories = services.GetServices().ToArray(); + Array.Reverse(factories); + } + + public class HybridCacheOptions { } + public override ValueTask GetOrCreateAsync(string key, TState state, Func> factory, HybridCacheEntryOptions options = null, IEnumerable tags = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(); diff --git a/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs index 94b6b80f53a1..0ba10d655cdf 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.E2ETesting; +using Microsoft.AspNetCore.InternalTesting; using TestServer; using Xunit.Abstractions; using Components.TestServer.RazorComponents; @@ -519,6 +520,7 @@ public void NavigationManagerUriGetsUpdatedOnEnhancedNavigation_OnlyServerOrWebA [Theory] [InlineData("server")] [InlineData("wasm")] + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/61143")] public void NavigationManagerUriGetsUpdatedOnEnhancedNavigation_BothServerAndWebAssembly(string runtimeThatInvokedNavigation) { Navigate($"{ServerPathBase}/nav"); diff --git a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs index 4b35b0c5eed0..53fd2addd404 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) { @@ -1340,6 +1340,7 @@ void AssertUiState(string expectedStringValue, bool expectedBoolValue) } [Fact] + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/61144")] public void RadioButtonGetsResetAfterSubmittingEnhancedForm() { GoTo("forms/form-with-checkbox-and-radio-button"); 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.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs b/src/Http/Http.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs index 33ecc3ff1ea8..b4071866534b 100644 --- a/src/Http/Http.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs +++ b/src/Http/Http.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs @@ -214,7 +214,7 @@ public void GetListT_StringWithQualityHeaderValidValue_Success() public void GetListT_CookieHeaderValue_Success() { var context = new DefaultHttpContext(); - context.Request.Headers.Cookie = "cookie1=a,cookie2=b"; + context.Request.Headers.Cookie = "cookie1=a;cookie2=b"; var result = context.Request.GetTypedHeaders().GetList(HeaderNames.Cookie); diff --git a/src/Http/Http/test/RequestCookiesCollectionTests.cs b/src/Http/Http/test/RequestCookiesCollectionTests.cs index e08a53f29711..d584f2db0599 100644 --- a/src/Http/Http/test/RequestCookiesCollectionTests.cs +++ b/src/Http/Http/test/RequestCookiesCollectionTests.cs @@ -33,11 +33,18 @@ 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("er=;,err=,errr=\\,errrr=\"", 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 })); diff --git a/src/Http/Shared/CookieHeaderParserShared.cs b/src/Http/Shared/CookieHeaderParserShared.cs index e4b1d83e519a..0eb1c64d533a 100644 --- a/src/Http/Shared/CookieHeaderParserShared.cs +++ b/src/Http/Shared/CookieHeaderParserShared.cs @@ -89,6 +89,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; } @@ -97,6 +108,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; } @@ -112,7 +134,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; } @@ -125,8 +147,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); @@ -136,6 +158,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 f55c2b48340d..fe7eb5ee9003 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/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj b/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj index a43b8e998eec..d36fe09d60c4 100644 --- a/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj +++ b/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj @@ -90,11 +90,27 @@ %(RecursiveDir)%(FileName)%(Extension) - - + + - + + @@ -106,7 +122,7 @@ > - + - - + + + Discovered + + + Discovered + + + + + - + - + diff --git a/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs b/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs index 6b2a118cb132..72cae1507a0e 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(); } @@ -220,19 +233,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/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs b/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs index 830b0375d396..f05c595b5c2b 100644 --- a/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs +++ b/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs @@ -136,6 +136,8 @@ internal sealed partial class OpenApiJsonSchema { type = "array"; var array = new OpenApiArray(); + // Read to process JsonTokenType.StartArray before advancing + reader.Read(); while (reader.TokenType != JsonTokenType.EndArray) { array.Add(ReadOpenApiAny(ref reader)); diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs index 1c7cb1ba5746..6f87374b1c92 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs @@ -722,4 +722,93 @@ public static bool TryParse(string value, out Student result) return true; } } + + // Regression test for https://github.com/dotnet/aspnetcore/issues/62023 + // Testing that the array parsing in our OpenApiJsonSchema works + [Fact] + public async Task CustomConverterThatOutputsArrayWithDefaultValue() + { + // Arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.Converters.Add(new EnumArrayTypeConverter()); + }); + var builder = CreateBuilder(serviceCollection); + + // Act + builder.MapPost("/api", (EnumArrayType e = EnumArrayType.None) => { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[OperationType.Post]; + var param = Assert.Single(operation.Parameters); + Assert.NotNull(param.Schema); + Assert.IsType(param.Schema.Default); + // Type is null, it's up to the user to configure this via a custom schema + // transformer for types with a converter. + Assert.Null(param.Schema.Type); + }); + } + + [Fact] + public async Task CustomConverterThatOutputsObjectWithDefaultValue() + { + // Arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.Converters.Add(new EnumObjectTypeConverter()); + }); + var builder = CreateBuilder(serviceCollection); + + // Act + builder.MapPost("/api", (EnumArrayType e = EnumArrayType.None) => { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[OperationType.Post]; + var param = Assert.Single(operation.Parameters); + Assert.NotNull(param.Schema); + Assert.IsType(param.Schema.Default); + // Type is null, it's up to the user to configure this via a custom schema + // transformer for types with a converter. + Assert.Null(param.Schema.Type); + }); + } + + public enum EnumArrayType + { + None = 1 + } + + public class EnumArrayTypeConverter : JsonConverter + { + public override EnumArrayType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new EnumArrayType(); + } + + public override void Write(Utf8JsonWriter writer, EnumArrayType value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + writer.WriteEndArray(); + } + } + + public class EnumObjectTypeConverter : JsonConverter + { + public override EnumArrayType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new EnumArrayType(); + } + + public override void Write(Utf8JsonWriter writer, EnumArrayType value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + } } 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/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 c583c4d370ed..9212fc475cf7 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -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 = X509CertificateLoader.LoadCertificateFromFile(certPath); + using var candidate = X509CertificateLoader.LoadCertificateFromFile(certPath); if (AreCertificatesEqual(certificate, candidate)) { foundCert = true; 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/Microsoft.AspNetCore.SignalR.Tests/Internal/MessageBufferTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Internal/MessageBufferTests.cs index a7e87d2b8bda..540ea462e199 100644 --- a/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Internal/MessageBufferTests.cs +++ b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Internal/MessageBufferTests.cs @@ -2,11 +2,12 @@ // 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.InternalTesting; using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Protocol; -using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; @@ -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 63445379ac35..f3501fb69c1e 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.InternalTesting; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.AspNetCore.SignalR.Tests; -using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Xunit; namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests; @@ -37,6 +35,7 @@ public RedisEndToEndTests(RedisServerFixture serverFixture) [ConditionalTheory] [SkipIfDockerNotPresent] [MemberData(nameof(TransportTypesAndProtocolTypes))] + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/62435")] public async Task HubConnectionCanSendAndReceiveMessages(HttpTransportType transportType, string protocolName) { using (StartVerifiableLog()) @@ -57,6 +56,7 @@ public async Task HubConnectionCanSendAndReceiveMessages(HttpTransportType trans [ConditionalTheory] [SkipIfDockerNotPresent] [MemberData(nameof(TransportTypesAndProtocolTypes))] + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/62435")] public async Task HubConnectionCanSendAndReceiveGroupMessages(HttpTransportType transportType, string protocolName) { using (StartVerifiableLog()) @@ -118,6 +118,7 @@ public async Task CanSendAndReceiveUserMessagesFromMultipleConnectionsWithSameUs [ConditionalTheory] [SkipIfDockerNotPresent] [MemberData(nameof(TransportTypesAndProtocolTypes))] + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/62435")] public async Task CanSendAndReceiveUserMessagesWhenOneConnectionWithUserDisconnects(HttpTransportType transportType, string protocolName) { // Regression test: @@ -147,6 +148,7 @@ public async Task CanSendAndReceiveUserMessagesWhenOneConnectionWithUserDisconne [ConditionalTheory] [SkipIfDockerNotPresent] [MemberData(nameof(TransportTypesAndProtocolTypes))] + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/62435")] public async Task HubConnectionCanSendAndReceiveGroupMessagesGroupNameWithPatternIsTreatedAsLiteral(HttpTransportType transportType, string protocolName) { using (StartVerifiableLog()) @@ -211,7 +213,106 @@ public async Task CanSendAndReceiveUserMessagesUserNameWithPatternIsTreatedAsLit } } - private static HubConnection CreateConnection(string url, HttpTransportType transportType, IHubProtocol protocol, ILoggerFactory loggerFactory, string userName = null) + [ConditionalTheory] + [SkipIfDockerNotPresent] + [InlineData("messagepack")] + [InlineData("json")] + [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/62435")] + public async Task StatefulReconnectPreservesMessageFromOtherServer(string protocolName) + { + using (StartVerifiableLog()) + { + var protocol = HubProtocolHelpers.GetHubProtocol(protocolName); + + ClientWebSocket innerWs = null; + WebSocketWrapper ws = null; + TaskCompletionSource reconnectTcs = null; + TaskCompletionSource startedReconnectTcs = null; + + var connection = CreateConnection(_serverFixture.FirstServer.Url + "/stateful", HttpTransportType.WebSockets, protocol, LoggerFactory, + customizeConnection: builder => + { + builder.WithStatefulReconnect(); + builder.Services.Configure(o => + { + // Replace the websocket creation for the first connection so we can make the client think there was an ungraceful closure + // Which will trigger the stateful reconnect flow + o.WebSocketFactory = async (context, token) => + { + if (reconnectTcs is null) + { + reconnectTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + startedReconnectTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + else + { + startedReconnectTcs.SetResult(); + // We only want to wait on the reconnect, not the initial connection attempt + await reconnectTcs.Task.DefaultTimeout(); + } + + innerWs = new ClientWebSocket(); + ws = new WebSocketWrapper(innerWs); + await innerWs.ConnectAsync(context.Uri, token); + + _ = Task.Run(async () => + { + try + { + while (innerWs.State == WebSocketState.Open) + { + var buffer = new byte[1024]; + var res = await innerWs.ReceiveAsync(buffer, default); + ws.SetReceiveResult((res, buffer.AsMemory(0, res.Count))); + } + } + // Log but ignore receive errors, that likely just means the connection closed + catch (Exception ex) + { + Logger.LogInformation(ex, "Error while reading from inner websocket"); + } + }); + + return ws; + }; + }); + }); + var secondConnection = CreateConnection(_serverFixture.SecondServer.Url + "/stateful", HttpTransportType.WebSockets, protocol, LoggerFactory); + + var tcs = new TaskCompletionSource(); + connection.On("SendToAll", message => tcs.TrySetResult(message)); + + var tcs2 = new TaskCompletionSource(); + secondConnection.On("SendToAll", message => tcs2.TrySetResult(message)); + + await connection.StartAsync().DefaultTimeout(); + await secondConnection.StartAsync().DefaultTimeout(); + + // Close first connection before the second connection sends a message to all clients + await ws.CloseOutputAsync(WebSocketCloseStatus.InternalServerError, statusDescription: null, default); + await startedReconnectTcs.Task.DefaultTimeout(); + + // Send to all clients, since both clients are on different servers this means the backplane will be used + // And we want to test that messages are still preserved for stateful reconnect purposes when a client disconnects + // But is on a different server from the original message sender. + await secondConnection.SendAsync("SendToAll", "test message").DefaultTimeout(); + + // Check that second connection still receives the message + Assert.Equal("test message", await tcs2.Task.DefaultTimeout()); + Assert.False(tcs.Task.IsCompleted); + + // allow first connection to reconnect + reconnectTcs.SetResult(); + + // Check that first connection received the message once it reconnected + Assert.Equal("test message", await tcs.Task.DefaultTimeout()); + + await connection.DisposeAsync().DefaultTimeout(); + } + } + + private static HubConnection CreateConnection(string url, HttpTransportType transportType, IHubProtocol protocol, ILoggerFactory loggerFactory, string userName = null, + Action customizeConnection = null) { var hubConnectionBuilder = new HubConnectionBuilder() .WithLoggerFactory(loggerFactory) @@ -225,6 +326,8 @@ private static HubConnection CreateConnection(string url, HttpTransportType tran hubConnectionBuilder.Services.AddSingleton(protocol); + customizeConnection?.Invoke(hubConnectionBuilder); + return hubConnectionBuilder.Build(); } @@ -253,4 +356,67 @@ public static IEnumerable TransportTypesAndProtocolTypes } } } + + internal sealed class WebSocketWrapper : WebSocket + { + private readonly WebSocket _inner; + private TaskCompletionSource<(WebSocketReceiveResult, ReadOnlyMemory)> _receiveTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public WebSocketWrapper(WebSocket inner) + { + _inner = inner; + } + + public override WebSocketCloseStatus? CloseStatus => _inner.CloseStatus; + + public override string CloseStatusDescription => _inner.CloseStatusDescription; + + public override WebSocketState State => _inner.State; + + public override string SubProtocol => _inner.SubProtocol; + + public override void Abort() + { + _inner.Abort(); + } + + public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + return _inner.CloseAsync(closeStatus, statusDescription, cancellationToken); + } + + public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + _receiveTcs.TrySetException(new IOException("force reconnect")); + return Task.CompletedTask; + } + + public override void Dispose() + { + _inner.Dispose(); + } + + public void SetReceiveResult((WebSocketReceiveResult, ReadOnlyMemory) result) + { + _receiveTcs.SetResult(result); + } + + public override async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) + { + var res = await _receiveTcs.Task; + // Handle zero-byte reads + if (buffer.Count == 0) + { + return res.Item1; + } + _receiveTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + res.Item2.CopyTo(buffer); + return res.Item1; + } + + public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + { + return _inner.SendAsync(buffer, messageType, endOfMessage, cancellationToken); + } + } } diff --git a/src/SignalR/server/StackExchangeRedis/test/Startup.cs b/src/SignalR/server/StackExchangeRedis/test/Startup.cs index 3fd461aed98e..1b55bd1cff53 100644 --- a/src/SignalR/server/StackExchangeRedis/test/Startup.cs +++ b/src/SignalR/server/StackExchangeRedis/test/Startup.cs @@ -33,6 +33,7 @@ public void Configure(IApplicationBuilder app) app.UseEndpoints(endpoints => { endpoints.MapHub("/echo"); + endpoints.MapHub("/stateful", o => o.AllowStatefulReconnects = true); }); } diff --git a/src/SignalR/server/StackExchangeRedis/test/StatefulHub.cs b/src/SignalR/server/StackExchangeRedis/test/StatefulHub.cs new file mode 100644 index 000000000000..1efa1d84fcd0 --- /dev/null +++ b/src/SignalR/server/StackExchangeRedis/test/StatefulHub.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests; + +public class StatefulHub : Hub +{ + public Task SendToAll(string message) + { + return Clients.All.SendAsync("SendToAll", message); + } +} diff --git a/src/submodules/googletest b/src/submodules/googletest index 24a9e940d481..373af2e3df71 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit 24a9e940d481f992ba852599c78bb2217362847b +Subproject commit 373af2e3df71599b87a40ce0e37164523849166b