Skip to content

Commit 4656f15

Browse files
authored
HTTP/3: Write static header names (#38565)
1 parent 15b9f5d commit 4656f15

18 files changed

+224
-43
lines changed

src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ public ValueTask<FlushResult> WriteResponseTrailersAsync(long streamId, HttpResp
311311

312312
_outgoingFrame.PrepareHeaders();
313313
var buffer = _headerEncodingBuffer.GetSpan(HeaderBufferSize);
314-
var done = QPackHeaderWriter.BeginEncode(_headersEnumerator, buffer, ref _headersTotalSize, out var payloadLength);
314+
var done = QPackHeaderWriter.BeginEncodeHeaders(_headersEnumerator, buffer, ref _headersTotalSize, out var payloadLength);
315315
FinishWritingHeaders(payloadLength, done);
316316
}
317317
// Any exception from the QPack encoder can leave the dynamic table in a corrupt state.
@@ -366,7 +366,7 @@ internal void WriteResponseHeaders(int statusCode, HttpResponseHeaders headers)
366366

367367
_outgoingFrame.PrepareHeaders();
368368
var buffer = _headerEncodingBuffer.GetSpan(HeaderBufferSize);
369-
var done = QPackHeaderWriter.BeginEncode(statusCode, _headersEnumerator, buffer, ref _headersTotalSize, out var payloadLength);
369+
var done = QPackHeaderWriter.BeginEncodeHeaders(statusCode, _headersEnumerator, buffer, ref _headersTotalSize, out var payloadLength);
370370
FinishWritingHeaders(payloadLength, done);
371371
}
372372
// Any exception from the QPack encoder can leave the dynamic table in a corrupt state.

src/Servers/Kestrel/Core/src/Internal/Http3/Http3HeadersEnumerator.cs

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections;
66
using System.Collections.Generic;
7+
using System.Net.Http.QPack;
78
using System.Text;
89
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
910
using Microsoft.Extensions.Primitives;
@@ -147,7 +148,89 @@ public void Dispose()
147148

148149
internal static int GetResponseHeaderStaticTableId(KnownHeaderType responseHeaderType)
149150
{
150-
// Not Implemented
151-
return -1;
151+
// Not every header in the QPACK static table is known.
152+
// These are missing from this test and the full header name is written.
153+
// Missing:
154+
// - link
155+
// - location
156+
// - strict-transport-security
157+
// - x-content-type-options
158+
// - x-xss-protection
159+
// - content-security-policy
160+
// - early-data
161+
// - expect-ct
162+
// - purpose
163+
// - timing-allow-origin
164+
// - x-forwarded-for
165+
// - x-frame-options
166+
switch (responseHeaderType)
167+
{
168+
case KnownHeaderType.Age:
169+
return H3StaticTable.Age0;
170+
case KnownHeaderType.ContentLength:
171+
return H3StaticTable.ContentLength0;
172+
case KnownHeaderType.Date:
173+
return H3StaticTable.Date;
174+
case KnownHeaderType.Cookie:
175+
return H3StaticTable.Cookie;
176+
case KnownHeaderType.ETag:
177+
return H3StaticTable.ETag;
178+
case KnownHeaderType.IfModifiedSince:
179+
return H3StaticTable.IfModifiedSince;
180+
case KnownHeaderType.IfNoneMatch:
181+
return H3StaticTable.IfNoneMatch;
182+
case KnownHeaderType.LastModified:
183+
return H3StaticTable.LastModified;
184+
case KnownHeaderType.Location:
185+
return H3StaticTable.Location;
186+
case KnownHeaderType.Referer:
187+
return H3StaticTable.Referer;
188+
case KnownHeaderType.SetCookie:
189+
return H3StaticTable.SetCookie;
190+
case KnownHeaderType.Method:
191+
return H3StaticTable.MethodConnect;
192+
case KnownHeaderType.Accept:
193+
return H3StaticTable.AcceptAny;
194+
case KnownHeaderType.AcceptEncoding:
195+
return H3StaticTable.AcceptEncodingGzipDeflateBr;
196+
case KnownHeaderType.AcceptRanges:
197+
return H3StaticTable.AcceptRangesBytes;
198+
case KnownHeaderType.AccessControlAllowHeaders:
199+
return H3StaticTable.AccessControlAllowHeadersCacheControl;
200+
case KnownHeaderType.AccessControlAllowOrigin:
201+
return H3StaticTable.AccessControlAllowOriginAny;
202+
case KnownHeaderType.CacheControl:
203+
return H3StaticTable.CacheControlMaxAge0;
204+
case KnownHeaderType.ContentEncoding:
205+
return H3StaticTable.ContentEncodingBr;
206+
case KnownHeaderType.ContentType:
207+
return H3StaticTable.ContentTypeApplicationDnsMessage;
208+
case KnownHeaderType.Range:
209+
return H3StaticTable.RangeBytes0ToAll;
210+
case KnownHeaderType.Vary:
211+
return H3StaticTable.VaryAcceptEncoding;
212+
case KnownHeaderType.AcceptLanguage:
213+
return H3StaticTable.AcceptLanguage;
214+
case KnownHeaderType.AccessControlAllowCredentials:
215+
return H3StaticTable.AccessControlAllowCredentials;
216+
case KnownHeaderType.AccessControlAllowMethods:
217+
return H3StaticTable.AccessControlAllowMethodsGet;
218+
case KnownHeaderType.AltSvc:
219+
return H3StaticTable.AltSvcClear;
220+
case KnownHeaderType.Authorization:
221+
return H3StaticTable.Authorization;
222+
case KnownHeaderType.IfRange:
223+
return H3StaticTable.IfRange;
224+
case KnownHeaderType.Origin:
225+
return H3StaticTable.Origin;
226+
case KnownHeaderType.Server:
227+
return H3StaticTable.Server;
228+
case KnownHeaderType.UpgradeInsecureRequests:
229+
return H3StaticTable.UpgradeInsecureRequests1;
230+
case KnownHeaderType.UserAgent:
231+
return H3StaticTable.UserAgent;
232+
default:
233+
return -1;
234+
}
152235
}
153236
}

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ private async Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext>
640640

641641
try
642642
{
643-
QPackDecoder.Decode(payload, handler: this);
643+
QPackDecoder.Decode(payload, endHeaders: true, handler: this);
644644
QPackDecoder.Reset();
645645
}
646646
catch (QPackDecodingException ex)

src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
using System;
55
using System.Diagnostics;
66
using System.Net.Http.QPack;
7+
using System.Text;
78

89
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
910

1011
internal static class QPackHeaderWriter
1112
{
12-
public static bool BeginEncode(Http3HeadersEnumerator enumerator, Span<byte> buffer, ref int totalHeaderSize, out int length)
13+
public static bool BeginEncodeHeaders(Http3HeadersEnumerator enumerator, Span<byte> buffer, ref int totalHeaderSize, out int length)
1314
{
1415
bool hasValue = enumerator.MoveNext();
1516
Debug.Assert(hasValue == true);
@@ -24,40 +25,47 @@ public static bool BeginEncode(Http3HeadersEnumerator enumerator, Span<byte> buf
2425
return doneEncode;
2526
}
2627

27-
public static bool BeginEncode(int statusCode, Http3HeadersEnumerator enumerator, Span<byte> buffer, ref int totalHeaderSize, out int length)
28+
public static bool BeginEncodeHeaders(int statusCode, Http3HeadersEnumerator headersEnumerator, Span<byte> buffer, ref int totalHeaderSize, out int length)
2829
{
29-
bool hasValue = enumerator.MoveNext();
30-
Debug.Assert(hasValue == true);
30+
length = 0;
3131

3232
// https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#header-prefix
3333
buffer[0] = 0;
3434
buffer[1] = 0;
3535

3636
int statusCodeLength = EncodeStatusCode(statusCode, buffer.Slice(2));
3737
totalHeaderSize += 42; // name (:status) + value (xxx) + overhead (32)
38+
length += statusCodeLength + 2;
3839

39-
bool done = Encode(enumerator, buffer.Slice(statusCodeLength + 2), throwIfNoneEncoded: false, ref totalHeaderSize, out int headersLength);
40-
length = statusCodeLength + headersLength + 2;
40+
if (!headersEnumerator.MoveNext())
41+
{
42+
return true;
43+
}
44+
45+
bool done = Encode(headersEnumerator, buffer.Slice(statusCodeLength + 2), throwIfNoneEncoded: false, ref totalHeaderSize, out int headersLength);
46+
length += headersLength;
4147

4248
return done;
4349
}
4450

45-
public static bool Encode(Http3HeadersEnumerator enumerator, Span<byte> buffer, ref int totalHeaderSize, out int length)
51+
public static bool Encode(Http3HeadersEnumerator headersEnumerator, Span<byte> buffer, ref int totalHeaderSize, out int length)
4652
{
47-
return Encode(enumerator, buffer, throwIfNoneEncoded: true, ref totalHeaderSize, out length);
53+
return Encode(headersEnumerator, buffer, throwIfNoneEncoded: true, ref totalHeaderSize, out length);
4854
}
4955

50-
private static bool Encode(Http3HeadersEnumerator enumerator, Span<byte> buffer, bool throwIfNoneEncoded, ref int totalHeaderSize, out int length)
56+
private static bool Encode(Http3HeadersEnumerator headersEnumerator, Span<byte> buffer, bool throwIfNoneEncoded, ref int totalHeaderSize, out int length)
5157
{
5258
length = 0;
5359

5460
do
5561
{
56-
var current = enumerator.Current;
57-
var valueEncoding = ReferenceEquals(enumerator.EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector)
58-
? null : enumerator.EncodingSelector(current.Key);
62+
var staticTableId = headersEnumerator.QPackStaticTableId;
63+
var name = headersEnumerator.Current.Key;
64+
var value = headersEnumerator.Current.Value;
65+
var valueEncoding = ReferenceEquals(headersEnumerator.EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector)
66+
? null : headersEnumerator.EncodingSelector(name);
5967

60-
if (!QPackEncoder.EncodeLiteralHeaderFieldWithoutNameReference(current.Key, current.Value, valueEncoding, buffer.Slice(length), out int headerLength))
68+
if (!EncodeHeader(buffer.Slice(length), staticTableId, name, value, valueEncoding, out var headerLength))
6169
{
6270
if (length == 0 && throwIfNoneEncoded)
6371
{
@@ -67,13 +75,20 @@ private static bool Encode(Http3HeadersEnumerator enumerator, Span<byte> buffer,
6775
}
6876

6977
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.3
70-
totalHeaderSize += HeaderField.GetLength(current.Key.Length, current.Value.Length);
78+
totalHeaderSize += HeaderField.GetLength(name.Length, value.Length);
7179
length += headerLength;
72-
} while (enumerator.MoveNext());
80+
} while (headersEnumerator.MoveNext());
7381

7482
return true;
7583
}
7684

85+
private static bool EncodeHeader(Span<byte> buffer, int staticTableId, string name, string value, Encoding? valueEncoding, out int headerLength)
86+
{
87+
return staticTableId == -1
88+
? QPackEncoder.EncodeLiteralHeaderFieldWithoutNameReference(name, value, valueEncoding, buffer, out headerLength)
89+
: QPackEncoder.EncodeLiteralHeaderFieldWithStaticNameReference(staticTableId, value, valueEncoding, buffer, out headerLength);
90+
}
91+
7792
private static int EncodeStatusCode(int statusCode, Span<byte> buffer)
7893
{
7994
switch (statusCode)

src/Servers/Kestrel/Core/test/Http3HeadersEnumeratorTests.cs renamed to src/Servers/Kestrel/Core/test/Http3/Http3HeadersEnumeratorTests.cs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,17 @@ public void CanIterateOverResponseHeaders()
3838

3939
Assert.Equal(new[]
4040
{
41-
CreateHeaderResult(-1, "Date", "Date!"),
42-
CreateHeaderResult(-1, "Accept-Ranges", "AcceptRanges!"),
43-
CreateHeaderResult(-1, "Age", "1"),
44-
CreateHeaderResult(-1, "Age", "2"),
45-
CreateHeaderResult(-1, "Grpc-Encoding", "Identity!"),
46-
CreateHeaderResult(-1, "Content-Length", "9"),
47-
CreateHeaderResult(-1, "Name1", "Value1"),
48-
CreateHeaderResult(-1, "Name2", "Value2-1"),
49-
CreateHeaderResult(-1, "Name2", "Value2-2"),
50-
CreateHeaderResult(-1, "Name3", "Value3"),
51-
}, headers);
41+
CreateHeaderResult(6, "Date", "Date!"),
42+
CreateHeaderResult(32, "Accept-Ranges", "AcceptRanges!"),
43+
CreateHeaderResult(2, "Age", "1"),
44+
CreateHeaderResult(2, "Age", "2"),
45+
CreateHeaderResult(-1, "Grpc-Encoding", "Identity!"),
46+
CreateHeaderResult(4, "Content-Length", "9"),
47+
CreateHeaderResult(-1, "Name1", "Value1"),
48+
CreateHeaderResult(-1, "Name2", "Value2-1"),
49+
CreateHeaderResult(-1, "Name2", "Value2-2"),
50+
CreateHeaderResult(-1, "Name3", "Value3"),
51+
}, headers);
5252
}
5353

5454
[Fact]
@@ -71,12 +71,12 @@ public void CanIterateOverResponseTrailers()
7171

7272
Assert.Equal(new[]
7373
{
74-
CreateHeaderResult(-1, "ETag", "ETag!"),
75-
CreateHeaderResult(-1, "Name1", "Value1"),
76-
CreateHeaderResult(-1, "Name2", "Value2-1"),
77-
CreateHeaderResult(-1, "Name2", "Value2-2"),
78-
CreateHeaderResult(-1, "Name3", "Value3"),
79-
}, headers);
74+
CreateHeaderResult(7, "ETag", "ETag!"),
75+
CreateHeaderResult(-1, "Name1", "Value1"),
76+
CreateHeaderResult(-1, "Name2", "Value2-1"),
77+
CreateHeaderResult(-1, "Name2", "Value2-2"),
78+
CreateHeaderResult(-1, "Name3", "Value3"),
79+
}, headers);
8080
}
8181

8282
[Fact]
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
6+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
7+
8+
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests;
9+
10+
public class Http3QPackEncoderTests
11+
{
12+
[Fact]
13+
public void BeginEncodeHeaders_StatusWithoutIndexedValue_WriteIndexNameAndFullValue()
14+
{
15+
Span<byte> buffer = new byte[1024 * 16];
16+
17+
var totalHeaderSize = 0;
18+
var headers = new HttpResponseHeaders();
19+
var enumerator = new Http3HeadersEnumerator();
20+
enumerator.Initialize(headers);
21+
22+
Assert.True(QPackHeaderWriter.BeginEncodeHeaders(302, enumerator, buffer, ref totalHeaderSize, out var length));
23+
24+
var result = buffer.Slice(0, length).ToArray();
25+
var hex = BitConverter.ToString(result);
26+
Assert.Equal("00-00-5F-30-03-33-30-32", hex);
27+
}
28+
29+
[Fact]
30+
public void BeginEncodeHeaders_StatusWithIndexedValue_WriteIndex()
31+
{
32+
Span<byte> buffer = new byte[1024 * 16];
33+
34+
var totalHeaderSize = 0;
35+
var headers = new HttpResponseHeaders();
36+
var enumerator = new Http3HeadersEnumerator();
37+
enumerator.Initialize(headers);
38+
39+
Assert.True(QPackHeaderWriter.BeginEncodeHeaders(200, enumerator, buffer, ref totalHeaderSize, out var length));
40+
41+
var result = buffer.Slice(0, length).ToArray();
42+
var hex = BitConverter.ToString(result);
43+
Assert.Equal("00-00-D9", hex);
44+
}
45+
46+
[Fact]
47+
public void BeginEncodeHeaders_NonStaticKey_WriteFullNameAndFullValue()
48+
{
49+
Span<byte> buffer = new byte[1024 * 16];
50+
51+
var headers = (IHeaderDictionary)new HttpResponseHeaders();
52+
headers.Translate = "private";
53+
54+
var totalHeaderSize = 0;
55+
var enumerator = new Http3HeadersEnumerator();
56+
enumerator.Initialize(headers);
57+
58+
Assert.True(QPackHeaderWriter.BeginEncodeHeaders(302, enumerator, buffer, ref totalHeaderSize, out var length));
59+
60+
var result = buffer.Slice(8, length - 8).ToArray();
61+
var hex = BitConverter.ToString(result);
62+
Assert.Equal("37-02-74-72-61-6E-73-6C-61-74-65-07-70-72-69-76-61-74-65", hex);
63+
}
64+
65+
[Fact]
66+
public void BeginEncodeHeaders_NoStatus_NonStaticKey_WriteFullNameAndFullValue()
67+
{
68+
Span<byte> buffer = new byte[1024 * 16];
69+
70+
var headers = (IHeaderDictionary)new HttpResponseHeaders();
71+
headers.Translate = "private";
72+
73+
var totalHeaderSize = 0;
74+
var enumerator = new Http3HeadersEnumerator();
75+
enumerator.Initialize(headers);
76+
77+
Assert.True(QPackHeaderWriter.BeginEncodeHeaders(enumerator, buffer, ref totalHeaderSize, out var length));
78+
79+
var result = buffer.Slice(2, length - 2).ToArray();
80+
var hex = BitConverter.ToString(result);
81+
Assert.Equal("37-02-74-72-61-6E-73-6C-61-74-65-07-70-72-69-76-61-74-65", hex);
82+
}
83+
}

src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ public async Task SendHeadersAsync(Http3HeadersEnumerator headers, bool endStrea
637637
var headersTotalSize = 0;
638638

639639
var buffer = _headerHandler.HeaderEncodingBuffer.AsMemory();
640-
var done = QPackHeaderWriter.BeginEncode(headers, buffer.Span, ref headersTotalSize, out var length);
640+
var done = QPackHeaderWriter.BeginEncodeHeaders(headers, buffer.Span, ref headersTotalSize, out var length);
641641
if (!done)
642642
{
643643
throw new InvalidOperationException("Headers not sent.");
@@ -676,7 +676,7 @@ internal async ValueTask<Dictionary<string, string>> ExpectHeadersAsync(bool exp
676676
Http3InMemory.AssertFrameType(http3WithPayload.Type, Http3FrameType.Headers);
677677

678678
_headerHandler.DecodedHeaders.Clear();
679-
_headerHandler.QpackDecoder.Decode(http3WithPayload.PayloadSequence, this);
679+
_headerHandler.QpackDecoder.Decode(http3WithPayload.PayloadSequence, endHeaders: true, this);
680680
_headerHandler.QpackDecoder.Reset();
681681
return _headerHandler.DecodedHeaders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, _headerHandler.DecodedHeaders.Comparer);
682682
}
@@ -693,7 +693,7 @@ internal async ValueTask<Dictionary<string, string>> ExpectTrailersAsync()
693693
Http3InMemory.AssertFrameType(http3WithPayload.Type, Http3FrameType.Headers);
694694

695695
_headerHandler.DecodedHeaders.Clear();
696-
_headerHandler.QpackDecoder.Decode(http3WithPayload.PayloadSequence, this);
696+
_headerHandler.QpackDecoder.Decode(http3WithPayload.PayloadSequence, endHeaders: true, this);
697697
_headerHandler.QpackDecoder.Reset();
698698
return _headerHandler.DecodedHeaders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, _headerHandler.DecodedHeaders.Comparer);
699699
}

0 commit comments

Comments
 (0)