diff --git a/src/libraries/Common/src/System/Net/Http/aspnetcore/Http3/QPack/H3StaticTable.Http3.cs b/src/libraries/Common/src/System/Net/Http/aspnetcore/Http3/QPack/H3StaticTable.Http3.cs index efa5f9b81d62e1..ebb9142752ef48 100644 --- a/src/libraries/Common/src/System/Net/Http/aspnetcore/Http3/QPack/H3StaticTable.Http3.cs +++ b/src/libraries/Common/src/System/Net/Http/aspnetcore/Http3/QPack/H3StaticTable.Http3.cs @@ -50,102 +50,102 @@ internal static partial class H3StaticTable CreateHeaderField(":authority", ""), // 0 CreateHeaderField(":path", "/"), // 1 CreateHeaderField("age", "0"), // 2 - CreateHeaderField("content-disposition", ""), - CreateHeaderField("content-length", "0"), - CreateHeaderField("cookie", ""), - CreateHeaderField("date", ""), - CreateHeaderField("etag", ""), - CreateHeaderField("if-modified-since", ""), - CreateHeaderField("if-none-match", ""), + CreateHeaderField("content-disposition", ""), //3 + CreateHeaderField("content-length", "0"), // 4 + CreateHeaderField("cookie", ""), // 5 + CreateHeaderField("date", ""), // 6 + CreateHeaderField("etag", ""), // 7 + CreateHeaderField("if-modified-since", ""), // 8 + CreateHeaderField("if-none-match", ""), // 9 CreateHeaderField("last-modified", ""), // 10 - CreateHeaderField("link", ""), - CreateHeaderField("location", ""), - CreateHeaderField("referer", ""), - CreateHeaderField("set-cookie", ""), - CreateHeaderField(":method", "CONNECT"), - CreateHeaderField(":method", "DELETE"), - CreateHeaderField(":method", "GET"), - CreateHeaderField(":method", "HEAD"), - CreateHeaderField(":method", "OPTIONS"), + CreateHeaderField("link", ""), // 11 + CreateHeaderField("location", ""), // 12 + CreateHeaderField("referer", ""), // 13 + CreateHeaderField("set-cookie", ""), // 14 + CreateHeaderField(":method", "CONNECT"), // 15 + CreateHeaderField(":method", "DELETE"), // 16 + CreateHeaderField(":method", "GET"), // 17 + CreateHeaderField(":method", "HEAD"), // 18 + CreateHeaderField(":method", "OPTIONS"), // 19 CreateHeaderField(":method", "POST"), // 20 - CreateHeaderField(":method", "PUT"), - CreateHeaderField(":scheme", "http"), - CreateHeaderField(":scheme", "https"), - CreateHeaderField(":status", "103"), - CreateHeaderField(":status", "200"), - CreateHeaderField(":status", "304"), - CreateHeaderField(":status", "404"), - CreateHeaderField(":status", "503"), - CreateHeaderField("accept", "*/*"), + CreateHeaderField(":method", "PUT"), // 21 + CreateHeaderField(":scheme", "http"), // 22 + CreateHeaderField(":scheme", "https"), // 23 + CreateHeaderField(":status", "103"), // 24 + CreateHeaderField(":status", "200"), // 25 + CreateHeaderField(":status", "304"), // 26 + CreateHeaderField(":status", "404"), // 27 + CreateHeaderField(":status", "503"), // 28 + CreateHeaderField("accept", "*/*"), //29 CreateHeaderField("accept", "application/dns-message"), // 30 - CreateHeaderField("accept-encoding", "gzip, deflate, br"), - CreateHeaderField("accept-ranges", "bytes"), - CreateHeaderField("access-control-allow-headers", "cache-control"), - CreateHeaderField("access-control-allow-origin", "content-type"), - CreateHeaderField("access-control-allow-origin", "*"), - CreateHeaderField("cache-control", "max-age=0"), - CreateHeaderField("cache-control", "max-age=2592000"), - CreateHeaderField("cache-control", "max-age=604800"), - CreateHeaderField("cache-control", "no-cache"), + CreateHeaderField("accept-encoding", "gzip, deflate, br"), // 31 + CreateHeaderField("accept-ranges", "bytes"), // 32 + CreateHeaderField("access-control-allow-headers", "cache-control"), // 33 + CreateHeaderField("access-control-allow-headers", "content-type"), // 34 + CreateHeaderField("access-control-allow-origin", "*"), // 35 + CreateHeaderField("cache-control", "max-age=0"), // 36 + CreateHeaderField("cache-control", "max-age=2592000"), // 37 + CreateHeaderField("cache-control", "max-age=604800"), // 38 + CreateHeaderField("cache-control", "no-cache"), // 39 CreateHeaderField("cache-control", "no-store"), // 40 - CreateHeaderField("cache-control", "public, max-age=31536000"), - CreateHeaderField("content-encoding", "br"), - CreateHeaderField("content-encoding", "gzip"), - CreateHeaderField("content-type", "application/dns-message"), - CreateHeaderField("content-type", "application/javascript"), - CreateHeaderField("content-type", "application/json"), - CreateHeaderField("content-type", "application/x-www-form-urlencoded"), - CreateHeaderField("content-type", "image/gif"), - CreateHeaderField("content-type", "image/jpeg"), + CreateHeaderField("cache-control", "public, max-age=31536000"), // 41 + CreateHeaderField("content-encoding", "br"), // 42 + CreateHeaderField("content-encoding", "gzip"), // 43 + CreateHeaderField("content-type", "application/dns-message"), // 44 + CreateHeaderField("content-type", "application/javascript"), // 45 + CreateHeaderField("content-type", "application/json"), // 46 + CreateHeaderField("content-type", "application/x-www-form-urlencoded"), // 47 + CreateHeaderField("content-type", "image/gif"), // 48 + CreateHeaderField("content-type", "image/jpeg"), // 49 CreateHeaderField("content-type", "image/png"), // 50 - CreateHeaderField("content-type", "text/css"), - CreateHeaderField("content-type", "text/html; charset=utf-8"), - CreateHeaderField("content-type", "text/plain"), - CreateHeaderField("content-type", "text/plain;charset=utf-8"), - CreateHeaderField("range", "bytes=0-"), - CreateHeaderField("strict-transport-security", "max-age=31536000"), - CreateHeaderField("strict-transport-security", "max-age=31536000;includesubdomains"), // TODO confirm spaces here don't matter? - CreateHeaderField("strict-transport-security", "max-age=31536000;includesubdomains; preload"), - CreateHeaderField("vary", "accept-encoding"), + CreateHeaderField("content-type", "text/css"), // 51 + CreateHeaderField("content-type", "text/html; charset=utf-8"), // 52 + CreateHeaderField("content-type", "text/plain"), // 53 + CreateHeaderField("content-type", "text/plain;charset=utf-8"), // 54 + CreateHeaderField("range", "bytes=0-"), // 55 + CreateHeaderField("strict-transport-security", "max-age=31536000"), // 56 + CreateHeaderField("strict-transport-security", "max-age=31536000; includesubdomains"), // 57; TODO confirm spaces here don't matter? + CreateHeaderField("strict-transport-security", "max-age=31536000; includesubdomains; preload"), // 58 + CreateHeaderField("vary", "accept-encoding"), // 59 CreateHeaderField("vary", "origin"), // 60 - CreateHeaderField("x-content-type-options", "nosniff"), - CreateHeaderField("x-xss-protection", "1; mode=block"), - CreateHeaderField(":status", "100"), - CreateHeaderField(":status", "204"), - CreateHeaderField(":status", "206"), - CreateHeaderField(":status", "302"), - CreateHeaderField(":status", "400"), - CreateHeaderField(":status", "403"), - CreateHeaderField(":status", "421"), + CreateHeaderField("x-content-type-options", "nosniff"), // 61 + CreateHeaderField("x-xss-protection", "1; mode=block"), // 62 + CreateHeaderField(":status", "100"), // 63 + CreateHeaderField(":status", "204"), // 64 + CreateHeaderField(":status", "206"), // 65 + CreateHeaderField(":status", "302"), // 66 + CreateHeaderField(":status", "400"), // 67 + CreateHeaderField(":status", "403"), // 68 + CreateHeaderField(":status", "421"), // 69 CreateHeaderField(":status", "425"), // 70 - CreateHeaderField(":status", "500"), - CreateHeaderField("accept-language", ""), - CreateHeaderField("access-control-allow-credentials", "FALSE"), - CreateHeaderField("access-control-allow-credentials", "TRUE"), - CreateHeaderField("access-control-allow-headers", "*"), - CreateHeaderField("access-control-allow-methods", "get"), - CreateHeaderField("access-control-allow-methods", "get, post, options"), - CreateHeaderField("access-control-allow-methods", "options"), - CreateHeaderField("access-control-expose-headers", "content-length"), + CreateHeaderField(":status", "500"), // 71 + CreateHeaderField("accept-language", ""), // 72 + CreateHeaderField("access-control-allow-credentials", "FALSE"), // 73 + CreateHeaderField("access-control-allow-credentials", "TRUE"), // 74 + CreateHeaderField("access-control-allow-headers", "*"), // 75 + CreateHeaderField("access-control-allow-methods", "get"), // 76 + CreateHeaderField("access-control-allow-methods", "get, post, options"), // 77 + CreateHeaderField("access-control-allow-methods", "options"), // 78 + CreateHeaderField("access-control-expose-headers", "content-length"), // 79 CreateHeaderField("access-control-request-headers", "content-type"), // 80 - CreateHeaderField("access-control-request-method", "get"), - CreateHeaderField("access-control-request-method", "post"), - CreateHeaderField("alt-svc", "clear"), - CreateHeaderField("authorization", ""), - CreateHeaderField("content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"), - CreateHeaderField("early-data", "1"), - CreateHeaderField("expect-ct", ""), - CreateHeaderField("forwarded", ""), - CreateHeaderField("if-range", ""), + CreateHeaderField("access-control-request-method", "get"), // 81 + CreateHeaderField("access-control-request-method", "post"), // 82 + CreateHeaderField("alt-svc", "clear"), // 83 + CreateHeaderField("authorization", ""), // 84 + CreateHeaderField("content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"), // 85 + CreateHeaderField("early-data", "1"), // 86 + CreateHeaderField("expect-ct", ""), // 87 + CreateHeaderField("forwarded", ""), // 88 + CreateHeaderField("if-range", ""), // 89 CreateHeaderField("origin", ""), // 90 - CreateHeaderField("purpose", "prefetch"), - CreateHeaderField("server", ""), - CreateHeaderField("timing-allow-origin", "*"), - CreateHeaderField("upgrading-insecure-requests", "1"), - CreateHeaderField("user-agent", ""), - CreateHeaderField("x-forwarded-for", ""), - CreateHeaderField("x-frame-options", "deny"), - CreateHeaderField("x-frame-options", "sameorigin"), + CreateHeaderField("purpose", "prefetch"), // 91 + CreateHeaderField("server", ""), // 92 + CreateHeaderField("timing-allow-origin", "*"), // 93 + CreateHeaderField("upgrading-insecure-requests", "1"), // 94 + CreateHeaderField("user-agent", ""), // 95 + CreateHeaderField("x-forwarded-for", ""), // 96 + CreateHeaderField("x-frame-options", "deny"), // 97 + CreateHeaderField("x-frame-options", "sameorigin"), // 98 }; private static HeaderField CreateHeaderField(string name, string value) diff --git a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs index a3d2c9a4215c2b..958cb2abc1e156 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs @@ -78,10 +78,19 @@ public async Task SendSettingsFrameAsync(ICollection<(long settingId, long setti await SendFrameAsync(SettingsFrame, buffer.AsMemory(0, bytesWritten)).ConfigureAwait(false); } - private Memory ConstructHeadersPayload(IEnumerable headers) + private Memory ConstructHeadersPayload(HttpStatusCode statusCode, IEnumerable headers, bool qpackEncodeStatus = false) { int bufferLength = QPackTestEncoder.MaxPrefixLength; + if (qpackEncodeStatus) + { + bufferLength += QPackTestEncoder.MaxVarIntLength * 2 + ":status".Length + 3; + } + else + { + headers = headers.Prepend(new HttpHeaderData(":status", ((int)statusCode).ToString(CultureInfo.InvariantCulture))); + }; + foreach (HttpHeaderData header in headers) { Debug.Assert(header.Name != null); @@ -96,6 +105,11 @@ private Memory ConstructHeadersPayload(IEnumerable headers bytesWritten += QPackTestEncoder.EncodePrefix(buffer.AsSpan(bytesWritten), 0, 0); + if (qpackEncodeStatus) + { + bytesWritten += QPackTestEncoder.EncodeStatusCode((int)statusCode, buffer.AsSpan(bytesWritten)); + } + foreach (HttpHeaderData header in headers) { bytesWritten += QPackTestEncoder.EncodeHeader(buffer.AsSpan(bytesWritten), header.Name, header.Value, header.ValueEncoding, header.HuffmanEncoded ? QPackFlags.HuffmanEncode : QPackFlags.None); @@ -104,14 +118,14 @@ private Memory ConstructHeadersPayload(IEnumerable headers return buffer.AsMemory(0, bytesWritten); } - private async Task SendHeadersFrameAsync(IEnumerable headers) + private async Task SendHeadersFrameAsync(HttpStatusCode statusCode, IEnumerable headers, bool qpackEncodeStatus = false) { - await SendFrameAsync(HeadersFrame, ConstructHeadersPayload(headers)).ConfigureAwait(false); + await SendFrameAsync(HeadersFrame, ConstructHeadersPayload(statusCode, headers, qpackEncodeStatus)).ConfigureAwait(false); } - private async Task SendPartialHeadersFrameAsync(IEnumerable headers) + private async Task SendPartialHeadersFrameAsync(HttpStatusCode statusCode, IEnumerable headers) { - Memory payload = ConstructHeadersPayload(headers); + Memory payload = ConstructHeadersPayload(statusCode, headers); await SendFrameHeaderAsync(HeadersFrame, payload.Length); @@ -234,28 +248,32 @@ public async Task SendResponseAsync(HttpStatusCode statusCode = HttpStatusCode.O await SendResponseBodyAsync(Encoding.UTF8.GetBytes(content ?? ""), isFinal).ConfigureAwait(false); } - private IEnumerable PrepareHeaders(HttpStatusCode statusCode, IEnumerable headers) + private IEnumerable PrepareHeaders(IEnumerable headers) { headers ??= Enumerable.Empty(); // Some tests use Content-Length with a null value to indicate Content-Length should not be set. headers = headers.Where(x => x.Name != "Content-Length" || x.Value != null); - headers = headers.Prepend(new HttpHeaderData(":status", ((int)statusCode).ToString(CultureInfo.InvariantCulture))); - return headers; } public async Task SendResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IEnumerable headers = null) { - headers = PrepareHeaders(statusCode, headers); - await SendHeadersFrameAsync(headers).ConfigureAwait(false); + headers = PrepareHeaders(headers); + await SendHeadersFrameAsync(statusCode, headers).ConfigureAwait(false); + } + + public async Task SendResponseHeadersWithEncodedStatusAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IEnumerable headers = null) + { + headers = PrepareHeaders(headers); + await SendHeadersFrameAsync(statusCode, headers, qpackEncodeStatus: true).ConfigureAwait(false); } public async Task SendPartialResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IEnumerable headers = null) { - headers = PrepareHeaders(statusCode, headers); - await SendPartialHeadersFrameAsync(headers).ConfigureAwait(false); + headers = PrepareHeaders(headers); + await SendPartialHeadersFrameAsync(statusCode, headers).ConfigureAwait(false); } public async Task SendResponseBodyAsync(byte[] content, bool isFinal = true) diff --git a/src/libraries/Common/tests/System/Net/Http/QPackTestEncoder.cs b/src/libraries/Common/tests/System/Net/Http/QPackTestEncoder.cs index a73f42067bcb7b..d1296bebd17bd7 100644 --- a/src/libraries/Common/tests/System/Net/Http/QPackTestEncoder.cs +++ b/src/libraries/Common/tests/System/Net/Http/QPackTestEncoder.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.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Text; namespace System.Net.Test.Common @@ -130,6 +132,39 @@ public static int EncodeInteger(Span buffer, int value, byte prefix, byte { return HPackEncoder.EncodeInteger(value, prefix, prefixMask, buffer); } + + // from System.Net.Http.QPack.H3StaticTable + private static readonly Dictionary s_statusIndex = new Dictionary + { + [103] = 24, + [200] = 25, + [304] = 26, + [404] = 27, + [503] = 28, + [100] = 63, + [204] = 64, + [206] = 65, + [302] = 66, + [400] = 67, + [403] = 68, + [421] = 69, + [425] = 70, + [500] = 71, + }; + + public static int EncodeStatusCode(int statusCode, Span buffer) + { + if (s_statusIndex.TryGetValue(statusCode, out var statusIdx)) + { + // Indexed Header Field + return EncodeHeader(buffer, statusIdx); + } + else + { + // Literal Header Field With Name Reference -- Index of any status present in the table can be used for reference + return EncodeHeader(buffer, s_statusIndex[100], statusCode.ToString(CultureInfo.InvariantCulture), valueEncoding: null); + } + } } [Flags] diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/QPackStaticTable.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/QPackStaticTable.cs index e2da7d0b6f2c0d..bc3d1992146962 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/QPackStaticTable.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/QPackStaticTable.cs @@ -9,105 +9,105 @@ internal static class QPackStaticTable // TODO: can we put some of this logic into H3StaticTable and/or generate it using data that is already there? internal static (HeaderDescriptor descriptor, string value)[] HeaderLookup { get; } = new (HeaderDescriptor descriptor, string value)[] { - (new HeaderDescriptor(":authority"), ""), - (new HeaderDescriptor(":path"), "/"), - (new HeaderDescriptor(KnownHeaders.Age), "0"), - (new HeaderDescriptor(KnownHeaders.ContentDisposition), ""), - (new HeaderDescriptor(KnownHeaders.ContentLength), "0"), - (new HeaderDescriptor(KnownHeaders.Date), ""), - (new HeaderDescriptor(KnownHeaders.ETag), ""), - (new HeaderDescriptor(KnownHeaders.IfModifiedSince), ""), - (new HeaderDescriptor(KnownHeaders.IfNoneMatch), ""), - (new HeaderDescriptor(KnownHeaders.LastModified), ""), - (new HeaderDescriptor(KnownHeaders.Link), ""), - (new HeaderDescriptor(KnownHeaders.Location), ""), - (new HeaderDescriptor(KnownHeaders.Referer), ""), - (new HeaderDescriptor(KnownHeaders.SetCookie), ""), - (new HeaderDescriptor(":method"), "CONNECT"), - (new HeaderDescriptor(":method"), "DELETE"), - (new HeaderDescriptor(":method"), "GET"), - (new HeaderDescriptor(":method"), "HEAD"), - (new HeaderDescriptor(":method"), "OPTIONS"), - (new HeaderDescriptor(":method"), "POST"), - (new HeaderDescriptor(":method"), "PUT"), - (new HeaderDescriptor(":scheme"), "http"), - (new HeaderDescriptor(":scheme"), "https"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "103"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "200"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "304"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "404"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "503"), - (new HeaderDescriptor(KnownHeaders.Accept), "*/*"), - (new HeaderDescriptor(KnownHeaders.Accept), "application/dns-message"), - (new HeaderDescriptor(KnownHeaders.AcceptEncoding), "gzip, deflate, br"), - (new HeaderDescriptor(KnownHeaders.AcceptRanges), "bytes"), - (new HeaderDescriptor(KnownHeaders.AccessControlAllowHeaders), "cache-control"), - (new HeaderDescriptor(KnownHeaders.AccessControlAllowHeaders), "content-type"), - (new HeaderDescriptor(KnownHeaders.AccessControlAllowHeaders), "*"), - (new HeaderDescriptor(KnownHeaders.AccessControlAllowOrigin), "*"), - (new HeaderDescriptor(KnownHeaders.CacheControl), "max-age=0"), - (new HeaderDescriptor(KnownHeaders.CacheControl), "max-age=2592000"), - (new HeaderDescriptor(KnownHeaders.CacheControl), "max-age=604800"), - (new HeaderDescriptor(KnownHeaders.CacheControl), "no-cache"), - (new HeaderDescriptor(KnownHeaders.CacheControl), "no-store"), - (new HeaderDescriptor(KnownHeaders.CacheControl), "public, max-age=31536000"), - (new HeaderDescriptor(KnownHeaders.ContentEncoding), "br"), - (new HeaderDescriptor(KnownHeaders.ContentEncoding), "gzip"), - (new HeaderDescriptor(KnownHeaders.ContentType), "application/dns-message"), - (new HeaderDescriptor(KnownHeaders.ContentType), "application/javascript"), - (new HeaderDescriptor(KnownHeaders.ContentType), "application/json"), - (new HeaderDescriptor(KnownHeaders.ContentType), "application/x-www-form-urlencoded"), - (new HeaderDescriptor(KnownHeaders.ContentType), "image/gif"), - (new HeaderDescriptor(KnownHeaders.ContentType), "image/jpeg"), - (new HeaderDescriptor(KnownHeaders.ContentType), "image/png"), - (new HeaderDescriptor(KnownHeaders.ContentType), "text/css"), - (new HeaderDescriptor(KnownHeaders.ContentType), "text/html; charset=utf-8"), // Whitespace is correct, see spec. - (new HeaderDescriptor(KnownHeaders.ContentType), "text/plain"), - (new HeaderDescriptor(KnownHeaders.ContentType), "text/plain;charset=utf-8"), // Whitespace is correct, see spec. - (new HeaderDescriptor(KnownHeaders.Range), "bytes=0-"), - (new HeaderDescriptor(KnownHeaders.StrictTransportSecurity), "max-age=31536000"), - (new HeaderDescriptor(KnownHeaders.StrictTransportSecurity), "max-age=31536000; includesubdomains"), - (new HeaderDescriptor(KnownHeaders.StrictTransportSecurity), "max-age=31536000; includesubdomains; preload"), - (new HeaderDescriptor(KnownHeaders.Vary), "accept-encoding"), - (new HeaderDescriptor(KnownHeaders.Vary), "origin"), - (new HeaderDescriptor(KnownHeaders.XContentTypeOptions), "nosniff"), - (new HeaderDescriptor("x-xss-protection"), "1; mode=block"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "100"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "204"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "206"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "302"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "400"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "403"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "421"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "425"), - (new HeaderDescriptor(KnownHeaders.PseudoStatus), "500"), - (new HeaderDescriptor(KnownHeaders.AcceptLanguage), ""), - (new HeaderDescriptor(KnownHeaders.AccessControlAllowCredentials), "FALSE"), - (new HeaderDescriptor(KnownHeaders.AccessControlAllowCredentials), "TRUE"), - (new HeaderDescriptor(KnownHeaders.AccessControlAllowHeaders), "*"), - (new HeaderDescriptor(KnownHeaders.AccessControlAllowMethods), "get"), - (new HeaderDescriptor(KnownHeaders.AccessControlAllowMethods), "get, post, options"), - (new HeaderDescriptor(KnownHeaders.AccessControlAllowMethods), "options"), - (new HeaderDescriptor(KnownHeaders.AccessControlExposeHeaders), "content-length"), - (new HeaderDescriptor("access-control-request-headers"), "content-type"), - (new HeaderDescriptor("access-control-request-method"), "get"), - (new HeaderDescriptor("access-control-request-method"), "post"), - (new HeaderDescriptor(KnownHeaders.AltSvc), "clear"), - (new HeaderDescriptor(KnownHeaders.Authorization), ""), - (new HeaderDescriptor(KnownHeaders.ContentSecurityPolicy), "script-src 'none'; object-src 'none'; base-uri 'none'"), - (new HeaderDescriptor("early-data"), "1"), - (new HeaderDescriptor("expect-ct"), ""), - (new HeaderDescriptor("forwarded"), ""), - (new HeaderDescriptor(KnownHeaders.IfRange), ""), - (new HeaderDescriptor(KnownHeaders.Origin), ""), - (new HeaderDescriptor("purpose"), "prefetch"), - (new HeaderDescriptor(KnownHeaders.Server), ""), - (new HeaderDescriptor("timing-allow-origin"), "*"), - (new HeaderDescriptor(KnownHeaders.UpgradeInsecureRequests), "1"), - (new HeaderDescriptor(KnownHeaders.UserAgent), ""), - (new HeaderDescriptor("x-forwarded-for"), ""), - (new HeaderDescriptor(KnownHeaders.XFrameOptions), "deny"), - (new HeaderDescriptor(KnownHeaders.XFrameOptions), "sameorigin") + (new HeaderDescriptor(":authority"), ""), // 0 + (new HeaderDescriptor(":path"), "/"), // 1 + (new HeaderDescriptor(KnownHeaders.Age), "0"), // 2 + (new HeaderDescriptor(KnownHeaders.ContentDisposition), ""), // 3 + (new HeaderDescriptor(KnownHeaders.ContentLength), "0"), // 4 + (new HeaderDescriptor(KnownHeaders.Cookie), ""), // 5 + (new HeaderDescriptor(KnownHeaders.Date), ""), // 6 + (new HeaderDescriptor(KnownHeaders.ETag), ""), // 7 + (new HeaderDescriptor(KnownHeaders.IfModifiedSince), ""), // 8 + (new HeaderDescriptor(KnownHeaders.IfNoneMatch), ""), // 9 + (new HeaderDescriptor(KnownHeaders.LastModified), ""), // 10 + (new HeaderDescriptor(KnownHeaders.Link), ""), // 11 + (new HeaderDescriptor(KnownHeaders.Location), ""), // 12 + (new HeaderDescriptor(KnownHeaders.Referer), ""), // 13 + (new HeaderDescriptor(KnownHeaders.SetCookie), ""), // 14 + (new HeaderDescriptor(":method"), "CONNECT"), // 15 + (new HeaderDescriptor(":method"), "DELETE"), // 16 + (new HeaderDescriptor(":method"), "GET"), // 17 + (new HeaderDescriptor(":method"), "HEAD"), // 18 + (new HeaderDescriptor(":method"), "OPTIONS"), // 19 + (new HeaderDescriptor(":method"), "POST"), // 20 + (new HeaderDescriptor(":method"), "PUT"), // 21 + (new HeaderDescriptor(":scheme"), "http"), // 22 + (new HeaderDescriptor(":scheme"), "https"), // 23 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "103"), // 24 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "200"), // 25 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "304"), // 26 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "404"), // 27 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "503"), // 28 + (new HeaderDescriptor(KnownHeaders.Accept), "*/*"), // 29 + (new HeaderDescriptor(KnownHeaders.Accept), "application/dns-message"), // 30 + (new HeaderDescriptor(KnownHeaders.AcceptEncoding), "gzip, deflate, br"), // 31 + (new HeaderDescriptor(KnownHeaders.AcceptRanges), "bytes"), // 32 + (new HeaderDescriptor(KnownHeaders.AccessControlAllowHeaders), "cache-control"), // 33 + (new HeaderDescriptor(KnownHeaders.AccessControlAllowHeaders), "content-type"), // 34 + (new HeaderDescriptor(KnownHeaders.AccessControlAllowOrigin), "*"), // 35 + (new HeaderDescriptor(KnownHeaders.CacheControl), "max-age=0"), // 36 + (new HeaderDescriptor(KnownHeaders.CacheControl), "max-age=2592000"), // 37 + (new HeaderDescriptor(KnownHeaders.CacheControl), "max-age=604800"), // 38 + (new HeaderDescriptor(KnownHeaders.CacheControl), "no-cache"), // 39 + (new HeaderDescriptor(KnownHeaders.CacheControl), "no-store"), // 40 + (new HeaderDescriptor(KnownHeaders.CacheControl), "public, max-age=31536000"), // 41 + (new HeaderDescriptor(KnownHeaders.ContentEncoding), "br"), // 42 + (new HeaderDescriptor(KnownHeaders.ContentEncoding), "gzip"), // 43 + (new HeaderDescriptor(KnownHeaders.ContentType), "application/dns-message"), // 44 + (new HeaderDescriptor(KnownHeaders.ContentType), "application/javascript"), // 45 + (new HeaderDescriptor(KnownHeaders.ContentType), "application/json"), // 46 + (new HeaderDescriptor(KnownHeaders.ContentType), "application/x-www-form-urlencoded"), // 47 + (new HeaderDescriptor(KnownHeaders.ContentType), "image/gif"), // 48 + (new HeaderDescriptor(KnownHeaders.ContentType), "image/jpeg"), // 49 + (new HeaderDescriptor(KnownHeaders.ContentType), "image/png"), // 50 + (new HeaderDescriptor(KnownHeaders.ContentType), "text/css"), // 51 + (new HeaderDescriptor(KnownHeaders.ContentType), "text/html; charset=utf-8"), // 52; Whitespace is correct, see spec. + (new HeaderDescriptor(KnownHeaders.ContentType), "text/plain"), // 53 + (new HeaderDescriptor(KnownHeaders.ContentType), "text/plain;charset=utf-8"), // 54; Whitespace is correct, see spec. + (new HeaderDescriptor(KnownHeaders.Range), "bytes=0-"), // 55 + (new HeaderDescriptor(KnownHeaders.StrictTransportSecurity), "max-age=31536000"), // 56 + (new HeaderDescriptor(KnownHeaders.StrictTransportSecurity), "max-age=31536000; includesubdomains"), // 57 + (new HeaderDescriptor(KnownHeaders.StrictTransportSecurity), "max-age=31536000; includesubdomains; preload"), // 58 + (new HeaderDescriptor(KnownHeaders.Vary), "accept-encoding"), // 59 + (new HeaderDescriptor(KnownHeaders.Vary), "origin"), // 60 + (new HeaderDescriptor(KnownHeaders.XContentTypeOptions), "nosniff"), // 61 + (new HeaderDescriptor("x-xss-protection"), "1; mode=block"), // 62 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "100"), // 63 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "204"), // 64 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "206"), // 65 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "302"), // 66 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "400"), // 67 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "403"), // 68 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "421"), // 69 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "425"), // 70 + (new HeaderDescriptor(KnownHeaders.PseudoStatus), "500"), // 71 + (new HeaderDescriptor(KnownHeaders.AcceptLanguage), ""), // 72 + (new HeaderDescriptor(KnownHeaders.AccessControlAllowCredentials), "FALSE"), // 73 + (new HeaderDescriptor(KnownHeaders.AccessControlAllowCredentials), "TRUE"), // 74 + (new HeaderDescriptor(KnownHeaders.AccessControlAllowHeaders), "*"), // 75 + (new HeaderDescriptor(KnownHeaders.AccessControlAllowMethods), "get"), // 76 + (new HeaderDescriptor(KnownHeaders.AccessControlAllowMethods), "get, post, options"), // 77 + (new HeaderDescriptor(KnownHeaders.AccessControlAllowMethods), "options"), // 78 + (new HeaderDescriptor(KnownHeaders.AccessControlExposeHeaders), "content-length"), // 79 + (new HeaderDescriptor("access-control-request-headers"), "content-type"), // 80 + (new HeaderDescriptor("access-control-request-method"), "get"), // 81 + (new HeaderDescriptor("access-control-request-method"), "post"), // 82 + (new HeaderDescriptor(KnownHeaders.AltSvc), "clear"), // 83 + (new HeaderDescriptor(KnownHeaders.Authorization), ""), // 84 + (new HeaderDescriptor(KnownHeaders.ContentSecurityPolicy), "script-src 'none'; object-src 'none'; base-uri 'none'"), // 85 + (new HeaderDescriptor("early-data"), "1"), // 86 + (new HeaderDescriptor("expect-ct"), ""), // 87 + (new HeaderDescriptor("forwarded"), ""), // 88 + (new HeaderDescriptor(KnownHeaders.IfRange), ""), // 89 + (new HeaderDescriptor(KnownHeaders.Origin), ""), // 90 + (new HeaderDescriptor("purpose"), "prefetch"), // 91 + (new HeaderDescriptor(KnownHeaders.Server), ""), // 92 + (new HeaderDescriptor("timing-allow-origin"), "*"), // 93 + (new HeaderDescriptor(KnownHeaders.UpgradeInsecureRequests), "1"), // 94 + (new HeaderDescriptor(KnownHeaders.UserAgent), ""), // 95 + (new HeaderDescriptor("x-forwarded-for"), ""), // 96 + (new HeaderDescriptor(KnownHeaders.XFrameOptions), "deny"), // 97 + (new HeaderDescriptor(KnownHeaders.XFrameOptions), "sameorigin") // 98 }; } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs index bd359c3f992c08..4f963788ef6bf3 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs @@ -869,24 +869,43 @@ private void OnHeader(int? staticIndex, HeaderDescriptor descriptor, string? sta throw new Http3ConnectionException(Http3ErrorCode.ProtocolError); } - int statusCode = staticIndex switch + int statusCode; + if (staticValue != null) // Indexed Header Field -- both name and value are taken from the table { - H3StaticTable.Status103 => 103, - H3StaticTable.Status200 => 200, - H3StaticTable.Status304 => 304, - H3StaticTable.Status404 => 404, - H3StaticTable.Status503 => 503, - H3StaticTable.Status100 => 100, - H3StaticTable.Status204 => 204, - H3StaticTable.Status206 => 206, - H3StaticTable.Status302 => 302, - H3StaticTable.Status400 => 400, - H3StaticTable.Status403 => 403, - H3StaticTable.Status421 => 421, - H3StaticTable.Status425 => 425, - H3StaticTable.Status500 => 500, - _ => HttpConnectionBase.ParseStatusCode(literalValue), - }; + statusCode = staticIndex switch + { + H3StaticTable.Status103 => 103, + H3StaticTable.Status200 => 200, + H3StaticTable.Status304 => 304, + H3StaticTable.Status404 => 404, + H3StaticTable.Status503 => 503, + H3StaticTable.Status100 => 100, + H3StaticTable.Status204 => 204, + H3StaticTable.Status206 => 206, + H3StaticTable.Status302 => 302, + H3StaticTable.Status400 => 400, + H3StaticTable.Status403 => 403, + H3StaticTable.Status421 => 421, + H3StaticTable.Status425 => 425, + H3StaticTable.Status500 => 500, + // We should never get here, at least while we only use static table. But we can still parse staticValue. + _ => ParseStatusCode(staticIndex, staticValue) + }; + + int ParseStatusCode(int? index, string value) + { + string message = $"Unexpected QPACK table reference for Status code: index={index} value=\'{value}\'"; + Debug.Fail(message); + if (NetEventSource.Log.IsEnabled()) Trace(message); + + // TODO: The parsing is not optimal, but I don't expect this line to be executed at all for now. + return HttpConnectionBase.ParseStatusCode(Encoding.ASCII.GetBytes(value)); + } + } + else // Literal Header Field With Name Reference -- only name is taken from the table + { + statusCode = HttpConnectionBase.ParseStatusCode(literalValue); + } _response = new HttpResponseMessage() { diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs index 0e1274d502f5f1..5f0ee5a9055fca 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs @@ -802,6 +802,59 @@ private SslApplicationProtocol ExtractMsQuicNegotiatedAlpn(Http3LoopbackConnecti return (SslApplicationProtocol)alpn; } + [ConditionalTheory(nameof(IsMsQuicSupported))] + [MemberData(nameof(StatusCodesTestData))] + public async Task StatusCodes_ReceiveSuccess(HttpStatusCode statusCode, bool qpackEncode) + { + using Http3LoopbackServer server = CreateHttp3LoopbackServer(); + + Http3LoopbackConnection connection = null; + Task serverTask = Task.Run(async () => + { + connection = (Http3LoopbackConnection)await server.EstablishGenericConnectionAsync(); + using Http3LoopbackStream stream = await connection.AcceptRequestStreamAsync(); + + HttpRequestData request = await stream.ReadRequestDataAsync().ConfigureAwait(false); + + if (qpackEncode) + { + await stream.SendResponseHeadersWithEncodedStatusAsync(statusCode).ConfigureAwait(false); + } + else + { + await stream.SendResponseHeadersAsync(statusCode).ConfigureAwait(false); + } + }); + + using HttpClient client = CreateHttpClient(); + using HttpRequestMessage request = new() + { + Method = HttpMethod.Get, + RequestUri = server.Address, + Version = HttpVersion30, + VersionPolicy = HttpVersionPolicy.RequestVersionExact + }; + HttpResponseMessage response = await client.SendAsync(request).WaitAsync(TimeSpan.FromSeconds(10)); + + Assert.Equal(statusCode, response.StatusCode); + + await serverTask; + Assert.NotNull(connection); + connection.Dispose(); + } + + public static TheoryData StatusCodesTestData() + { + var statuses = Enum.GetValues(typeof(HttpStatusCode)).Cast().Where(s => s >= HttpStatusCode.OK); // exclude informational + var data = new TheoryData(); + foreach (var status in statuses) + { + data.Add(status, true); + data.Add(status, false); + } + return data; + } + /// /// These are public interop test servers for various QUIC and HTTP/3 implementations, /// taken from https://github.com/quicwg/base-drafts/wiki/Implementations and https://bagder.github.io/HTTP3-test/.