diff --git a/NuGet.config b/NuGet.config index f8b0b0f8f7db..9bc1f58f58ef 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,10 +4,10 @@ - + - + @@ -30,10 +30,10 @@ - + - + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index ccae9cf9ba4d..28d773064132 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -9,325 +9,325 @@ --> - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 68c7e19496df80819410fc6de1682a194aad33d3 + 9275e9ac55e413546a09551c29d5227d6d009747 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 68c7e19496df80819410fc6de1682a194aad33d3 + 9275e9ac55e413546a09551c29d5227d6d009747 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 68c7e19496df80819410fc6de1682a194aad33d3 + 9275e9ac55e413546a09551c29d5227d6d009747 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 68c7e19496df80819410fc6de1682a194aad33d3 + 9275e9ac55e413546a09551c29d5227d6d009747 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 68c7e19496df80819410fc6de1682a194aad33d3 + 9275e9ac55e413546a09551c29d5227d6d009747 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 68c7e19496df80819410fc6de1682a194aad33d3 + 9275e9ac55e413546a09551c29d5227d6d009747 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 68c7e19496df80819410fc6de1682a194aad33d3 + 9275e9ac55e413546a09551c29d5227d6d009747 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 68c7e19496df80819410fc6de1682a194aad33d3 + 9275e9ac55e413546a09551c29d5227d6d009747 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 https://github.com/dotnet/xdt @@ -367,9 +367,9 @@ bc1c3011064a493b0ca527df6fb7215e2e5cfa96 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 @@ -380,47 +380,47 @@ - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 831d23e56149cd59c40fc00c7feb7c5334bd19c4 + f57e6dc747158ab7ade4e62a75a6750d16b771e8 https://github.com/dotnet/winforms 9b822fd70005bf5632d12fe76811b97b3dd044e4 - + https://github.com/dotnet/arcade - bac7e1caea791275b7c3ccb4cb75fd6a04a26618 + 5da211e1c42254cb35e7ef3d5a8428fb24853169 - + https://github.com/dotnet/arcade - bac7e1caea791275b7c3ccb4cb75fd6a04a26618 + 5da211e1c42254cb35e7ef3d5a8428fb24853169 - + https://github.com/dotnet/arcade - bac7e1caea791275b7c3ccb4cb75fd6a04a26618 + 5da211e1c42254cb35e7ef3d5a8428fb24853169 - + https://github.com/dotnet/arcade - bac7e1caea791275b7c3ccb4cb75fd6a04a26618 + 5da211e1c42254cb35e7ef3d5a8428fb24853169 - + https://github.com/dotnet/arcade - bac7e1caea791275b7c3ccb4cb75fd6a04a26618 + 5da211e1c42254cb35e7ef3d5a8428fb24853169 - + https://github.com/dotnet/arcade - bac7e1caea791275b7c3ccb4cb75fd6a04a26618 + 5da211e1c42254cb35e7ef3d5a8428fb24853169 - + https://github.com/dotnet/extensions - ca2fe808b3d6c55817467f46ca58657456b4a928 + c221abef4b4f1bf3fcf0bda27490e8b26bb479f4 - + https://github.com/dotnet/extensions - ca2fe808b3d6c55817467f46ca58657456b4a928 + c221abef4b4f1bf3fcf0bda27490e8b26bb479f4 https://github.com/nuget/nuget.client diff --git a/eng/Versions.props b/eng/Versions.props index 8f45c2dcc337..3e45eded9065 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -8,10 +8,10 @@ 9 0 - 3 + 4 - true + false 8.0.1 *-* - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3-servicing.25111.13 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3-servicing.25111.13 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3-servicing.25111.13 - 9.0.3-servicing.25111.13 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 + 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.3-servicing.25111.13 - 9.0.3 + 9.0.4-servicing.25163.5 + 9.0.4 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 - 9.3.0-preview.1.25107.9 - 9.3.0-preview.1.25107.9 + 9.3.0-preview.1.25156.1 + 9.3.0-preview.1.25156.1 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 - 9.0.3 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 + 9.0.4 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.25077.4 - 9.0.0-beta.25077.4 - 9.0.0-beta.25077.4 - 9.0.0-beta.25077.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-alpha.1.24575.1 diff --git a/eng/common/core-templates/steps/source-build.yml b/eng/common/core-templates/steps/source-build.yml index 29515fc23f64..2915d29bb7f6 100644 --- a/eng/common/core-templates/steps/source-build.yml +++ b/eng/common/core-templates/steps/source-build.yml @@ -37,7 +37,7 @@ steps: # in the default public locations. internalRuntimeDownloadArgs= if [ '$(dotnetbuilds-internal-container-read-token-base64)' != '$''(dotnetbuilds-internal-container-read-token-base64)' ]; then - internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://ci.dot.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' + internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://dotnetbuilds.blob.core.windows.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://dotnetbuilds.blob.core.windows.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' fi buildConfig=Release diff --git a/global.json b/global.json index 97b2439c9343..e52525e2ebde 100644 --- a/global.json +++ b/global.json @@ -27,7 +27,7 @@ "jdk": "latest" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25077.4", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25077.4" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25111.5", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25111.5" } } diff --git a/src/Components/CustomElements/src/Microsoft.AspNetCore.Components.CustomElements.csproj b/src/Components/CustomElements/src/Microsoft.AspNetCore.Components.CustomElements.csproj index 4c5ae75fa8a4..5d27de7c059f 100644 --- a/src/Components/CustomElements/src/Microsoft.AspNetCore.Components.CustomElements.csproj +++ b/src/Components/CustomElements/src/Microsoft.AspNetCore.Components.CustomElements.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -18,7 +18,7 @@ - + <_JsBuildOutput Include="$(InteropWorkingDir)dist\$(Configuration)\**" Exclude="$(InteropWorkingDir)dist\.gitignore" /> diff --git a/src/OpenApi/sample/EndpointRouteBuilderExtensions.cs b/src/OpenApi/sample/EndpointRouteBuilderExtensions.cs index fd196d7fc101..acf7ae1bd41d 100644 --- a/src/OpenApi/sample/EndpointRouteBuilderExtensions.cs +++ b/src/OpenApi/sample/EndpointRouteBuilderExtensions.cs @@ -43,4 +43,92 @@ public static IEndpointConventionBuilder MapSwaggerUi(this IEndpointRouteBuilder """, "text/html")).ExcludeFromDescription(); } + + public static IEndpointRouteBuilder MapTypesWithRef(this IEndpointRouteBuilder endpoints) + { + endpoints.MapPost("/category", (Category category) => + { + return Results.Ok(category); + }); + endpoints.MapPost("/container", (ContainerType container) => + { + return Results.Ok(container); + }); + endpoints.MapPost("/root", (Root root) => + { + return Results.Ok(root); + }); + endpoints.MapPost("/location", (LocationContainer location) => + { + return Results.Ok(location); + }); + endpoints.MapPost("/parent", (ParentObject parent) => + { + return Results.Ok(parent); + }); + endpoints.MapPost("/child", (ChildObject child) => + { + return Results.Ok(child); + }); + return endpoints; + } + + public sealed class Category + { + public required string Name { get; set; } + + public required Category Parent { get; set; } + + public IEnumerable Tags { get; set; } = []; + } + + public sealed class Tag + { + public required string Name { get; set; } + } + + public sealed class ContainerType + { + public List> Seq1 { get; set; } = []; + public List> Seq2 { get; set; } = []; + } + + public sealed class Root + { + public Item Item1 { get; set; } = null!; + public Item Item2 { get; set; } = null!; + } + + public sealed class Item + { + public string[] Name { get; set; } = null!; + public int value { get; set; } + } + + public sealed class LocationContainer + { + public required LocationDto Location { get; set; } + } + + public sealed class LocationDto + { + public required AddressDto Address { get; set; } + } + + public sealed class AddressDto + { + public required LocationDto RelatedLocation { get; set; } + } + + public sealed class ParentObject + { + public int Id { get; set; } + public List Children { get; set; } = []; + } + + public sealed class ChildObject + { + public int Id { get; set; } + public required ParentObject Parent { get; set; } + } } diff --git a/src/OpenApi/sample/Program.cs b/src/OpenApi/sample/Program.cs index a622780ff482..e2a1c4c0866f 100644 --- a/src/OpenApi/sample/Program.cs +++ b/src/OpenApi/sample/Program.cs @@ -113,6 +113,7 @@ schemas.MapPost("/shape", (Shape shape) => { }); schemas.MapPost("/weatherforecastbase", (WeatherForecastBase forecast) => { }); schemas.MapPost("/person", (Person person) => { }); +schemas.MapTypesWithRef(); app.MapControllers(); diff --git a/src/OpenApi/src/Comparers/OpenApiSchemaComparer.cs b/src/OpenApi/src/Comparers/OpenApiSchemaComparer.cs index 2e69b10f213f..46f91cd8a494 100644 --- a/src/OpenApi/src/Comparers/OpenApiSchemaComparer.cs +++ b/src/OpenApi/src/Comparers/OpenApiSchemaComparer.cs @@ -24,9 +24,30 @@ public bool Equals(OpenApiSchema? x, OpenApiSchema? y) return true; } - // If a local reference is present, we can't compare the schema directly - // and should instead use the schema ID as a type-check to assert if the schemas are - // equivalent. + // If both have references, compare the final segments to handle + // equivalent types in different contexts, like the same schema + // in a dictionary value or list like "#/components/schemas/#/additionalProperties/properties/location/properties/address" + if (x.Reference != null && y.Reference != null) + { + if (x.Reference.Id.StartsWith("#", StringComparison.OrdinalIgnoreCase) && + y.Reference.Id.StartsWith("#", StringComparison.OrdinalIgnoreCase) && + x.Reference.ReferenceV3 is string xFullReferencePath && + y.Reference.ReferenceV3 is string yFullReferencePath) + { + // Compare the last segments of the reference paths + // to handle equivalent types in different contexts, + // like the same schema in a dictionary value or list + var xLastIndexOf = xFullReferencePath.LastIndexOf('/'); + var yLastIndexOf = yFullReferencePath.LastIndexOf('/'); + + if (xLastIndexOf != -1 && yLastIndexOf != -1) + { + return xFullReferencePath.AsSpan(xLastIndexOf).Equals(yFullReferencePath.AsSpan(yLastIndexOf), StringComparison.OrdinalIgnoreCase); + } + } + } + + // If only one has a reference, compare using schema IDs if ((x.Reference != null && y.Reference == null) || (x.Reference == null && y.Reference != null)) { diff --git a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs index c5bed38669e4..de74fd8d1257 100644 --- a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs @@ -43,7 +43,7 @@ public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder e } else { - var document = await documentService.GetOpenApiDocumentAsync(context.RequestServices, context.RequestAborted); + var document = await documentService.GetOpenApiDocumentAsync(context.RequestServices, context.Request, context.RequestAborted); var documentOptions = options.Get(documentName); using var output = MemoryBufferWriter.Get(); using var writer = Utf8BufferTextWriter.Get(output); diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index 5d678e67c8c7..b907cc6ecb20 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; @@ -55,7 +56,7 @@ internal sealed class OpenApiDocumentService( internal bool TryGetCachedOperationTransformerContext(string descriptionId, [NotNullWhen(true)] out OpenApiOperationTransformerContext? context) => _operationTransformerContextCache.TryGetValue(descriptionId, out context); - public async Task GetOpenApiDocumentAsync(IServiceProvider scopedServiceProvider, CancellationToken cancellationToken = default) + public async Task GetOpenApiDocumentAsync(IServiceProvider scopedServiceProvider, HttpRequest? httpRequest = null, CancellationToken cancellationToken = default) { // For good hygiene, operation-level tags must also appear in the document-level // tags collection. This set captures all tags that have been seen so far. @@ -74,7 +75,7 @@ public async Task GetOpenApiDocumentAsync(IServiceProvider scop { Info = GetOpenApiInfo(), Paths = await GetOpenApiPathsAsync(capturedTags, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken), - Servers = GetOpenApiServers(), + Servers = GetOpenApiServers(httpRequest), Tags = [.. capturedTags] }; try @@ -192,12 +193,26 @@ internal OpenApiInfo GetOpenApiInfo() }; } - internal List GetOpenApiServers() + // Resolve server URL from the request to handle reverse proxies. + // If there is active request object, assume a development environment and use the server addresses. + internal List GetOpenApiServers(HttpRequest? httpRequest = null) + { + if (httpRequest is not null) + { + var serverUrl = UriHelper.BuildAbsolute(httpRequest.Scheme, httpRequest.Host, httpRequest.PathBase); + return [new OpenApiServer { Url = serverUrl }]; + } + else + { + return GetDevelopmentOpenApiServers(); + } + } + private List GetDevelopmentOpenApiServers() { if (hostEnvironment.IsDevelopment() && server?.Features.Get()?.Addresses is { Count: > 0 } addresses) { - return addresses.Select(address => new OpenApiServer { Url = address }).ToList(); + return [.. addresses.Select(address => new OpenApiServer { Url = address })]; } return []; } diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs index 537eb5d5db72..812f896ee25d 100644 --- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs +++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs @@ -102,7 +102,7 @@ internal sealed class OpenApiSchemaService( // "nested": "#/properties/nested" becomes "nested": "#/components/schemas/NestedType" if (jsonPropertyInfo.PropertyType == jsonPropertyInfo.DeclaringType) { - return new JsonObject { [OpenApiSchemaKeywords.RefKeyword] = createSchemaReferenceId(context.TypeInfo) }; + schema[OpenApiSchemaKeywords.RefKeyword] = createSchemaReferenceId(context.TypeInfo); } schema.ApplyNullabilityContextInfo(jsonPropertyInfo); } @@ -213,13 +213,118 @@ private async Task InnerApplySchemaTransformersAsync(OpenApiSchema schema, } } - if (schema is { AdditionalPropertiesAllowed: true, AdditionalProperties: not null } && jsonTypeInfo.ElementType is not null) - { + if (schema is { AdditionalPropertiesAllowed: true, AdditionalProperties: not null } && jsonTypeInfo.ElementType is not null) + { var elementTypeInfo = _jsonSerializerOptions.GetTypeInfo(jsonTypeInfo.ElementType); await InnerApplySchemaTransformersAsync(schema.AdditionalProperties, elementTypeInfo, null, context, transformer, cancellationToken); } - } + } private JsonNode CreateSchema(OpenApiSchemaKey key) - => JsonSchemaExporter.GetJsonSchemaAsNode(_jsonSerializerOptions, key.Type, _configuration); + { + var sourceSchema = JsonSchemaExporter.GetJsonSchemaAsNode(_jsonSerializerOptions, key.Type, _configuration); + + // Resolve any relative references in the schema + ResolveRelativeReferences(sourceSchema, sourceSchema); + + return sourceSchema; + } + + // Helper method to recursively resolve relative references in a schema + private static void ResolveRelativeReferences(JsonNode node, JsonNode rootNode) + { + if (node is JsonObject jsonObj) + { + // Check if this node has a $ref property with a relative reference and no schemaId to + // resolve to + if (jsonObj.TryGetPropertyValue(OpenApiSchemaKeywords.RefKeyword, out var refNode) && + refNode is JsonValue refValue && + refValue.TryGetValue(out var refPath) && + refPath.StartsWith("#/", StringComparison.OrdinalIgnoreCase) && + !jsonObj.TryGetPropertyValue(OpenApiConstants.SchemaId, out var schemaId) && + schemaId is null) + { + // Found a relative reference, resolve it + var resolvedNode = ResolveJsonPointer(rootNode, refPath); + if (resolvedNode != null) + { + // Copy all properties from the resolved node + if (resolvedNode is JsonObject resolvedObj) + { + foreach (var property in resolvedObj) + { + // Clone the property value to avoid modifying the original + var clonedValue = property.Value != null + ? JsonNode.Parse(property.Value.ToJsonString()) + : null; + + jsonObj[property.Key] = clonedValue; + } + } + } + } + else + { + // Recursively process all properties + foreach (var property in jsonObj) + { + if (property.Value is JsonNode propNode) + { + ResolveRelativeReferences(propNode, rootNode); + } + } + } + } + else if (node is JsonArray jsonArray) + { + // Process each item in the array + for (var i = 0; i < jsonArray.Count; i++) + { + if (jsonArray[i] is JsonNode arrayItem) + { + ResolveRelativeReferences(arrayItem, rootNode); + } + } + } + } + + // Helper method to resolve a JSON pointer path and return the referenced node + private static JsonNode? ResolveJsonPointer(JsonNode root, string pointer) + { + if (string.IsNullOrEmpty(pointer) || !pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase)) + { + return null; // Invalid pointer + } + + // Remove the leading "#/" and split the path into segments + var jsonPointer = pointer.AsSpan(2); + var segments = jsonPointer.Split('/'); + var currentNode = root; + + foreach (var segment in segments) + { + if (currentNode is JsonObject jsonObj) + { + if (!jsonObj.TryGetPropertyValue(jsonPointer[segment].ToString(), out var nextNode)) + { + return null; // Path segment not found + } + currentNode = nextNode; + } + else if (currentNode is JsonArray jsonArray && int.TryParse(jsonPointer[segment], out var index)) + { + if (index < 0 || index >= jsonArray.Count) + { + return null; // Index out of range + } + currentNode = jsonArray[index]; + } + else + { + return null; // Cannot navigate further + } + } + + return currentNode; + } } diff --git a/src/OpenApi/src/Transformers/Implementations/OpenApiSchemaReferenceTransformer.cs b/src/OpenApi/src/Transformers/Implementations/OpenApiSchemaReferenceTransformer.cs index ee7e166daab7..aa98a21894ff 100644 --- a/src/OpenApi/src/Transformers/Implementations/OpenApiSchemaReferenceTransformer.cs +++ b/src/OpenApi/src/Transformers/Implementations/OpenApiSchemaReferenceTransformer.cs @@ -112,6 +112,13 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC return new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = schemaId?.ToString() } }; } + // Handle relative schemas that don't point to the parent document but to another property in the same type. + // In this case, remove the reference and rely on the properties that have been resolved and copied by the OpenApiSchemaService. + if (schema.Reference is { Type: ReferenceType.Schema, Id: var id } && id.StartsWith("#/", StringComparison.Ordinal)) + { + schema.Reference = null; + } + if (schema.AllOf is not null) { for (var i = 0; i < schema.AllOf.Count; i++) diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt index cd00d261b632..3e5373a7e36b 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt @@ -375,6 +375,138 @@ } } } + }, + "/schemas-by-ref/category": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Category" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/container": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContainerType" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/root": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Root" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/location": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationContainer" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/parent": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ParentObject" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/child": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChildObject" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } } }, "components": { @@ -391,6 +523,128 @@ } } }, + "AddressDto": { + "required": [ + "relatedLocation" + ], + "type": "object", + "properties": { + "relatedLocation": { + "$ref": "#/components/schemas/LocationDto" + } + } + }, + "Category": { + "required": [ + "name", + "parent" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/Category" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + } + } + }, + "ChildObject": { + "required": [ + "parent" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "parent": { + "$ref": "#/components/schemas/ParentObject" + } + } + }, + "ContainerType": { + "type": "object", + "properties": { + "seq1": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "seq2": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "Item": { + "type": "object", + "properties": { + "name": { + "type": "array", + "items": { + "type": "string" + } + }, + "value": { + "type": "integer", + "format": "int32" + } + } + }, + "LocationContainer": { + "required": [ + "location" + ], + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/LocationDto" + } + } + }, + "LocationDto": { + "required": [ + "address" + ], + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/AddressDto" + } + } + }, + "ParentObject": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChildObject" + } + } + } + }, "Person": { "required": [ "discriminator" @@ -454,6 +708,17 @@ } } }, + "Root": { + "type": "object", + "properties": { + "item1": { + "$ref": "#/components/schemas/Item" + }, + "item2": { + "$ref": "#/components/schemas/Item" + } + } + }, "Shape": { "required": [ "$type" @@ -517,6 +782,17 @@ } } }, + "Tag": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, "Triangle": { "type": "object", "properties": { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Servers.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Servers.cs index c84c7e258510..1bc247c95ad4 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Servers.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Servers.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 Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.OpenApi; using Microsoft.Extensions.DependencyInjection; @@ -10,6 +11,45 @@ public partial class OpenApiDocumentServiceTests { + [Theory] + [InlineData("Development", "localhost:5001", "", "http", "http://localhost:5001/")] + [InlineData("Development", "example.com", "/api", "https", "https://example.com/api")] + [InlineData("Staging", "localhost:5002", "/v1", "http", "http://localhost:5002/v1")] + [InlineData("Staging", "api.example.com", "/base/path", "https", "https://api.example.com/base/path")] + [InlineData("Development", "localhost", "/", "http", "http://localhost/")] + public void GetOpenApiServers_FavorsHttpContextRequestOverServerAddress(string environment, string host, string pathBase, string scheme, string expectedUri) + { + // Arrange + var hostEnvironment = new HostingEnvironment + { + ApplicationName = "TestApplication", + EnvironmentName = environment + }; + var docService = new OpenApiDocumentService( + "v1", + new Mock().Object, + hostEnvironment, + GetMockOptionsMonitor(), + new Mock().Object, + new OpenApiTestServer(["http://localhost:5000"])); + var httpContext = new DefaultHttpContext() + { + Request = + { + Host = new HostString(host), + PathBase = pathBase, + Scheme = scheme + + } + }; + + // Act + var servers = docService.GetOpenApiServers(httpContext.Request); + + // Assert + Assert.Contains(expectedUri, servers.Select(s => s.Url)); + } + [Fact] public void GetOpenApiServers_HandlesServerAddressFeatureWithValues() { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs index e773ebf5ff89..b33eb153de4c 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs @@ -35,16 +35,16 @@ public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, Op { var documentService = CreateDocumentService(builder, openApiOptions); var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope(); - var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, cancellationToken); + var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, null, cancellationToken); verifyOpenApiDocument(document); } - public static async Task VerifyOpenApiDocument(ActionDescriptor action, Action verifyOpenApiDocument) + public static async Task VerifyOpenApiDocument(ActionDescriptor action, Action verifyOpenApiDocument, CancellationToken cancellationToken = default) { var builder = CreateBuilder(); var documentService = CreateDocumentService(builder, action); var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope(); - var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider); + var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, null); verifyOpenApiDocument(document); } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs index 7209715e3516..f8d46f771ca1 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs @@ -505,6 +505,58 @@ await VerifyOpenApiDocument(builder, document => // Assert that $ref is used for related LocationDto var addressSchema = locationSchema.Properties["address"].GetEffective(document); Assert.Equal("LocationDto", addressSchema.Properties["relatedLocation"].Reference.Id); + + // Assert that only expected schemas are generated at the top-level + Assert.Equal(["AddressDto", "LocationContainer", "LocationDto"], document.Components.Schemas.Keys); + }); + } + + [Fact] + public async Task SupportsListOfNestedSchemasWithSelfReference() + { + // Arrange + var builder = CreateBuilder(); + + builder.MapPost("/list", (List items) => { }); + builder.MapPost("/array", (LocationContainer[] items) => { }); + builder.MapPost("/dictionary", (Dictionary items) => { }); + builder.MapPost("/", (LocationContainer item) => { }); + + await VerifyOpenApiDocument(builder, document => + { + var listOperation = document.Paths["/list"].Operations[OperationType.Post]; + var listRequestSchema = listOperation.RequestBody.Content["application/json"].Schema; + + var arrayOperation = document.Paths["/array"].Operations[OperationType.Post]; + var arrayRequestSchema = arrayOperation.RequestBody.Content["application/json"].Schema; + + var dictionaryOperation = document.Paths["/dictionary"].Operations[OperationType.Post]; + var dictionaryRequestSchema = dictionaryOperation.RequestBody.Content["application/json"].Schema; + + var operation = document.Paths["/"].Operations[OperationType.Post]; + var requestSchema = operation.RequestBody.Content["application/json"].Schema; + + // Assert $ref used for top-level + Assert.Equal("LocationContainer", listRequestSchema.Items.Reference.Id); + Assert.Equal("LocationContainer", arrayRequestSchema.Items.Reference.Id); + Assert.Equal("LocationContainer", dictionaryRequestSchema.AdditionalProperties.Reference.Id); + Assert.Equal("LocationContainer", requestSchema.Reference.Id); + + // Assert that $ref is used for nested LocationDto + var locationContainerSchema = requestSchema.GetEffective(document); + Assert.Equal("LocationDto", locationContainerSchema.Properties["location"].Reference.Id); + + // Assert that $ref is used for nested AddressDto + var locationSchema = locationContainerSchema.Properties["location"].GetEffective(document); + Assert.Equal("AddressDto", locationSchema.Properties["address"].Reference.Id); + + // Assert that $ref is used for related LocationDto + var addressSchema = locationSchema.Properties["address"].GetEffective(document); + Assert.Equal("LocationDto", addressSchema.Properties["relatedLocation"].Reference.Id); + + // Assert that only expected schemas are generated at the top-level + Assert.Equal(3, document.Components.Schemas.Count); + Assert.Equal(["AddressDto", "LocationContainer", "LocationDto"], document.Components.Schemas.Keys); }); } @@ -531,6 +583,9 @@ await VerifyOpenApiDocument(builder, document => // Assert that $ref is used for nested Parent var childSchema = parentSchema.Properties["children"].Items.GetEffective(document); Assert.Equal("ParentObject", childSchema.Properties["parent"].Reference.Id); + + // Assert that only the expected schemas are registered + Assert.Equal(["ChildObject", "ParentObject"], document.Components.Schemas.Keys); }); } @@ -559,6 +614,161 @@ await VerifyOpenApiDocument(builder, document => }); } + // Test for: https://github.com/dotnet/aspnetcore/issues/60381 + [Fact] + public async Task ResolvesListBasedReferencesCorrectly() + { + // Arrange + var builder = CreateBuilder(); + + builder.MapPost("/", (ContainerType item) => { }); + + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/"].Operations[OperationType.Post]; + var requestSchema = operation.RequestBody.Content["application/json"].Schema; + + // Assert $ref used for top-level + Assert.Equal("ContainerType", requestSchema.Reference.Id); + + // Get effective schema for ContainerType + var containerSchema = requestSchema.GetEffective(document); + Assert.Equal(2, containerSchema.Properties.Count); + + // Check Seq1 and Seq2 properties + var seq1Schema = containerSchema.Properties["seq1"]; + var seq2Schema = containerSchema.Properties["seq2"]; + + // Assert both are array types + Assert.Equal("array", seq1Schema.Type); + Assert.Equal("array", seq2Schema.Type); + + // Assert items are arrays of strings + Assert.Equal("array", seq1Schema.Items.Type); + Assert.Equal("array", seq2Schema.Items.Type); + + // Since both Seq1 and Seq2 are the same type (List>), + // they should reference the same schema structure + Assert.Equal(seq1Schema.Items.Type, seq2Schema.Items.Type); + + // Verify the inner arrays contain strings + Assert.Equal("string", seq1Schema.Items.Items.Type); + Assert.Equal("string", seq2Schema.Items.Items.Type); + + Assert.Equal(["ContainerType"], document.Components.Schemas.Keys); + }); + } + + // Tests for: https://github.com/dotnet/aspnetcore/issues/60012 + [Fact] + public async Task SupportsListOfClassInSelfReferentialSchema() + { + // Arrange + var builder = CreateBuilder(); + + builder.MapPost("/", (Category item) => { }); + + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/"].Operations[OperationType.Post]; + var requestSchema = operation.RequestBody.Content["application/json"].Schema; + + // Assert $ref used for top-level + Assert.Equal("Category", requestSchema.Reference.Id); + + // Assert that $ref is used for nested Tags + var categorySchema = requestSchema.GetEffective(document); + Assert.Equal("Tag", categorySchema.Properties["tags"].Items.Reference.Id); + + // Assert that $ref is used for nested Parent + Assert.Equal("Category", categorySchema.Properties["parent"].Reference.Id); + + // Assert that no duplicate schemas are emitted + Assert.Collection(document.Components.Schemas, + schema => + { + Assert.Equal("Category", schema.Key); + }, + schema => + { + Assert.Equal("Tag", schema.Key); + }); + }); + } + + [Fact] + public async Task UsesSameReferenceForSameTypeInDifferentLocations() + { + // Arrange + var builder = CreateBuilder(); + + builder.MapPost("/parent-object", (ParentObject item) => { }); + builder.MapPost("/list", (List item) => { }); + builder.MapPost("/dictionary", (Dictionary item) => { }); + + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/parent-object"].Operations[OperationType.Post]; + var requestSchema = operation.RequestBody.Content["application/json"].Schema; + + // Assert $ref used for top-level + Assert.Equal("ParentObject", requestSchema.Reference.Id); + + // Assert that $ref is used for nested Children + var parentSchema = requestSchema.GetEffective(document); + Assert.Equal("ChildObject", parentSchema.Properties["children"].Items.Reference.Id); + + // Assert that $ref is used for nested Parent + var childSchema = parentSchema.Properties["children"].Items.GetEffective(document); + Assert.Equal("ParentObject", childSchema.Properties["parent"].Reference.Id); + + operation = document.Paths["/list"].Operations[OperationType.Post]; + requestSchema = operation.RequestBody.Content["application/json"].Schema; + + // Assert $ref used for items in the list definition + Assert.Equal("ParentObject", requestSchema.Items.Reference.Id); + parentSchema = requestSchema.Items.GetEffective(document); + Assert.Equal("ChildObject", parentSchema.Properties["children"].Items.Reference.Id); + + childSchema = parentSchema.Properties["children"].Items.GetEffective(document); + Assert.Equal("ParentObject", childSchema.Properties["parent"].Reference.Id); + + operation = document.Paths["/dictionary"].Operations[OperationType.Post]; + requestSchema = operation.RequestBody.Content["application/json"].Schema; + + // Assert $ref used for items in the dictionary definition + Assert.Equal("ParentObject", requestSchema.AdditionalProperties.Reference.Id); + parentSchema = requestSchema.AdditionalProperties.GetEffective(document); + Assert.Equal("ChildObject", parentSchema.Properties["children"].Items.Reference.Id); + + childSchema = parentSchema.Properties["children"].Items.GetEffective(document); + Assert.Equal("ParentObject", childSchema.Properties["parent"].Reference.Id); + + // Assert that only the expected schemas are registered + Assert.Equal(["ChildObject", "ParentObject"], document.Components.Schemas.Keys); + }); + } + + private class Category + { + public required string Name { get; set; } + + public Category Parent { get; set; } + + public IEnumerable Tags { get; set; } = []; + } + + public class Tag + { + public required string Name { get; set; } + } + + private class ContainerType + { + public List> Seq1 { get; set; } = []; + public List> Seq2 { get; set; } = []; + } + private class Root { public Item Item1 { get; set; } = null!; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLibTests/main.cpp b/src/Servers/IIS/AspNetCoreModuleV2/CommonLibTests/main.cpp index 86c764df8533..3e1bbc1add95 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLibTests/main.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLibTests/main.cpp @@ -8,5 +8,5 @@ DECLARE_DEBUG_PRINT_OBJECT2("tests", ASPNETCORE_DEBUG_FLAG_INFO | ASPNETCORE_DEB int wmain(int argc, wchar_t* argv[]) { ::testing::InitGoogleTest(&argc, argv); - RUN_ALL_TESTS(); + return RUN_ALL_TESTS(); } diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index 7f6c785963f6..55f5bde688f0 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -737,4 +737,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l Call UseKestrelHttpsConfiguration() on IWebHostBuilder to automatically enable HTTPS when an https:// address is used. - + + The client sent a {frameType} frame to a control stream that was too large. + + \ No newline at end of file diff --git a/src/Servers/Kestrel/Core/src/Http3Limits.cs b/src/Servers/Kestrel/Core/src/Http3Limits.cs index 0d7801e48bf8..b6556557a340 100644 --- a/src/Servers/Kestrel/Core/src/Http3Limits.cs +++ b/src/Servers/Kestrel/Core/src/Http3Limits.cs @@ -37,7 +37,7 @@ internal int HeaderTableSize /// /// Indicates the size of the maximum allowed size of a request header field sequence. This limit applies to both name and value sequences in their compressed and uncompressed representations. /// - /// Value must be greater than 0, defaults to 2^14 (16,384). + /// Value must be greater than 0, defaults to 2^15 (32,768). /// /// public int MaxRequestHeaderFieldSize diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Data.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Data.cs index 95dbbcb8e4d5..ce1e9b0db815 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Data.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Data.cs @@ -7,7 +7,7 @@ internal partial class Http3RawFrame { public void PrepareData() { - Length = 0; + RemainingLength = 0; Type = Http3FrameType.Data; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.GoAway.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.GoAway.cs index fe2eb3a6e42e..de1a73cb830e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.GoAway.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.GoAway.cs @@ -7,7 +7,7 @@ internal partial class Http3RawFrame { public void PrepareGoAway() { - Length = 0; + RemainingLength = 0; Type = Http3FrameType.GoAway; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Headers.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Headers.cs index bcf65929694d..11e8c971ff21 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Headers.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Headers.cs @@ -7,7 +7,7 @@ internal partial class Http3RawFrame { public void PrepareHeaders() { - Length = 0; + RemainingLength = 0; Type = Http3FrameType.Headers; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Settings.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Settings.cs index 9e74e07db5b8..03ed2a670250 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Settings.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Settings.cs @@ -7,7 +7,7 @@ internal partial class Http3RawFrame { public void PrepareSettings() { - Length = 0; + RemainingLength = 0; Type = Http3FrameType.Settings; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs index 076b9640d0bb..5839d515524c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs @@ -9,7 +9,7 @@ namespace System.Net.Http; internal partial class Http3RawFrame #pragma warning restore CA1852 // Seal internal types { - public long Length { get; set; } + public long RemainingLength { get; set; } public Http3FrameType Type { get; internal set; } @@ -17,6 +17,6 @@ internal partial class Http3RawFrame public override string ToString() { - return $"{FormattedType} Length: {Length}"; + return $"{FormattedType} Length: {RemainingLength}"; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs index dbd99d838a0e..c179676663ff 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Diagnostics; using System.Globalization; using System.IO.Pipelines; using System.Net.Http; @@ -19,13 +20,18 @@ internal abstract class Http3ControlStream : IHttp3Stream, IThreadPoolWorkItem private const int EncoderStreamTypeId = 2; private const int DecoderStreamTypeId = 3; + // Arbitrarily chosen max frame length + // ControlStream frames currently are very small, either a single variable length integer (max 8 bytes), two variable length integers, + // or in the case of SETTINGS a small collection of two variable length integers + // We'll use a generous value of 10k in case new optional frame(s) are added that might be a little larger than the current frames. + private const int MaxFrameSize = 10_000; + private readonly Http3FrameWriter _frameWriter; private readonly Http3StreamContext _context; private readonly Http3PeerSettings _serverPeerSettings; private readonly IStreamIdFeature _streamIdFeature; private readonly IStreamClosedFeature _streamClosedFeature; private readonly IProtocolErrorCodeFeature _errorCodeFeature; - private readonly Http3RawFrame _incomingFrame = new Http3RawFrame(); private volatile int _isClosed; private long _headerType; private readonly object _completionLock = new(); @@ -159,9 +165,9 @@ private async ValueTask TryReadStreamHeaderAsync() { if (!readableBuffer.IsEmpty) { - var id = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out examined); - if (id != -1) + if (VariableLengthIntegerHelper.TryGetInteger(readableBuffer, out consumed, out var id)) { + examined = consumed; return id; } } @@ -240,6 +246,8 @@ public async Task ProcessRequestAsync(IHttpApplication appli } finally { + await _context.StreamContext.DisposeAsync(); + ApplyCompletionFlag(StreamCompletionFlags.Completed); _context.StreamLifetimeHandler.OnStreamCompleted(this); } @@ -247,6 +255,8 @@ public async Task ProcessRequestAsync(IHttpApplication appli private async Task HandleControlStream() { + var incomingFrame = new Http3RawFrame(); + var isContinuedFrame = false; while (_isClosed == 0) { var result = await Input.ReadAsync(); @@ -259,12 +269,33 @@ private async Task HandleControlStream() if (!readableBuffer.IsEmpty) { // need to kick off httpprotocol process request async here. - while (Http3FrameReader.TryReadFrame(ref readableBuffer, _incomingFrame, out var framePayload)) + while (Http3FrameReader.TryReadFrame(ref readableBuffer, incomingFrame, isContinuedFrame, out var framePayload)) { - Log.Http3FrameReceived(_context.ConnectionId, _streamIdFeature.StreamId, _incomingFrame); - - consumed = examined = framePayload.End; - await ProcessHttp3ControlStream(framePayload); + Debug.Assert(incomingFrame.RemainingLength >= framePayload.Length); + + // Only log when parsing the beginning of the frame + if (!isContinuedFrame) + { + Log.Http3FrameReceived(_context.ConnectionId, _streamIdFeature.StreamId, incomingFrame); + } + + examined = framePayload.End; + await ProcessHttp3ControlStream(incomingFrame, isContinuedFrame, framePayload, out consumed); + + if (incomingFrame.RemainingLength == framePayload.Length) + { + Debug.Assert(framePayload.Slice(0, consumed).Length == framePayload.Length); + + incomingFrame.RemainingLength = 0; + isContinuedFrame = false; + } + else + { + incomingFrame.RemainingLength -= framePayload.Slice(0, consumed).Length; + isContinuedFrame = true; + + Debug.Assert(incomingFrame.RemainingLength > 0); + } } } @@ -294,56 +325,71 @@ private async ValueTask HandleEncodingDecodingTask() } } - private ValueTask ProcessHttp3ControlStream(in ReadOnlySequence payload) + private ValueTask ProcessHttp3ControlStream(Http3RawFrame incomingFrame, bool isContinuedFrame, in ReadOnlySequence payload, out SequencePosition consumed) { - switch (_incomingFrame.Type) + // default to consuming the entire payload, this is so that we don't need to set consumed from all the frame types that aren't implemented yet. + // individual frame types can set consumed if they're implemented and want to be able to partially consume the payload. + consumed = payload.End; + switch (incomingFrame.Type) { case Http3FrameType.Data: case Http3FrameType.Headers: case Http3FrameType.PushPromise: - // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2 - throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame); + // https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1-2.12.1 + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame); case Http3FrameType.Settings: - return ProcessSettingsFrameAsync(payload); + CheckMaxFrameSize(incomingFrame); + return ProcessSettingsFrameAsync(isContinuedFrame, payload, out consumed); case Http3FrameType.GoAway: - return ProcessGoAwayFrameAsync(); + return ProcessGoAwayFrameAsync(isContinuedFrame, incomingFrame, payload, out consumed); case Http3FrameType.CancelPush: - return ProcessCancelPushFrameAsync(); + return ProcessCancelPushFrameAsync(incomingFrame, payload, out consumed); case Http3FrameType.MaxPushId: - return ProcessMaxPushIdFrameAsync(); + return ProcessMaxPushIdFrameAsync(incomingFrame, payload, out consumed); default: - return ProcessUnknownFrameAsync(_incomingFrame.Type); + CheckMaxFrameSize(incomingFrame); + return ProcessUnknownFrameAsync(incomingFrame.Type); } - } - private ValueTask ProcessSettingsFrameAsync(ReadOnlySequence payload) - { - if (_haveReceivedSettingsFrame) + static void CheckMaxFrameSize(Http3RawFrame http3RawFrame) { - // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-settings - throw new Http3ConnectionErrorException(CoreStrings.Http3ErrorControlStreamMultipleSettingsFrames, Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame); + // Not part of the RFC, but it's a good idea to limit the size of frames when we know they're supposed to be small. + if (http3RawFrame.RemainingLength >= MaxFrameSize) + { + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamFrameTooLarge(http3RawFrame.FormattedType), Http3ErrorCode.FrameError, ConnectionEndReason.InvalidFrameLength); + } } + } - _haveReceivedSettingsFrame = true; - _streamClosedFeature.OnClosed(static state => + private ValueTask ProcessSettingsFrameAsync(bool isContinuedFrame, ReadOnlySequence payload, out SequencePosition consumed) + { + if (!isContinuedFrame) { - var stream = (Http3ControlStream)state!; - stream.OnStreamClosed(); - }, this); + if (_haveReceivedSettingsFrame) + { + // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4 + throw new Http3ConnectionErrorException(CoreStrings.Http3ErrorControlStreamMultipleSettingsFrames, Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame); + } + + _haveReceivedSettingsFrame = true; + _streamClosedFeature.OnClosed(static state => + { + var stream = (Http3ControlStream)state!; + stream.OnStreamClosed(); + }, this); + } while (true) { - var id = VariableLengthIntegerHelper.GetInteger(payload, out var consumed, out _); - if (id == -1) + if (!VariableLengthIntegerHelper.TryGetInteger(payload, out consumed, out var id)) { break; } - payload = payload.Slice(consumed); - - var value = VariableLengthIntegerHelper.GetInteger(payload, out consumed, out _); - if (value == -1) + if (!VariableLengthIntegerHelper.TryGetInteger(payload.Slice(consumed), out consumed, out var value)) { + // Reset consumed to very start even though we successfully read 1 varint. It's because we want to keep the id for when we have the value as well. + consumed = payload.Start; break; } @@ -382,37 +428,48 @@ private void ProcessSetting(long id, long value) } } - private ValueTask ProcessGoAwayFrameAsync() + private ValueTask ProcessGoAwayFrameAsync(bool isContinuedFrame, Http3RawFrame incomingFrame, ReadOnlySequence payload, out SequencePosition consumed) { - EnsureSettingsFrame(Http3FrameType.GoAway); + // https://www.rfc-editor.org/rfc/rfc9114.html#name-goaway + + // We've already triggered RequestClose since isContinuedFrame is only true + // after we've already parsed the frame type and called the processing function at least once. + if (!isContinuedFrame) + { + EnsureSettingsFrame(Http3FrameType.GoAway); - // StopProcessingNextRequest must be called before RequestClose to ensure it's considered client initiated. - _context.Connection.StopProcessingNextRequest(serverInitiated: false, ConnectionEndReason.ClientGoAway); - _context.ConnectionContext.Features.Get()?.RequestClose(); + // StopProcessingNextRequest must be called before RequestClose to ensure it's considered client initiated. + _context.Connection.StopProcessingNextRequest(serverInitiated: false, ConnectionEndReason.ClientGoAway); + _context.ConnectionContext.Features.Get()?.RequestClose(); + } - // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-goaway - // PUSH is not implemented so nothing to do. + // PUSH is not implemented but we still want to parse the frame to do error checking + ParseVarIntWithFrameLengthValidation(incomingFrame, payload, out consumed); // TODO: Double check the connection remains open. return default; } - private ValueTask ProcessCancelPushFrameAsync() + private ValueTask ProcessCancelPushFrameAsync(Http3RawFrame incomingFrame, ReadOnlySequence payload, out SequencePosition consumed) { + // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.3 + EnsureSettingsFrame(Http3FrameType.CancelPush); - // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-cancel_push - // PUSH is not implemented so nothing to do. + // PUSH is not implemented but we still want to parse the frame to do error checking + ParseVarIntWithFrameLengthValidation(incomingFrame, payload, out consumed); return default; } - private ValueTask ProcessMaxPushIdFrameAsync() + private ValueTask ProcessMaxPushIdFrameAsync(Http3RawFrame incomingFrame, ReadOnlySequence payload, out SequencePosition consumed) { + // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.7 + EnsureSettingsFrame(Http3FrameType.MaxPushId); - // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-cancel_push - // PUSH is not implemented so nothing to do. + // PUSH is not implemented but we still want to parse the frame to do error checking + ParseVarIntWithFrameLengthValidation(incomingFrame, payload, out consumed); return default; } @@ -426,6 +483,23 @@ private ValueTask ProcessUnknownFrameAsync(Http3FrameType frameType) return default; } + // Used for frame types that aren't (fully) implemented yet and contain a single var int as part of their framing. (CancelPush, MaxPushId, GoAway) + // We want to throw an error if the length field of the frame is larger than the spec defined format of the frame. + private static void ParseVarIntWithFrameLengthValidation(Http3RawFrame incomingFrame, ReadOnlySequence payload, out SequencePosition consumed) + { + if (!VariableLengthIntegerHelper.TryGetInteger(payload, out consumed, out _)) + { + return; + } + + if (incomingFrame.RemainingLength > payload.Slice(0, consumed).Length) + { + // https://www.rfc-editor.org/rfc/rfc9114.html#section-10.8 + // An implementation MUST ensure that the length of a frame exactly matches the length of the fields it contains. + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamFrameTooLarge(Http3Formatting.ToFormattedType(incomingFrame.Type)), Http3ErrorCode.FrameError, ConnectionEndReason.InvalidFrameLength); + } + } + private void EnsureSettingsFrame(Http3FrameType frameType) { if (!_haveReceivedSettingsFrame) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs index 66740c710f10..2de0472483a1 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs @@ -19,36 +19,44 @@ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 | Frame Payload (*) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ - internal static bool TryReadFrame(ref ReadOnlySequence readableBuffer, Http3RawFrame frame, out ReadOnlySequence framePayload) + // Reads and returns partial frames, don't rely on the frame being complete when using this method + // Set isContinuedFrame to true when expecting to read more of the previous frame + internal static bool TryReadFrame(ref ReadOnlySequence readableBuffer, Http3RawFrame frame, bool isContinuedFrame, out ReadOnlySequence framePayload) { framePayload = ReadOnlySequence.Empty; - SequencePosition consumed; + SequencePosition consumed = readableBuffer.Start; + var length = frame.RemainingLength; - var type = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out _); - if (type == -1) + if (!isContinuedFrame) { - return false; - } + if (!VariableLengthIntegerHelper.TryGetInteger(readableBuffer, out consumed, out var type)) + { + return false; + } - var firstLengthBuffer = readableBuffer.Slice(consumed); + var firstLengthBuffer = readableBuffer.Slice(consumed); - var length = VariableLengthIntegerHelper.GetInteger(firstLengthBuffer, out consumed, out _); + if (!VariableLengthIntegerHelper.TryGetInteger(firstLengthBuffer, out consumed, out length)) + { + return false; + } - // Make sure the whole frame is buffered - if (length == -1) - { - return false; + frame.RemainingLength = length; + frame.Type = (Http3FrameType)type; } var startOfFramePayload = readableBuffer.Slice(consumed); - if (startOfFramePayload.Length < length) + + // Get all the available bytes or the rest of the frame whichever is less + length = Math.Min(startOfFramePayload.Length, length); + + // If we were expecting a non-empty payload, but haven't received any of it yet, + // there is nothing to process until we wait for more data. + if (length == 0 && frame.RemainingLength != 0) { return false; } - frame.Length = length; - frame.Type = (Http3FrameType)type; - // The remaining payload minus the extra fields framePayload = startOfFramePayload.Slice(0, length); readableBuffer = readableBuffer.Slice(framePayload.End); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs index 44ade9362ea1..6b94153f80d1 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs @@ -121,7 +121,7 @@ internal Task WriteSettingsAsync(List settings) WriteSettings(settings, buffer); // Advance pipe writer and flush - _outgoingFrame.Length = totalLength; + _outgoingFrame.RemainingLength = totalLength; _outputWriter.Advance(totalLength); return _outputWriter.FlushAsync().GetAsTask(); @@ -186,7 +186,7 @@ private void WriteDataUnsynchronized(in ReadOnlySequence data, long dataLe return; } - _outgoingFrame.Length = (int)dataLength; + _outgoingFrame.RemainingLength = (int)dataLength; WriteHeaderUnsynchronized(); @@ -209,7 +209,7 @@ void SplitAndWriteDataUnsynchronized(in ReadOnlySequence data, long dataLe do { var currentData = remainingData.Slice(0, dataPayloadLength); - _outgoingFrame.Length = dataPayloadLength; + _outgoingFrame.RemainingLength = dataPayloadLength; WriteHeaderUnsynchronized(); @@ -223,7 +223,7 @@ void SplitAndWriteDataUnsynchronized(in ReadOnlySequence data, long dataLe } while (dataLength > dataPayloadLength); - _outgoingFrame.Length = (int)dataLength; + _outgoingFrame.RemainingLength = (int)dataLength; WriteHeaderUnsynchronized(); @@ -240,7 +240,7 @@ internal ValueTask WriteGoAway(long id) var length = VariableLengthIntegerHelper.GetByteCount(id); - _outgoingFrame.Length = length; + _outgoingFrame.RemainingLength = length; WriteHeaderUnsynchronized(); @@ -253,10 +253,10 @@ internal ValueTask WriteGoAway(long id) private void WriteHeaderUnsynchronized() { _log.Http3FrameSending(_connectionId, _streamIdFeature.StreamId, _outgoingFrame); - var headerLength = WriteHeader(_outgoingFrame.Type, _outgoingFrame.Length, _outputWriter); + var headerLength = WriteHeader(_outgoingFrame.Type, _outgoingFrame.RemainingLength, _outputWriter); // We assume the payload will be written prior to the next flush. - _unflushedBytes += headerLength + _outgoingFrame.Length; + _unflushedBytes += headerLength + _outgoingFrame.RemainingLength; } public ValueTask Write100ContinueAsync() @@ -269,7 +269,7 @@ public ValueTask Write100ContinueAsync() } _outgoingFrame.PrepareHeaders(); - _outgoingFrame.Length = ContinueBytes.Length; + _outgoingFrame.RemainingLength = ContinueBytes.Length; WriteHeaderUnsynchronized(); _outputWriter.Write(ContinueBytes); return TimeFlushUnsynchronizedAsync(); @@ -394,7 +394,7 @@ private void FinishWritingHeaders(int payloadLength, bool done) ValidateHeadersTotalSize(); - _outgoingFrame.Length = _headerEncodingBuffer.WrittenCount; + _outgoingFrame.RemainingLength = _headerEncodingBuffer.WrittenCount; WriteHeaderUnsynchronized(); _outputWriter.Write(_headerEncodingBuffer.WrittenSpan); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PendingStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PendingStream.cs index b6db0bb810db..7dabb9654c56 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PendingStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PendingStream.cs @@ -60,8 +60,7 @@ public async ValueTask ReadNextStreamHeaderAsync(Http3StreamContext contex if (!readableBuffer.IsEmpty) { - var value = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out _); - if (value != -1) + if (VariableLengthIntegerHelper.TryGetInteger(readableBuffer, out consumed, out var value)) { if (!advanceOn.HasValue || value == (long)advanceOn) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index 795fdc42b521..832ba5b7540a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -59,7 +59,6 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS private readonly object _completionLock = new(); protected RequestHeaderParsingState _requestHeaderParsingState; - protected readonly Http3RawFrame _incomingFrame = new(); public bool EndStreamReceived => (_completionState & StreamCompletionFlags.EndStreamReceived) == StreamCompletionFlags.EndStreamReceived; public bool IsAborted => (_completionState & StreamCompletionFlags.Aborted) == StreamCompletionFlags.Aborted; @@ -609,6 +608,8 @@ public async Task ProcessRequestAsync(IHttpApplication appli try { + var incomingFrame = new Http3RawFrame(); + var isContinuedFrame = false; while (_isClosed == 0) { var result = await Input.ReadAsync(); @@ -620,12 +621,19 @@ public async Task ProcessRequestAsync(IHttpApplication appli { if (!readableBuffer.IsEmpty) { - while (Http3FrameReader.TryReadFrame(ref readableBuffer, _incomingFrame, out var framePayload)) + while (Http3FrameReader.TryReadFrame(ref readableBuffer, incomingFrame, isContinuedFrame, out var framePayload)) { - Log.Http3FrameReceived(ConnectionId, _streamIdFeature.StreamId, _incomingFrame); + // Only log when parsing the beginning of the frame + if (!isContinuedFrame) + { + Log.Http3FrameReceived(ConnectionId, _streamIdFeature.StreamId, incomingFrame); + } consumed = examined = framePayload.End; - await ProcessHttp3Stream(application, framePayload, result.IsCompleted && readableBuffer.IsEmpty); + await ProcessHttp3Stream(application, incomingFrame, isContinuedFrame, framePayload, result.IsCompleted && readableBuffer.IsEmpty); + + incomingFrame.RemainingLength -= framePayload.Length; + isContinuedFrame = incomingFrame.RemainingLength > 0 ? true : false; } } @@ -748,22 +756,23 @@ private ValueTask OnEndStreamReceived() return RequestBodyPipe.Writer.CompleteAsync(); } - private Task ProcessHttp3Stream(IHttpApplication application, in ReadOnlySequence payload, bool isCompleted) where TContext : notnull + private Task ProcessHttp3Stream(IHttpApplication application, Http3RawFrame incomingFrame, bool isContinuedFrame, + in ReadOnlySequence payload, bool isCompleted) where TContext : notnull { - return _incomingFrame.Type switch + return incomingFrame.Type switch { Http3FrameType.Data => ProcessDataFrameAsync(payload), - Http3FrameType.Headers => ProcessHeadersFrameAsync(application, payload, isCompleted), + Http3FrameType.Headers => ProcessHeadersFrameAsync(application, incomingFrame, isContinuedFrame, payload, isCompleted), // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2.4 // These frames need to be on a control stream Http3FrameType.Settings or Http3FrameType.CancelPush or Http3FrameType.GoAway or Http3FrameType.MaxPushId => throw new Http3ConnectionErrorException( - CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame), + CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame), // The server should never receive push promise Http3FrameType.PushPromise => throw new Http3ConnectionErrorException( - CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame), + CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame), _ => ProcessUnknownFrameAsync(), }; } @@ -775,11 +784,13 @@ private static Task ProcessUnknownFrameAsync() return Task.CompletedTask; } - private async Task ProcessHeadersFrameAsync(IHttpApplication application, ReadOnlySequence payload, bool isCompleted) where TContext : notnull + private async Task ProcessHeadersFrameAsync(IHttpApplication application, Http3RawFrame incomingFrame, bool isContinuedFrame, + ReadOnlySequence payload, bool isCompleted) where TContext : notnull { // HEADERS frame after trailing headers is invalid. // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1 - if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) + // Since we parse data as we get it, we can receive partial frames which means we need to check that we're in the middle of handling the trailers header frame + if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers && !isContinuedFrame) { throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3Formatting.ToFormattedType(Http3FrameType.Headers)), Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame); } @@ -791,8 +802,17 @@ private async Task ProcessHeadersFrameAsync(IHttpApplication try { - QPackDecoder.Decode(payload, endHeaders: true, handler: this); - QPackDecoder.Reset(); + var endHeaders = payload.Length == incomingFrame.RemainingLength; + QPackDecoder.Decode(payload, endHeaders, handler: this); + if (endHeaders) + { + QPackDecoder.Reset(); + } + else + { + // Headers frame isn't complete, return to read more of the frame + return; + } } catch (QPackDecodingException ex) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http3.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http3.cs index 4159c927e531..54e32f258f00 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http3.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http3.cs @@ -37,7 +37,7 @@ public void Http3FrameReceived(string connectionId, long streamId, Http3RawFrame { if (_http3Logger.IsEnabled(LogLevel.Trace)) { - Http3Log.Http3FrameReceived(_http3Logger, connectionId, Http3Formatting.ToFormattedType(frame.Type), streamId, frame.Length); + Http3Log.Http3FrameReceived(_http3Logger, connectionId, Http3Formatting.ToFormattedType(frame.Type), streamId, frame.RemainingLength); } } @@ -45,7 +45,7 @@ public void Http3FrameSending(string connectionId, long streamId, Http3RawFrame { if (_http3Logger.IsEnabled(LogLevel.Trace)) { - Http3Log.Http3FrameSending(_http3Logger, connectionId, Http3Formatting.ToFormattedType(frame.Type), streamId, frame.Length); + Http3Log.Http3FrameSending(_http3Logger, connectionId, Http3Formatting.ToFormattedType(frame.Type), streamId, frame.RemainingLength); } } diff --git a/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs b/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs index 8b73bd0e2c48..f8fa53170829 100644 --- a/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs +++ b/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs @@ -14,7 +14,8 @@ public class VariableIntHelperTests [MemberData(nameof(IntegerData))] public void CheckDecoding(long expected, byte[] input) { - var decoded = VariableLengthIntegerHelper.GetInteger(new ReadOnlySequence(input), out _, out _); + var result = VariableLengthIntegerHelper.TryGetInteger(new ReadOnlySequence(input), out _, out var decoded); + Assert.True(result); Assert.Equal(expected, decoded); } diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs index 64ebcdb07b41..9cae05272fab 100644 --- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs @@ -395,15 +395,15 @@ private static long GetOutputResponseBufferSize(ServiceContext serviceContext) return bufferSize ?? 0; } - internal ValueTask CreateControlStream() + internal ValueTask CreateControlStream(PipeScheduler clientWriterScheduler = null) { - return CreateControlStream(id: 0); + return CreateControlStream(id: 0, clientWriterScheduler); } - internal async ValueTask CreateControlStream(int? id) + internal async ValueTask CreateControlStream(int? id, PipeScheduler clientWriterScheduler = null) { var testStreamContext = new TestStreamContext(canRead: true, canWrite: false, this); - testStreamContext.Initialize(streamId: 2); + testStreamContext.Initialize(streamId: 2, clientWriterScheduler); var stream = new Http3ControlStream(this, testStreamContext); _runningStreams[stream.StreamId] = stream; @@ -416,16 +416,17 @@ internal async ValueTask CreateControlStream(int? id) return stream; } - internal async ValueTask CreateRequestStream(IEnumerable> headers, Http3RequestHeaderHandler headerHandler = null, bool endStream = false, TaskCompletionSource tsc = null) + internal async ValueTask CreateRequestStream(IEnumerable> headers, + Http3RequestHeaderHandler headerHandler = null, bool endStream = false, TaskCompletionSource tsc = null, PipeScheduler clientWriterScheduler = null) { - var stream = CreateRequestStreamCore(headerHandler); + var stream = CreateRequestStreamCore(headerHandler, clientWriterScheduler); if (tsc is not null) { stream.StartStreamDisposeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } - if (headers is not null) + if (headers is not null && headers.Any()) { await stream.SendHeadersAsync(headers, endStream); } @@ -437,9 +438,10 @@ internal async ValueTask CreateRequestStream(IEnumerable CreateRequestStream(Http3HeadersEnumerator headers, Http3RequestHeaderHandler headerHandler = null, bool endStream = false, TaskCompletionSource tsc = null) + internal async ValueTask CreateRequestStream(Http3HeadersEnumerator headers, Http3RequestHeaderHandler headerHandler = null, + bool endStream = false, TaskCompletionSource tsc = null, PipeScheduler clientWriterScheduler = null) { - var stream = CreateRequestStreamCore(headerHandler); + var stream = CreateRequestStreamCore(headerHandler, clientWriterScheduler); if (tsc is not null) { @@ -455,7 +457,7 @@ internal async ValueTask CreateRequestStream(Http3HeadersEnu return stream; } - private Http3RequestStream CreateRequestStreamCore(Http3RequestHeaderHandler headerHandler) + private Http3RequestStream CreateRequestStreamCore(Http3RequestHeaderHandler headerHandler, PipeScheduler clientWriterScheduler) { var requestStreamId = GetStreamId(0x00); if (!_streamContextPool.TryDequeue(out var testStreamContext)) @@ -466,7 +468,7 @@ private Http3RequestStream CreateRequestStreamCore(Http3RequestHeaderHandler hea { Logger.LogDebug($"Reusing context for request stream {requestStreamId}."); } - testStreamContext.Initialize(requestStreamId); + testStreamContext.Initialize(requestStreamId, clientWriterScheduler); return new Http3RequestStream(this, Connection, testStreamContext, headerHandler ?? new Http3RequestHeaderHandler()); } @@ -566,7 +568,7 @@ internal async ValueTask ReceiveFrameAsync(bool expectEnd throw new InvalidOperationException("No data received."); } - if (Http3FrameReader.TryReadFrame(ref buffer, frame, out var framePayload)) + if (Http3FrameReader.TryReadFrame(ref buffer, frame, isContinuedFrame: false, out var framePayload)) { consumed = examined = framePayload.End; frame.Payload = framePayload.ToArray(); @@ -844,16 +846,14 @@ internal async ValueTask> ExpectSettingsAsync() var settings = new Dictionary(); while (true) { - var id = VariableLengthIntegerHelper.GetInteger(payload, out var consumed, out _); - if (id == -1) + if (!VariableLengthIntegerHelper.TryGetInteger(payload, out var consumed, out var id)) { break; } payload = payload.Slice(consumed); - var value = VariableLengthIntegerHelper.GetInteger(payload, out consumed, out _); - if (value == -1) + if (!VariableLengthIntegerHelper.TryGetInteger(payload, out consumed, out var value)) { break; } @@ -934,9 +934,9 @@ public async ValueTask TryReadStreamIdAsync() { if (!readableBuffer.IsEmpty) { - var id = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out examined); - if (id != -1) + if (VariableLengthIntegerHelper.TryGetInteger(readableBuffer, out consumed, out var id)) { + examined = consumed; return id; } } @@ -1013,6 +1013,7 @@ public TestMultiplexedConnectionContext(Http3InMemory testBase) Features.Set(this); Features.Set(this); ConnectionClosedRequested = ConnectionClosingCts.Token; + ConnectionClosed = ConnectionClosedCts.Token; MetricsContext = TestContextFactory.CreateMetricsContext(this); } @@ -1027,6 +1028,8 @@ public TestMultiplexedConnectionContext(Http3InMemory testBase) public CancellationTokenSource ConnectionClosingCts { get; set; } = new CancellationTokenSource(); + public CancellationTokenSource ConnectionClosedCts { get; set; } = new CancellationTokenSource(); + public long Error { get => _error ?? -1; @@ -1046,6 +1049,7 @@ public override void Abort(ConnectionAbortedException abortReason) { ToServerAcceptQueue.Writer.TryComplete(); ToClientAcceptQueue.Writer.TryComplete(); + ConnectionClosedCts.Cancel(); } public override async ValueTask AcceptAsync(CancellationToken cancellationToken = default) @@ -1119,38 +1123,30 @@ public TestStreamContext(bool canRead, bool canWrite, Http3InMemory testBase) _testBase = testBase; } - public void Initialize(long streamId) + public void Initialize(long streamId, PipeScheduler clientWriterScheduler = null) { - if (!_isComplete) - { - // Create new pipes when test stream context is reused rather than reseting them. - // This is required because the client tests read from these directly from these pipes. - // When a request is finished they'll check to see whether there is anymore content - // in the Application.Output pipe. If it has been reset then that code will error. - var inputOptions = Http3InMemory.GetInputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); - var outputOptions = Http3InMemory.GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); - - _inputPipe = new Pipe(inputOptions); - _outputPipe = new Pipe(outputOptions); - - _transportPipeReader = new CompletionPipeReader(_inputPipe.Reader); - _transportPipeWriter = new CompletionPipeWriter(_outputPipe.Writer); - - _pair = new DuplexPipePair( - new DuplexPipe(_transportPipeReader, _transportPipeWriter), - new DuplexPipe(_outputPipe.Reader, _inputPipe.Writer)); - } - else + if (_isComplete) { _pair.Application.Input.Complete(); _pair.Application.Output.Complete(); + } - _transportPipeReader.Reset(); - _transportPipeWriter.Reset(); + // Create new pipes when test stream context is reused rather than reseting them. + // This is required because the client tests read from these directly from these pipes. + // When a request is finished they'll check to see whether there is anymore content + // in the Application.Output pipe. If it has been reset then that code will error. + var inputOptions = Http3InMemory.GetInputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, clientWriterScheduler ?? PipeScheduler.ThreadPool); + var outputOptions = Http3InMemory.GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); - _inputPipe.Reset(); - _outputPipe.Reset(); - } + _inputPipe = new Pipe(inputOptions); + _outputPipe = new Pipe(outputOptions); + + _transportPipeReader = new CompletionPipeReader(_inputPipe.Reader); + _transportPipeWriter = new CompletionPipeWriter(_outputPipe.Writer); + + _pair = new DuplexPipePair( + new DuplexPipe(_transportPipeReader, _transportPipeWriter), + new DuplexPipe(_outputPipe.Reader, _inputPipe.Writer)); Features.Set(this); Features.Set(this); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs index 06d96adc238f..ab8bc5e9a1e6 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs @@ -360,6 +360,35 @@ await Http3Api.WaitForConnectionErrorAsync( MetricsAssert.Equal(ConnectionEndReason.ClosedCriticalStream, Http3Api.ConnectionTags); } + [Theory] + [InlineData((int)Http3FrameType.Settings, 20_000)] + //[InlineData((int)Http3FrameType.GoAway, 30)] // GoAway frames trigger graceful connection close which races with sending FRAME_ERROR + [InlineData((int)Http3FrameType.CancelPush, 30)] + [InlineData((int)Http3FrameType.MaxPushId, 30)] + [InlineData(int.MaxValue, 20_000)] // Unknown frame type + public async Task ControlStream_ClientToServer_LargeFrame_ConnectionError(int frameType, int length) + { + await Http3Api.InitializeConnectionAsync(_noopApplication); + + var controlStream = await Http3Api.CreateControlStream(); + + // Need to send settings frame before other frames, otherwise it's a connection error + if (frameType != (int)Http3FrameType.Settings) + { + await controlStream.SendSettingsAsync(new List()); + } + + await controlStream.SendFrameAsync((Http3FrameType)frameType, new byte[length]); + + await Http3Api.WaitForConnectionErrorAsync( + ignoreNonGoAwayFrames: true, + expectedLastStreamId: 0, + expectedErrorCode: Http3ErrorCode.FrameError, + matchExpectedErrorMessage: AssertExpectedErrorMessages, + expectedErrorMessage: CoreStrings.FormatHttp3ControlStreamFrameTooLarge(Http3Formatting.ToFormattedType((Http3FrameType)frameType))); + MetricsAssert.Equal(ConnectionEndReason.InvalidFrameLength, Http3Api.ConnectionTags); + } + [Fact] public async Task SETTINGS_MaxFieldSectionSizeSent_ServerReceivesValue() { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs index 6a44a92e6805..b7697117452e 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Globalization; +using System.IO.Pipelines; using System.Net.Http; using System.Runtime.ExceptionServices; using System.Text; @@ -11,8 +13,8 @@ using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; @@ -1977,7 +1979,7 @@ public async Task RequestTrailers_CanReadTrailersFromRequest() var trailers = new[] { new KeyValuePair("TestName", "TestValue"), - }; + }; var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async c => { await c.Request.Body.DrainAsync(default); @@ -2370,6 +2372,21 @@ public Task HEADERS_Received_HeaderBlockOverLimitx2_ConnectionError() return HEADERS_Received_InvalidHeaderFields_StreamError(headers, CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http3ErrorCode.RequestRejected); } + [Fact] + public Task HEADERS_Received_HeaderValueOverLimit_ConnectionError() + { + var limit = _serviceContext.ServerOptions.Limits.Http3.MaxRequestHeaderFieldSize; + // Single header value exceeds limit + var headers = new[] + { + new KeyValuePair("a", new string('a', limit + 1)), + }; + + return HEADERS_Received_InvalidHeaderFields_StreamError(headers, + SR.Format(SR.net_http_headers_exceeded_length, limit), + Http3ErrorCode.InternalError); + } + [Fact] public async Task HEADERS_Received_TooManyHeaders_431() { @@ -3000,4 +3017,296 @@ public async Task GetMemory_AfterAbort_GetsFakeMemory(int sizeHint) context.Response.BodyWriter.Advance(memory.Length); }, headers); } + + [Fact] + public async Task ControlStream_CloseBeforeSendingSettings() + { + await Http3Api.InitializeConnectionAsync(_noopApplication); + + var outboundcontrolStream = await Http3Api.CreateControlStream(); + + await outboundcontrolStream.EndStreamAsync(); + + await outboundcontrolStream.ReceiveEndAsync(); + } + + [Fact] + public async Task ControlStream_PartialFrameThenClose() + { + await Http3Api.InitializeConnectionAsync(_noopApplication); + + var outboundcontrolStream = await Http3Api.CreateControlStream(); + + var settings = new List + { + new Http3PeerSetting(Internal.Http3.Http3SettingType.MaxFieldSectionSize, 100), + new Http3PeerSetting(Internal.Http3.Http3SettingType.EnableWebTransport, 1), + new Http3PeerSetting(Internal.Http3.Http3SettingType.H3Datagram, 1) + }; + var len = Http3FrameWriter.CalculateSettingsSize(settings); + + Http3FrameWriter.WriteHeader(Http3FrameType.Settings, len, outboundcontrolStream.Pair.Application.Output); + + var parameterLength = VariableLengthIntegerHelper.WriteInteger(outboundcontrolStream.Pair.Application.Output.GetSpan(), (long)Internal.Http3.Http3SettingType.MaxFieldSectionSize); + outboundcontrolStream.Pair.Application.Output.Advance(parameterLength); + await outboundcontrolStream.Pair.Application.Output.FlushAsync(); + + await outboundcontrolStream.EndStreamAsync(); + + await outboundcontrolStream.ReceiveEndAsync(); + } + + [Fact] + public async Task SendDataObservesBackpressureFromApp() + { + var headers = new[] + { + new KeyValuePair(InternalHeaderNames.Method, "Custom"), + new KeyValuePair(InternalHeaderNames.Path, "/"), + new KeyValuePair(InternalHeaderNames.Scheme, "http"), + new KeyValuePair(InternalHeaderNames.Authority, "localhost:80"), + }; + + // Http3Stream hardcodes a 64k size for the RequestBodyPipe there is also the transport Pipe which we can influence with MaxRequestBufferSize + // So we need to send enough to fill up the 64k Pipe as well as the 100 byte Pipe. + var sendSize = 1024 * 65; + _serviceContext.ServerOptions.Limits.MaxRequestBufferSize = 100; + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var startedReadingTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async c => + { + // Read a single byte to make sure data has gotten here before we start verifying backpressure in the test code + var res = await c.Request.BodyReader.ReadAsync(); + Assert.Equal(sendSize, res.Buffer.Length); + c.Request.BodyReader.AdvanceTo(res.Buffer.Slice(1).Start); + startedReadingTcs.SetResult(); + + await tcs.Task; + res = await c.Request.BodyReader.ReadAsync(); + Assert.Equal(sendSize - 1, res.Buffer.Length); + c.Request.BodyReader.AdvanceTo(res.Buffer.End); + }, headers); + + var sendTask = requestStream.SendDataAsync(Encoding.ASCII.GetBytes(new string('a', sendSize))); + + // Wait for "app" code to start reading to ensure it has gotten bytes before we start verifying backpressure + await startedReadingTcs.Task; + Assert.False(sendTask.IsCompleted); + tcs.SetResult(); + + await sendTask; + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + Assert.Equal("200", responseHeaders[InternalHeaderNames.Status]); + + await requestStream.ExpectReceiveEndOfStream(); + } + + [Fact] + public async Task Request_FrameParsingSingleByteAtATimeWorks() + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var total = 0; + var trailerValue = string.Empty; + await Http3Api.InitializeConnectionAsync(async context => + { + var buffer = new byte[100]; + var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length); + var captureTcs = tcs; + tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + captureTcs.SetResult(); + Assert.Equal(1, read); + total = read; + while (read > 0) + { + read = await context.Request.Body.ReadAsync(buffer, total, buffer.Length - total); + captureTcs = tcs; + tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + captureTcs.SetResult(); + total += read; + if (read == 0) + { + break; + } + Assert.Equal(1, read); + } + + trailerValue = context.Request.GetTrailer("TestName"); + }); + + // Use Inline scheduling and buffer size of 1 to guarantee each write will wait for the parsing loop to complete before writing more data + _serviceContext.ServerOptions.Limits.MaxRequestBufferSize = 1; + var stream = await Http3Api.CreateRequestStream(headers: [], clientWriterScheduler: PipeScheduler.Inline); + + // Use local pipe to write frames so we can get the entire buffer in order to write it one byte at a time + var bufferPipe = new Pipe(); + Http3FrameWriter.WriteHeader(Http3FrameType.Headers, frameLength: 38, bufferPipe.Writer); + + var headersTotalSize = 0; + var headers = new Http3HeadersEnumerator(); + headers.Initialize(new Dictionary() { + { InternalHeaderNames.Method, "POST" }, + { InternalHeaderNames.Path, "/" }, + { InternalHeaderNames.Scheme, "http" }, }); + + var mem = bufferPipe.Writer.GetMemory(); + var done = QPackHeaderWriter.BeginEncodeHeaders(headers, mem.Span, ref headersTotalSize, out var length); + Assert.True(done); + bufferPipe.Writer.Advance(length); + await bufferPipe.Writer.FlushAsync(); + + // Write header frame one byte at a time + await WriteOneByteAtATime(bufferPipe.Reader, stream.Pair.Application.Output); + + Http3FrameWriter.WriteHeader(Http3FrameType.Data, frameLength: 12, bufferPipe.Writer); + await bufferPipe.Writer.FlushAsync(); + + // Write data header one byte at a time + await WriteOneByteAtATime(bufferPipe.Reader, stream.Pair.Application.Output); + + bufferPipe.Writer.Write(new byte[12]); + await bufferPipe.Writer.FlushAsync(); + + // Write data in data frame one byte at a time + // Don't use WriteOneByteAtATime() as we want to wait on the TCS after every flush to make sure app code consumed the data + // before we send another byte + var res = await bufferPipe.Reader.ReadAsync(); + for (var i = 0; i < res.Buffer.Length; i++) + { + mem = stream.Pair.Application.Output.GetMemory(); + mem.Span[0] = res.Buffer.Slice(i).FirstSpan[0]; + stream.Pair.Application.Output.Advance(1); + // Use TCS to make sure app can read data before we send more + var capturedTcs = tcs; + await stream.Pair.Application.Output.FlushAsync(); + await capturedTcs.Task; + } + bufferPipe.Reader.AdvanceTo(res.Buffer.End); + + var trailers = new Http3HeadersEnumerator(); + trailers.Initialize(new Dictionary() + { + { "TestName", "TestValue" } + }); + + Http3FrameWriter.WriteHeader(Http3FrameType.Headers, frameLength: 22, bufferPipe.Writer); + mem = bufferPipe.Writer.GetMemory(); + done = QPackHeaderWriter.BeginEncodeHeaders(trailers, mem.Span, ref headersTotalSize, out length); + Assert.True(done); + bufferPipe.Writer.Advance(length); + await bufferPipe.Writer.FlushAsync(); + + // Write trailer frame one byte at a time + await WriteOneByteAtATime(bufferPipe.Reader, stream.Pair.Application.Output); + + await stream.EndStreamAsync(); + + var responseHeaders = await stream.ExpectHeadersAsync(); + Assert.Equal(3, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[InternalHeaderNames.Status]); + Assert.Equal("0", responseHeaders["content-length"]); + + await stream.ExpectReceiveEndOfStream(); + + Assert.Equal(12, total); + Assert.Equal("TestValue", trailerValue); + } + + [Fact] + public async Task Control_FrameParsingSingleByteAtATimeWorks() + { + await Http3Api.InitializeConnectionAsync(_noopApplication); + + // Use Inline scheduling and buffer size of 1 to guarantee each write will wait for the parsing loop to complete before writing more data + _serviceContext.ServerOptions.Limits.MaxRequestBufferSize = 1; + var outboundcontrolStream = await Http3Api.CreateControlStream(clientWriterScheduler: PipeScheduler.Inline); + + // Use local pipe to write frames so we can get the entire buffer in order to write it one byte at a time + var bufferPipe = new Pipe(); + + var settings = new List + { + new Http3PeerSetting(Internal.Http3.Http3SettingType.MaxFieldSectionSize, 100), + new Http3PeerSetting(Internal.Http3.Http3SettingType.EnableWebTransport, 1), + new Http3PeerSetting(Internal.Http3.Http3SettingType.H3Datagram, 1) + }; + var len = Http3FrameWriter.CalculateSettingsSize(settings); + + Http3FrameWriter.WriteHeader(Http3FrameType.Settings, len, bufferPipe.Writer); + var mem = bufferPipe.Writer.GetMemory(); + Http3FrameWriter.WriteSettings(settings, mem.Span); + + bufferPipe.Writer.Advance(len); + await bufferPipe.Writer.FlushAsync(); + + // Write Settings frame one byte at a time + await WriteOneByteAtATime(bufferPipe.Reader, outboundcontrolStream.Pair.Application.Output); + + var fieldSetting = await Http3Api.ServerReceivedSettingsReader.ReadAsync().DefaultTimeout(); + + Assert.Equal(Internal.Http3.Http3SettingType.MaxFieldSectionSize, fieldSetting.Key); + Assert.Equal(100, fieldSetting.Value); + + fieldSetting = await Http3Api.ServerReceivedSettingsReader.ReadAsync().DefaultTimeout(); + Assert.Equal(Internal.Http3.Http3SettingType.EnableWebTransport, fieldSetting.Key); + Assert.Equal(1, fieldSetting.Value); + + fieldSetting = await Http3Api.ServerReceivedSettingsReader.ReadAsync().DefaultTimeout(); + Assert.Equal(Internal.Http3.Http3SettingType.H3Datagram, fieldSetting.Key); + Assert.Equal(1, fieldSetting.Value); + + // Frames must be well-formed otherwise we close the connection with a frame error + Http3FrameWriter.WriteHeader(Http3FrameType.CancelPush, frameLength: 2, bufferPipe.Writer); + var idLength = VariableLengthIntegerHelper.WriteInteger(bufferPipe.Writer.GetSpan(), longToEncode: 1026); + bufferPipe.Writer.Advance(idLength); + await bufferPipe.Writer.FlushAsync(); + + // Write CancelPush frame one byte at a time + await WriteOneByteAtATime(bufferPipe.Reader, outboundcontrolStream.Pair.Application.Output); + + // Frames must be well-formed otherwise we close the connection with a frame error + Http3FrameWriter.WriteHeader(Http3FrameType.GoAway, frameLength: 4, bufferPipe.Writer); + idLength = VariableLengthIntegerHelper.WriteInteger(bufferPipe.Writer.GetSpan(), longToEncode: 100026); + bufferPipe.Writer.Advance(idLength); + await bufferPipe.Writer.FlushAsync(); + + try + { + // Write GoAway frame one byte at a time + await WriteOneByteAtATime(bufferPipe.Reader, outboundcontrolStream.Pair.Application.Output); + } + // As soon as the GOAWAY frame identifier is processed we initiate the connection close process. + // That means it's possible to still be writing to the stream when we close the + // connection which would result in an exception. We'll just ignore the exception in this case. + catch (Exception) { } + + await outboundcontrolStream.EndStreamAsync(); + + // Check that connection is closed. + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + Http3Api.MultiplexedConnectionContext.ConnectionClosed.Register(() => tcs.TrySetResult()); + await tcs.Task; + + await outboundcontrolStream.ReceiveEndAsync(); + } + + private async Task WriteOneByteAtATime(PipeReader reader, PipeWriter writer) + { + var res = await reader.ReadAsync(); + try + { + for (var i = 0; i < res.Buffer.Length; i++) + { + var mem = writer.GetMemory(); + mem.Span[0] = res.Buffer.Slice(i).FirstSpan[0]; + writer.Advance(1); + await writer.FlushAsync(); + } + } + finally + { + reader.AdvanceTo(res.Buffer.End); + } + } } diff --git a/src/Shared/runtime/Http3/Helpers/VariableLengthIntegerHelper.cs b/src/Shared/runtime/Http3/Helpers/VariableLengthIntegerHelper.cs index 3a343a62a4cc..c7f1ec908d0f 100644 --- a/src/Shared/runtime/Http3/Helpers/VariableLengthIntegerHelper.cs +++ b/src/Shared/runtime/Http3/Helpers/VariableLengthIntegerHelper.cs @@ -128,19 +128,19 @@ static bool TryReadSlow(ref SequenceReader reader, out long value) } } - public static long GetInteger(in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined) + // If callsite has 'examined', set it to buffer.End if the integer wasn't successfully read, otherwise set examined = consumed. + public static bool TryGetInteger(in ReadOnlySequence buffer, out SequencePosition consumed, out long integer) { var reader = new SequenceReader(buffer); - if (TryRead(ref reader, out long value)) + if (TryRead(ref reader, out integer)) { - consumed = examined = buffer.GetPosition(reader.Consumed); - return value; + consumed = buffer.GetPosition(reader.Consumed); + return true; } else { - consumed = default; - examined = buffer.End; - return -1; + consumed = buffer.Start; + return false; } } diff --git a/src/Shared/test/Shared.Tests/runtime/Http3/VariableLengthIntegerHelperTests.cs b/src/Shared/test/Shared.Tests/runtime/Http3/VariableLengthIntegerHelperTests.cs index d67d24c0ba25..e461bfd41ed4 100644 --- a/src/Shared/test/Shared.Tests/runtime/Http3/VariableLengthIntegerHelperTests.cs +++ b/src/Shared/test/Shared.Tests/runtime/Http3/VariableLengthIntegerHelperTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -223,12 +223,12 @@ public void GetInteger_ValidSegmentedSequence() MemorySegment memorySegment2 = memorySegment1.Append(new byte[] { 0, 0, 0, 0, 0, 0, 2 }); ReadOnlySequence readOnlySequence = new ReadOnlySequence( memorySegment1, 0, memorySegment2, memorySegment2.Memory.Length); - long result = VariableLengthIntegerHelper.GetInteger(readOnlySequence, - out SequencePosition consumed, out SequencePosition examined); + bool result = VariableLengthIntegerHelper.TryGetInteger(readOnlySequence, + out SequencePosition consumed, out long integer); - Assert.Equal(2, result); + Assert.True(result); + Assert.Equal(2, integer); Assert.Equal(7, consumed.GetInteger()); - Assert.Equal(7, examined.GetInteger()); } [Fact] @@ -238,12 +238,11 @@ public void GetInteger_NotValidSegmentedSequence() MemorySegment memorySegment2 = memorySegment1.Append(new byte[] { 0, 0, 0, 0, 0, 2 }); ReadOnlySequence readOnlySequence = new ReadOnlySequence( memorySegment1, 0, memorySegment2, memorySegment2.Memory.Length); - long result = VariableLengthIntegerHelper.GetInteger(readOnlySequence, - out SequencePosition consumed, out SequencePosition examined); + bool result = VariableLengthIntegerHelper.TryGetInteger(readOnlySequence, + out SequencePosition consumed, out long integer); - Assert.Equal(-1, result); + Assert.False(result); Assert.Equal(0, consumed.GetInteger()); - Assert.Equal(6, examined.GetInteger()); } [Fact] diff --git a/src/submodules/googletest b/src/submodules/googletest index e235eb34c6c4..24a9e940d481 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit e235eb34c6c4fed790ccdad4b16394301360dcd4 +Subproject commit 24a9e940d481f992ba852599c78bb2217362847b