Skip to content

Commit

Permalink
HTTP/3: Improve static table compression to include values (#38681)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Nov 30, 2021
1 parent 3b6ad60 commit 5adbdcb
Show file tree
Hide file tree
Showing 7 changed files with 449 additions and 218 deletions.
173 changes: 173 additions & 0 deletions src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,179 @@ internal enum KnownHeaderType
WWWAuthenticate,
}

internal static class HttpHeadersCompression
{
internal static (int index, bool matchedValue) MatchKnownHeaderQPack(KnownHeaderType knownHeader, string value)
{
switch (knownHeader)
{
case KnownHeaderType.Age:
switch (value)
{
case "0":
return (2, true);
default:
return (2, false);
}
case KnownHeaderType.ContentLength:
switch (value)
{
case "0":
return (4, true);
default:
return (4, false);
}
case KnownHeaderType.Date:
return (6, false);
case KnownHeaderType.ETag:
return (7, false);
case KnownHeaderType.LastModified:
return (10, false);
case KnownHeaderType.Location:
return (12, false);
case KnownHeaderType.SetCookie:
return (14, false);
case KnownHeaderType.AcceptRanges:
switch (value)
{
case "bytes":
return (32, true);
default:
return (32, false);
}
case KnownHeaderType.AccessControlAllowHeaders:
switch (value)
{
case "cache-control":
return (33, true);
case "content-type":
return (34, true);
case "*":
return (75, true);
default:
return (33, false);
}
case KnownHeaderType.AccessControlAllowOrigin:
switch (value)
{
case "*":
return (35, true);
default:
return (35, false);
}
case KnownHeaderType.CacheControl:
switch (value)
{
case "max-age=0":
return (36, true);
case "max-age=2592000":
return (37, true);
case "max-age=604800":
return (38, true);
case "no-cache":
return (39, true);
case "no-store":
return (40, true);
case "public, max-age=31536000":
return (41, true);
default:
return (36, false);
}
case KnownHeaderType.ContentEncoding:
switch (value)
{
case "br":
return (42, true);
case "gzip":
return (43, true);
default:
return (42, false);
}
case KnownHeaderType.ContentType:
switch (value)
{
case "application/dns-message":
return (44, true);
case "application/javascript":
return (45, true);
case "application/json":
return (46, true);
case "application/x-www-form-urlencoded":
return (47, true);
case "image/gif":
return (48, true);
case "image/jpeg":
return (49, true);
case "image/png":
return (50, true);
case "text/css":
return (51, true);
case "text/html; charset=utf-8":
return (52, true);
case "text/plain":
return (53, true);
case "text/plain;charset=utf-8":
return (54, true);
default:
return (44, false);
}
case KnownHeaderType.Vary:
switch (value)
{
case "accept-encoding":
return (59, true);
case "origin":
return (60, true);
default:
return (59, false);
}
case KnownHeaderType.AccessControlAllowCredentials:
switch (value)
{
case "FALSE":
return (73, true);
case "TRUE":
return (74, true);
default:
return (73, false);
}
case KnownHeaderType.AccessControlAllowMethods:
switch (value)
{
case "get":
return (76, true);
case "get, post, options":
return (77, true);
case "options":
return (78, true);
default:
return (76, false);
}
case KnownHeaderType.AccessControlExposeHeaders:
switch (value)
{
case "content-length":
return (79, true);
default:
return (79, false);
}
case KnownHeaderType.AltSvc:
switch (value)
{
case "clear":
return (83, true);
default:
return (83, false);
}
case KnownHeaderType.Server:
return (92, false);

default:
return (-1, false);
}
}
}

internal partial class HttpHeaders
{
private readonly static HashSet<string> _internedHeaderNames = new HashSet<string>(96, StringComparer.OrdinalIgnoreCase)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ private enum HeadersType : byte

public Func<string, Encoding?> EncodingSelector { get; set; } = KestrelServerOptions.DefaultHeaderEncodingSelector;

public int QPackStaticTableId => GetResponseHeaderStaticTableId(_knownHeaderType);
public (int index, bool matchedValue) GetQPackStaticTableId() => HttpHeadersCompression.MatchKnownHeaderQPack(_knownHeaderType, Current.Value);
public KeyValuePair<string, string> Current { get; private set; }
object IEnumerator.Current => Current;

Expand Down Expand Up @@ -145,66 +145,4 @@ public void Reset()
public void Dispose()
{
}

internal static int GetResponseHeaderStaticTableId(KnownHeaderType responseHeaderType)
{
// Removed from this test are request-only headers, e.g. cookie.
//
// Not every header in the QPACK static table is known.
// These are missing from this test and the full header name is written.
// Missing:
// - link
// - location
// - strict-transport-security
// - x-content-type-options
// - x-xss-protection
// - content-security-policy
// - early-data
// - expect-ct
// - purpose
// - timing-allow-origin
// - x-forwarded-for
// - x-frame-options
switch (responseHeaderType)
{
case KnownHeaderType.Age:
return H3StaticTable.Age0;
case KnownHeaderType.ContentLength:
return H3StaticTable.ContentLength0;
case KnownHeaderType.Date:
return H3StaticTable.Date;
case KnownHeaderType.ETag:
return H3StaticTable.ETag;
case KnownHeaderType.LastModified:
return H3StaticTable.LastModified;
case KnownHeaderType.Location:
return H3StaticTable.Location;
case KnownHeaderType.SetCookie:
return H3StaticTable.SetCookie;
case KnownHeaderType.AcceptRanges:
return H3StaticTable.AcceptRangesBytes;
case KnownHeaderType.AccessControlAllowHeaders:
return H3StaticTable.AccessControlAllowHeadersCacheControl;
case KnownHeaderType.AccessControlAllowOrigin:
return H3StaticTable.AccessControlAllowOriginAny;
case KnownHeaderType.CacheControl:
return H3StaticTable.CacheControlMaxAge0;
case KnownHeaderType.ContentEncoding:
return H3StaticTable.ContentEncodingBr;
case KnownHeaderType.ContentType:
return H3StaticTable.ContentTypeApplicationDnsMessage;
case KnownHeaderType.Vary:
return H3StaticTable.VaryAcceptEncoding;
case KnownHeaderType.AccessControlAllowCredentials:
return H3StaticTable.AccessControlAllowCredentials;
case KnownHeaderType.AccessControlAllowMethods:
return H3StaticTable.AccessControlAllowMethodsGet;
case KnownHeaderType.AltSvc:
return H3StaticTable.AltSvcClear;
case KnownHeaderType.Server:
return H3StaticTable.Server;
default:
return -1;
}
}
}
34 changes: 27 additions & 7 deletions src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,39 @@ private static bool Encode(Http3HeadersEnumerator headersEnumerator, Span<byte>

do
{
var staticTableId = headersEnumerator.QPackStaticTableId;
// Match the current header to the QPACK static table. Possible outcomes:
// 1. Known header and value. Write index.
// 2. Known header with custom value. Write name index and full value.
// 3. Unknown header. Write full name and value.
var (staticTableId, matchedValue) = headersEnumerator.GetQPackStaticTableId();
var name = headersEnumerator.Current.Key;
var value = headersEnumerator.Current.Value;
var valueEncoding = ReferenceEquals(headersEnumerator.EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector)
? null : headersEnumerator.EncodingSelector(name);

if (!EncodeHeader(buffer.Slice(length), staticTableId, name, value, valueEncoding, out var headerLength))
int headerLength;
if (matchedValue)
{
if (length == 0 && throwIfNoneEncoded)
if (!QPackEncoder.EncodeStaticIndexedHeaderField(staticTableId, buffer.Slice(length), out headerLength))
{
throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */);
if (length == 0 && throwIfNoneEncoded)
{
throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */);
}
return false;
}
}
else
{
var valueEncoding = ReferenceEquals(headersEnumerator.EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector)
? null : headersEnumerator.EncodingSelector(name);

if (!EncodeHeader(buffer.Slice(length), staticTableId, name, value, valueEncoding, out headerLength))
{
if (length == 0 && throwIfNoneEncoded)
{
throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */);
}
return false;
}
return false;
}

// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.3
Expand Down
23 changes: 15 additions & 8 deletions src/Servers/Kestrel/Core/test/Http3/Http3HeadersEnumeratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,20 @@ public void Initialize_ChangeHeadersSource_EnumeratorUsesNewSource()
Assert.True(e.MoveNext());
Assert.Equal("Name1", e.Current.Key);
Assert.Equal("Value1", e.Current.Value);
Assert.Equal(-1, e.QPackStaticTableId);
var (index, matchedValue) = e.GetQPackStaticTableId();
Assert.Equal(-1, index);

Assert.True(e.MoveNext());
Assert.Equal("Name2", e.Current.Key);
Assert.Equal("Value2-1", e.Current.Value);
Assert.Equal(-1, e.QPackStaticTableId);
(index, matchedValue) = e.GetQPackStaticTableId();
Assert.Equal(-1, index);

Assert.True(e.MoveNext());
Assert.Equal("Name2", e.Current.Key);
Assert.Equal("Value2-2", e.Current.Value);
Assert.Equal(-1, e.QPackStaticTableId);
(index, matchedValue) = e.GetQPackStaticTableId();
Assert.Equal(-1, index);

var responseTrailers = (IHeaderDictionary)new HttpResponseTrailers();

Expand All @@ -118,22 +121,26 @@ public void Initialize_ChangeHeadersSource_EnumeratorUsesNewSource()
Assert.True(e.MoveNext());
Assert.Equal("Grpc-Status", e.Current.Key);
Assert.Equal("1", e.Current.Value);
Assert.Equal(-1, e.QPackStaticTableId);
(index, matchedValue) = e.GetQPackStaticTableId();
Assert.Equal(-1, index);

Assert.True(e.MoveNext());
Assert.Equal("Name1", e.Current.Key);
Assert.Equal("Value1", e.Current.Value);
Assert.Equal(-1, e.QPackStaticTableId);
(index, matchedValue) = e.GetQPackStaticTableId();
Assert.Equal(-1, index);

Assert.True(e.MoveNext());
Assert.Equal("Name2", e.Current.Key);
Assert.Equal("Value2-1", e.Current.Value);
Assert.Equal(-1, e.QPackStaticTableId);
(index, matchedValue) = e.GetQPackStaticTableId();
Assert.Equal(-1, index);

Assert.True(e.MoveNext());
Assert.Equal("Name2", e.Current.Key);
Assert.Equal("Value2-2", e.Current.Value);
Assert.Equal(-1, e.QPackStaticTableId);
(index, matchedValue) = e.GetQPackStaticTableId();
Assert.Equal(-1, index);

Assert.False(e.MoveNext());
}
Expand All @@ -143,7 +150,7 @@ public void Initialize_ChangeHeadersSource_EnumeratorUsesNewSource()
var headers = new List<(int HPackStaticTableId, string Name, string Value)>();
while (enumerator.MoveNext())
{
headers.Add(CreateHeaderResult(enumerator.QPackStaticTableId, enumerator.Current.Key, enumerator.Current.Value));
headers.Add(CreateHeaderResult(enumerator.GetQPackStaticTableId().index, enumerator.Current.Key, enumerator.Current.Value));
}
return headers.ToArray();
}
Expand Down
Loading

0 comments on commit 5adbdcb

Please sign in to comment.