public static readonly Version HttpVersion30 = new Version(3, 0);
public static readonly SslApplicationProtocol Http3ApplicationProtocol = new SslApplicationProtocol("h3-29");
- /// <summary>
- /// If we receive a settings frame larger than this, tear down the connection with an error.
- /// </summary>
- private const int MaximumSettingsPayloadLength = 4096;
-
- /// <summary>
- /// Unknown frame types with a payload larger than this will result in tearing down the connection with an error.
- /// Frames smaller than this will be ignored and drained.
- /// </summary>
- private const int MaximumUnknownFramePayloadLength = 4096;
-
private readonly HttpConnectionPool _pool;
private readonly HttpAuthority? _origin;
private readonly HttpAuthority _authority;
buffer[2] = (byte)payloadLength;
buffer[3] = (byte)Http3SettingType.MaxHeaderListSize;
- return buffer.Slice(4 + integerLength).ToArray();
+ return buffer.Slice(0, 4 + integerLength).ToArray();
}
/// <summary>
/// </summary>
private async Task ProcessServerStreamAsync(QuicStream stream)
{
+ ArrayBuffer buffer = default;
+
try
{
await using (stream.ConfigureAwait(false))
- using (var buffer = new ArrayBuffer(initialSize: 32, usePool: true))
{
if (stream.CanWrite)
{
throw new Http3ConnectionException(Http3ErrorCode.StreamCreationError);
}
+ buffer = new ArrayBuffer(initialSize: 32, usePool: true);
+
int bytesRead;
try
// Discard the stream type header.
buffer.Discard(1);
- await ProcessServerControlStreamAsync(stream, buffer).ConfigureAwait(false);
+ // Ownership of buffer is transferred to ProcessServerControlStreamAsync.
+ ArrayBuffer bufferCopy = buffer;
+ buffer = default;
+
+ await ProcessServerControlStreamAsync(stream, bufferCopy).ConfigureAwait(false);
return;
case (byte)Http3StreamType.QPackDecoder:
if (Interlocked.Exchange(ref _haveServerQpackDecodeStream, 1) != 0)
throw new Http3ConnectionException(Http3ErrorCode.StreamCreationError);
}
- // The stream must not be closed, but we aren't using QPACK right now -- ignore.
+ // We haven't enabled QPack in our SETTINGS frame, so we shouldn't receive any meaningful data here.
+ // However, the standard says the stream must not be closed for the lifetime of the connection. Just ignore any data.
buffer.Dispose();
await stream.CopyToAsync(Stream.Null).ConfigureAwait(false);
return;
{
Abort(ex);
}
+ finally
+ {
+ buffer.Dispose();
+ }
}
/// <summary>
/// </summary>
private async Task ProcessServerControlStreamAsync(QuicStream stream, ArrayBuffer buffer)
{
- (Http3FrameType? frameType, long payloadLength) = await ReadFrameEnvelopeAsync().ConfigureAwait(false);
-
- if (frameType == null)
+ using (buffer)
{
- // Connection closed prematurely, expected SETTINGS frame.
- throw new Http3ConnectionException(Http3ErrorCode.ClosedCriticalStream);
- }
+ // Read the first frame of the control stream. Per spec:
+ // A SETTINGS frame MUST be sent as the first frame of each control stream.
- if (frameType != Http3FrameType.Settings)
- {
- // Expected SETTINGS as first frame of control stream.
- throw new Http3ConnectionException(Http3ErrorCode.MissingSettings);
- }
+ (Http3FrameType? frameType, long payloadLength) = await ReadFrameEnvelopeAsync().ConfigureAwait(false);
- await ProcessSettingsFrameAsync(payloadLength).ConfigureAwait(false);
+ if (frameType == null)
+ {
+ // Connection closed prematurely, expected SETTINGS frame.
+ throw new Http3ConnectionException(Http3ErrorCode.ClosedCriticalStream);
+ }
- while (true)
- {
- (frameType, payloadLength) = await ReadFrameEnvelopeAsync().ConfigureAwait(false);
+ if (frameType != Http3FrameType.Settings)
+ {
+ throw new Http3ConnectionException(Http3ErrorCode.MissingSettings);
+ }
+
+ await ProcessSettingsFrameAsync(payloadLength).ConfigureAwait(false);
+
+ // Read subsequent frames.
- switch (frameType)
+ while (true)
{
- case Http3FrameType.GoAway:
- await ProcessGoAwayFameAsync(payloadLength).ConfigureAwait(false);
- break;
- case Http3FrameType.Settings:
- // Only a single SETTINGS frame is supported.
- throw new Http3ConnectionException(Http3ErrorCode.UnexpectedFrame);
- case Http3FrameType.Headers:
- case Http3FrameType.Data:
- case Http3FrameType.MaxPushId:
- case Http3FrameType.DuplicatePush:
- // Servers should not send these frames to a control stream.
- throw new Http3ConnectionException(Http3ErrorCode.UnexpectedFrame);
- case Http3FrameType.PushPromise:
- case Http3FrameType.CancelPush:
- // Because we haven't sent any MAX_PUSH_ID frame, it is invalid to receive any push-related frames as they will all reference a too-large ID.
- throw new Http3ConnectionException(Http3ErrorCode.IdError);
- case null:
- // End of stream reached. If we're shutting down, stop looping. Otherwise, this is an error (this stream should not be closed for life of connection).
- bool shuttingDown;
- lock (SyncObj)
- {
- shuttingDown = ShuttingDown;
- }
- if (!shuttingDown)
- {
- throw new Http3ConnectionException(Http3ErrorCode.ClosedCriticalStream);
- }
- return;
- default:
- await SkipUnknownPayloadAsync(frameType.GetValueOrDefault(), payloadLength).ConfigureAwait(false);
- break;
+ (frameType, payloadLength) = await ReadFrameEnvelopeAsync().ConfigureAwait(false);
+
+ switch (frameType)
+ {
+ case Http3FrameType.GoAway:
+ await ProcessGoAwayFameAsync(payloadLength).ConfigureAwait(false);
+ break;
+ case Http3FrameType.Settings:
+ // If an endpoint receives a second SETTINGS frame on the control stream, the endpoint MUST respond with a connection error of type H3_FRAME_UNEXPECTED.
+ throw new Http3ConnectionException(Http3ErrorCode.UnexpectedFrame);
+ case Http3FrameType.Headers:
+ case Http3FrameType.Data:
+ case Http3FrameType.MaxPushId:
+ case Http3FrameType.DuplicatePush:
+ // Servers should not send these frames to a control stream.
+ throw new Http3ConnectionException(Http3ErrorCode.UnexpectedFrame);
+ case Http3FrameType.PushPromise:
+ case Http3FrameType.CancelPush:
+ // Because we haven't sent any MAX_PUSH_ID frame, it is invalid to receive any push-related frames as they will all reference a too-large ID.
+ throw new Http3ConnectionException(Http3ErrorCode.IdError);
+ case null:
+ // End of stream reached. If we're shutting down, stop looping. Otherwise, this is an error (this stream should not be closed for life of connection).
+ bool shuttingDown;
+ lock (SyncObj)
+ {
+ shuttingDown = ShuttingDown;
+ }
+ if (!shuttingDown)
+ {
+ throw new Http3ConnectionException(Http3ErrorCode.ClosedCriticalStream);
+ }
+ return;
+ default:
+ await SkipUnknownPayloadAsync(frameType.GetValueOrDefault(), payloadLength).ConfigureAwait(false);
+ break;
+ }
}
}
async ValueTask ProcessSettingsFrameAsync(long settingsPayloadLength)
{
- if (settingsPayloadLength > MaximumSettingsPayloadLength)
- {
- if (NetEventSource.Log.IsEnabled())
- {
- Trace($"Received SETTINGS frame with {settingsPayloadLength} byte payload exceeding the {MaximumSettingsPayloadLength} byte maximum.");
- }
- throw new Http3ConnectionException(Http3ErrorCode.ExcessiveLoad);
- }
-
while (settingsPayloadLength != 0)
{
long settingId, settingValue;
settingsPayloadLength -= bytesRead;
+ if (settingsPayloadLength < 0)
+ {
+ // An integer was encoded past the payload length.
+ // A frame payload that contains additional bytes after the identified fields or a frame payload that terminates before the end of the identified fields MUST be treated as a connection error of type H3_FRAME_ERROR.
+ throw new Http3ConnectionException(Http3ErrorCode.FrameError);
+ }
+
+ buffer.Discard(bytesRead);
+
// Only support this single setting. Skip others.
if (settingId == (long)Http3SettingType.MaxHeaderListSize)
{
async ValueTask SkipUnknownPayloadAsync(Http3FrameType frameType, long payloadLength)
{
- if (payloadLength > MaximumUnknownFramePayloadLength)
- {
- Trace($"Received unknown frame type 0x{(long)frameType:x} with {payloadLength} byte payload exceeding the {MaximumUnknownFramePayloadLength} byte maximum.");
- throw new Http3ConnectionException(Http3ErrorCode.ExcessiveLoad);
- }
-
while (payloadLength != 0)
{
if (buffer.ActiveLength == 0)
{
}
- /// <summary>
- /// These are public interop test servers for various QUIC and HTTP/3 implementations,
- /// taken from https://github.com/quicwg/base-drafts/wiki/Implementations
- /// </summary>
[OuterLoop]
[Theory]
- [InlineData("https://quic.rocks:4433/")] // Chromium
- [InlineData("https://www.litespeedtech.com/")] // LiteSpeed
- [InlineData("https://quic.tech:8443/")] // Cloudflare
- public async Task Public_Interop_Success(string uri)
+ [MemberData(nameof(InteropUris))]
+ public async Task Public_Interop_ExactVersion_Success(string uri)
{
using HttpClient client = CreateHttpClient();
using HttpRequestMessage request = new HttpRequestMessage
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(3, response.Version.Major);
}
+
+ [OuterLoop]
+ [Theory]
+ [MemberData(nameof(InteropUris))]
+ public async Task Public_Interop_Upgrade_Success(string uri)
+ {
+ using HttpClient client = CreateHttpClient();
+
+ // First request uses HTTP/1 or HTTP/2 and receives an Alt-Svc either by header or (with HTTP/2) by frame.
+
+ using (HttpRequestMessage requestA = new HttpRequestMessage
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri(uri, UriKind.Absolute),
+ Version = HttpVersion30,
+ VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
+ })
+ {
+ using HttpResponseMessage responseA = await client.SendAsync(requestA).TimeoutAfter(20_000);
+ Assert.Equal(HttpStatusCode.OK, responseA.StatusCode);
+ Assert.NotEqual(3, responseA.Version.Major);
+ }
+
+ // Second request uses HTTP/3.
+
+ using (HttpRequestMessage requestB = new HttpRequestMessage
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri(uri, UriKind.Absolute),
+ Version = HttpVersion30,
+ VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
+ })
+ {
+ using HttpResponseMessage responseB = await client.SendAsync(requestB).TimeoutAfter(20_000);
+
+ Assert.Equal(HttpStatusCode.OK, responseB.StatusCode);
+ Assert.NotEqual(3, responseB.Version.Major);
+ }
+ }
+
+ /// <summary>
+ /// These are public interop test servers for various QUIC and HTTP/3 implementations,
+ /// taken from https://github.com/quicwg/base-drafts/wiki/Implementations
+ /// </summary>
+ public static TheoryData<string> InteropUris() =>
+ new TheoryData<string>
+ {
+ { "https://quic.rocks:4433/" }, // Chromium
+ { "https://http3-test.litespeedtech.com:4433/" }, // LiteSpeed
+ { "https://quic.tech:8443/" } // Cloudflare
+ };
}
}