Fix QPACK status decode (#57236)
authorNatalia Kondratyeva <knatalia@microsoft.com>
Thu, 12 Aug 2021 15:05:58 +0000 (17:05 +0200)
committerGitHub <noreply@github.com>
Thu, 12 Aug 2021 15:05:58 +0000 (17:05 +0200)
Fix "Literal Header Field With Name Reference" parsing for Status code. Add tests for QPACK-ed and not QPACK-ed status receiving for all status codes (except informational).

Fix QPackStaticTable.HeaderLookup to comply with spec (the order was off, so lookup didn't work correctly in some cases).

Fixes #55988

src/libraries/Common/src/System/Net/Http/aspnetcore/Http3/QPack/H3StaticTable.Http3.cs
src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs
src/libraries/Common/tests/System/Net/Http/QPackTestEncoder.cs
src/libraries/System.Net.Http/src/System/Net/Http/Headers/QPackStaticTable.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs

index efa5f9b81d62e155dfcfc689cd7d0c3d4dc7a025..ebb9142752ef48b85e1debd96fe9d5ec194191a8 100644 (file)
@@ -50,102 +50,102 @@ namespace System.Net.Http.QPack
             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)
index a3d2c9a4215c2bafea68c3e48cb23a9222465da8..958cb2abc1e15678dba703687889ed4b4642a2e5 100644 (file)
@@ -78,10 +78,19 @@ namespace System.Net.Test.Common
             await SendFrameAsync(SettingsFrame, buffer.AsMemory(0, bytesWritten)).ConfigureAwait(false);
         }
 
-        private Memory<byte> ConstructHeadersPayload(IEnumerable<HttpHeaderData> headers)
+        private Memory<byte> ConstructHeadersPayload(HttpStatusCode statusCode, IEnumerable<HttpHeaderData> 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 @@ namespace System.Net.Test.Common
 
             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 @@ namespace System.Net.Test.Common
             return buffer.AsMemory(0, bytesWritten);
         }
 
-        private async Task SendHeadersFrameAsync(IEnumerable<HttpHeaderData> headers)
+        private async Task SendHeadersFrameAsync(HttpStatusCode statusCode, IEnumerable<HttpHeaderData> 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<HttpHeaderData> headers)
+        private async Task SendPartialHeadersFrameAsync(HttpStatusCode statusCode, IEnumerable<HttpHeaderData> headers)
         {
-            Memory<byte> payload = ConstructHeadersPayload(headers);
+            Memory<byte> payload = ConstructHeadersPayload(statusCode, headers);
 
             await SendFrameHeaderAsync(HeadersFrame, payload.Length);
 
@@ -234,28 +248,32 @@ namespace System.Net.Test.Common
             await SendResponseBodyAsync(Encoding.UTF8.GetBytes(content ?? ""), isFinal).ConfigureAwait(false);
         }
 
-        private IEnumerable<HttpHeaderData> PrepareHeaders(HttpStatusCode statusCode, IEnumerable<HttpHeaderData> headers)
+        private IEnumerable<HttpHeaderData> PrepareHeaders(IEnumerable<HttpHeaderData> headers)
         {
             headers ??= Enumerable.Empty<HttpHeaderData>();
 
             // 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<HttpHeaderData> 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<HttpHeaderData> headers = null)
+        {
+            headers = PrepareHeaders(headers);
+            await SendHeadersFrameAsync(statusCode, headers, qpackEncodeStatus: true).ConfigureAwait(false);
         }
 
         public async Task SendPartialResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IEnumerable<HttpHeaderData> 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)
index a73f42067bcb7b4af55eb618d9537d85121c1646..d1296bebd17bd71517ec728cb2a53703609aa746 100644 (file)
@@ -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 @@ namespace System.Net.Test.Common
         {
             return HPackEncoder.EncodeInteger(value, prefix, prefixMask, buffer);
         }
+
+        // from System.Net.Http.QPack.H3StaticTable
+        private static readonly Dictionary<int, int> s_statusIndex = new Dictionary<int, int>
+        {
+            [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<byte> 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]
index e2da7d0b6f2c0def38a9754c66d46d831e0e6b47..bc3d1992146962fc7144cba2189ecba9214309a8 100644 (file)
@@ -9,105 +9,105 @@ namespace System.Net.Http.Headers
         // 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
         };
     }
 }
index 7c3e4d4cbb9e8f60770fe9a059694a2642f5e401..115f2b93c7509182678377d3c0b4f0817f7ef3aa 100644 (file)
@@ -880,24 +880,43 @@ namespace System.Net.Http
                     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()
                 {
index ec8dad21da959de37230d1f11e2f58400f460c19..f9bd724a72d33bcdcc01a66614696ff9068f22ea 100644 (file)
@@ -852,6 +852,59 @@ namespace System.Net.Http.Functional.Tests
             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<HttpStatusCode, bool> StatusCodesTestData()
+        {
+            var statuses = Enum.GetValues(typeof(HttpStatusCode)).Cast<HttpStatusCode>().Where(s => s >= HttpStatusCode.OK); // exclude informational
+            var data = new TheoryData<HttpStatusCode, bool>();
+            foreach (var status in statuses)
+            {
+                data.Add(status, true);
+                data.Add(status, false);
+            }
+            return data;
+        }
+
         /// <summary>
         /// 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/.