diff --git a/NuGet.config b/NuGet.config index 1c2f27eb90ce..a972af091630 100644 --- a/NuGet.config +++ b/NuGet.config @@ -6,8 +6,10 @@ + + @@ -28,8 +30,10 @@ + + diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props index af5cc98a0d47..f1b0e807fea1 100644 --- a/eng/Baseline.Designer.props +++ b/eng/Baseline.Designer.props @@ -2,117 +2,117 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - + - + - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 @@ -120,138 +120,138 @@ - 8.0.13 + 8.0.15 - + - + - + - 8.0.13 + 8.0.15 - + - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - + - 8.0.13 + 8.0.15 - - + + - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - - + + - 8.0.13 + 8.0.15 - + - 8.0.13 + 8.0.15 - + - 8.0.13 + 8.0.15 - + - 8.0.13 + 8.0.15 - - + + - 8.0.13 + 8.0.15 - - - + + + - 8.0.13 + 8.0.15 - - + + - 8.0.13 + 8.0.15 - - + + - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - - + + @@ -259,7 +259,7 @@ - 8.0.13 + 8.0.15 @@ -268,51 +268,51 @@ - 8.0.13 + 8.0.15 - + - + - + - + - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - + - + - + - 8.0.13 + 8.0.15 - - + + @@ -322,8 +322,8 @@ - - + + @@ -331,8 +331,8 @@ - - + + @@ -343,58 +343,58 @@ - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - - + + - 8.0.13 + 8.0.15 - + - + - + - 8.0.13 + 8.0.15 - + - + - + - 8.0.13 + 8.0.15 - + - 8.0.13 + 8.0.15 @@ -403,7 +403,7 @@ - 8.0.13 + 8.0.15 @@ -411,71 +411,71 @@ - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - + - + - + - + - 8.0.13 + 8.0.15 - + - + - + - 8.0.13 + 8.0.15 - - + + - 8.0.13 + 8.0.15 - - + + - 8.0.13 + 8.0.15 @@ -491,27 +491,27 @@ - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - + - 8.0.13 + 8.0.15 @@ -520,23 +520,23 @@ - 8.0.13 + 8.0.15 - + - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 @@ -545,54 +545,54 @@ - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - - + + - - + + - - + + - 8.0.13 + 8.0.15 - - + + - - + + - - + + - - + + @@ -600,83 +600,83 @@ - 8.0.13 + 8.0.15 - + - + - + - 8.0.13 + 8.0.15 - + - + - + - 8.0.13 + 8.0.15 - + - + - + - 8.0.13 + 8.0.15 - + - + - + - 8.0.13 + 8.0.15 - - - - + + + + - 8.0.13 + 8.0.15 @@ -685,64 +685,64 @@ - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - + - 8.0.13 + 8.0.15 - + - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 @@ -764,7 +764,7 @@ - 8.0.13 + 8.0.15 @@ -786,7 +786,7 @@ - 8.0.13 + 8.0.15 @@ -802,23 +802,23 @@ - 8.0.13 + 8.0.15 - + - + - + @@ -826,24 +826,24 @@ - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - - - + + + - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 @@ -853,7 +853,7 @@ - 8.0.13 + 8.0.15 @@ -862,73 +862,73 @@ - 8.0.13 + 8.0.15 - + - + - + - 8.0.13 + 8.0.15 - + - + - + - 8.0.13 + 8.0.15 - + - + - + - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 @@ -957,11 +957,11 @@ - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 @@ -979,18 +979,18 @@ - 8.0.13 + 8.0.15 - 8.0.13 + 8.0.15 - + - 8.0.13 + 8.0.15 diff --git a/eng/Baseline.xml b/eng/Baseline.xml index 9efbb290ef60..9746528d4365 100644 --- a/eng/Baseline.xml +++ b/eng/Baseline.xml @@ -4,110 +4,110 @@ This file contains a list of all the packages and their versions which were rele Update this list when preparing for a new patch. --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index f67573e8e603..5a8d0360c7e2 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -9,37 +9,37 @@ --> - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - d00955545e8afc997726aead9b0e6103b1ceade6 + 0118cb6810a48869bf7494aabd86ef44da5940a3 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - d00955545e8afc997726aead9b0e6103b1ceade6 + 0118cb6810a48869bf7494aabd86ef44da5940a3 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - d00955545e8afc997726aead9b0e6103b1ceade6 + 0118cb6810a48869bf7494aabd86ef44da5940a3 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - d00955545e8afc997726aead9b0e6103b1ceade6 + 0118cb6810a48869bf7494aabd86ef44da5940a3 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - d00955545e8afc997726aead9b0e6103b1ceade6 + 0118cb6810a48869bf7494aabd86ef44da5940a3 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - d00955545e8afc997726aead9b0e6103b1ceade6 + 0118cb6810a48869bf7494aabd86ef44da5940a3 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - d00955545e8afc997726aead9b0e6103b1ceade6 + 0118cb6810a48869bf7494aabd86ef44da5940a3 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - d00955545e8afc997726aead9b0e6103b1ceade6 + 0118cb6810a48869bf7494aabd86ef44da5940a3 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -121,9 +121,9 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 5535e31a712343a63f5d7d796cd874e563e5ac14 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 1584e493603cfc4e9b36b77d6d4afe97de6363f9 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -139,7 +139,7 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 1584e493603cfc4e9b36b77d6d4afe97de6363f9 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -185,9 +185,9 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 5535e31a712343a63f5d7d796cd874e563e5ac14 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 1584e493603cfc4e9b36b77d6d4afe97de6363f9 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 https://github.com/dotnet/source-build-externals @@ -207,13 +207,13 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 2d7eea252964e69be94cb9c847b371b23e4dd470 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 81cabf2857a01351e5ab578947c7403a5b128ad1 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 5535e31a712343a63f5d7d796cd874e563e5ac14 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -275,17 +275,17 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 81cabf2857a01351e5ab578947c7403a5b128ad1 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 1584e493603cfc4e9b36b77d6d4afe97de6363f9 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 1584e493603cfc4e9b36b77d6d4afe97de6363f9 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 1584e493603cfc4e9b36b77d6d4afe97de6363f9 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -316,22 +316,22 @@ Win-x64 is used here because we have picked an arbitrary runtime identifier to flow the version of the latest NETCore.App runtime. All Runtime.$rid packages should have the same version. --> - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 1584e493603cfc4e9b36b77d6d4afe97de6363f9 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 1584e493603cfc4e9b36b77d6d4afe97de6363f9 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 1584e493603cfc4e9b36b77d6d4afe97de6363f9 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 1584e493603cfc4e9b36b77d6d4afe97de6363f9 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 https://github.com/dotnet/xdt @@ -368,9 +368,9 @@ - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 1584e493603cfc4e9b36b77d6d4afe97de6363f9 + 50c4cb9fc31c47f03eac865d7bc518af173b74b7 https://github.com/dotnet/winforms diff --git a/eng/Versions.props b/eng/Versions.props index 400fb877670e..5f7b7b1220b7 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -11,7 +11,7 @@ 16 - false + true 7.1.2 7.* 8.0.2 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14-servicing.25111.18 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15-servicing.25164.13 8.0.0 8.0.1 8.0.0 @@ -93,7 +93,7 @@ 8.0.0 8.0.0 8.0.0 - 8.0.14-servicing.25111.18 + 8.0.15-servicing.25164.13 8.0.1 8.0.1 8.0.1 @@ -109,11 +109,11 @@ 8.0.0 8.0.2 8.0.0 - 8.0.14-servicing.25111.18 + 8.0.15-servicing.25164.13 8.0.1 8.0.1 - 8.0.1 - 8.0.0 + 8.0.2 + 8.0.1 8.0.0-rtm.23520.14 8.0.0 8.0.1 @@ -129,9 +129,9 @@ 8.0.0 8.0.0 8.0.0 - 8.0.14-servicing.25111.18 + 8.0.15-servicing.25164.13 - 8.0.14-servicing.25111.18 + 8.0.15-servicing.25164.13 8.0.0 8.0.1 @@ -143,14 +143,14 @@ 8.1.0-preview.23604.1 8.1.0-preview.23604.1 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 - 8.0.14 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 + 8.0.15 4.8.0-7.24574.2 4.8.0-7.24574.2 diff --git a/global.json b/global.json index 1ea9bd10bd84..dfea7ae6dee6 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "8.0.113" + "version": "8.0.114" }, "tools": { - "dotnet": "8.0.113", + "dotnet": "8.0.114", "runtimes": { "dotnet/x86": [ "$(MicrosoftNETCoreBrowserDebugHostTransportVersion)" diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index 2c7c3e8ef18d..68908731bf54 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -734,4 +734,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 599a55f50212..5bb9452a5da1 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); + // https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1-2.12.1 + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(incomingFrame.FormattedType), Http3ErrorCode.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); + // 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); + } } + } - _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); + } + + _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); - _context.ConnectionContext.Features.Get()?.RequestClose(); + // StopProcessingNextRequest must be called before RequestClose to ensure it's considered client initiated. + _context.Connection.StopProcessingNextRequest(serverInitiated: false); + _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); + } + } + 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 dbcd774af4d6..44034fa365f2 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 61625f180cd2..17178ffbf133 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; @@ -607,6 +606,8 @@ public async Task ProcessRequestAsync(IHttpApplication appli try { + var incomingFrame = new Http3RawFrame(); + var isContinuedFrame = false; while (_isClosed == 0) { var result = await Input.ReadAsync(); @@ -618,12 +619,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; } } @@ -746,22 +754,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), + CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame), // The server should never receive push promise Http3FrameType.PushPromise => throw new Http3ConnectionErrorException( - CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame), + CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame), _ => ProcessUnknownFrameAsync(), }; } @@ -773,11 +782,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); } @@ -789,8 +800,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 e02573b7b225..544b338ee38f 100644 --- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs @@ -388,15 +388,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; @@ -409,16 +409,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); } @@ -430,9 +431,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) { @@ -448,7 +450,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)) @@ -459,7 +461,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()); } @@ -559,7 +561,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(); @@ -837,16 +839,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; } @@ -927,9 +927,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; } } @@ -1005,6 +1005,7 @@ public TestMultiplexedConnectionContext(Http3InMemory testBase) Features.Set(this); Features.Set(this); ConnectionClosedRequested = ConnectionClosingCts.Token; + ConnectionClosed = ConnectionClosedCts.Token; } public override string ConnectionId { get; set; } @@ -1017,6 +1018,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; @@ -1033,6 +1036,7 @@ public override void Abort(ConnectionAbortedException abortReason) { ToServerAcceptQueue.Writer.TryComplete(); ToClientAcceptQueue.Writer.TryComplete(); + ConnectionClosedCts.Cancel(); } public override async ValueTask AcceptAsync(CancellationToken cancellationToken = default) @@ -1106,38 +1110,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 b1ff24b43bf8..054577365d7d 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs @@ -354,6 +354,34 @@ await Http3Api.WaitForConnectionErrorAsync( expectedErrorMessage: CoreStrings.Http3ErrorControlStreamClosed); } + [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))); + } + [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 728bc3458b74..c23e11328e43 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -1,11 +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; -using System.Collections.Generic; +using System.Buffers; using System.Globalization; -using System.IO; -using System.Linq; +using System.IO.Pipelines; using System.Net.Http; using System.Runtime.ExceptionServices; using System.Text; @@ -17,6 +15,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Xunit; @@ -1982,7 +1981,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); @@ -2360,6 +2359,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() { @@ -2976,4 +2990,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]