Skip to content

HTTP/3: Write static header names #38565

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions src/Hosting/Hosting/src/Internal/WebHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,14 @@ public WebHost(
_hostingServiceProvider = hostingServiceProvider;
_applicationServiceCollection.AddSingleton<ApplicationLifetime>();
// There's no way to to register multiple service types per definition. See https://github.com/aspnet/DependencyInjection/issues/360
#pragma warning disable CS8634 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'class' constraint.
_applicationServiceCollection.AddSingleton(services
=> services.GetService<ApplicationLifetime>() as IHostApplicationLifetime);
_applicationServiceCollection.AddSingleton<IHostApplicationLifetime>(services
=> services.GetService<ApplicationLifetime>()!);
#pragma warning disable CS0618 // Type or member is obsolete
_applicationServiceCollection.AddSingleton(services
=> services.GetService<ApplicationLifetime>() as AspNetCore.Hosting.IApplicationLifetime);
_applicationServiceCollection.AddSingleton(services
=> services.GetService<ApplicationLifetime>() as Extensions.Hosting.IApplicationLifetime);
_applicationServiceCollection.AddSingleton<AspNetCore.Hosting.IApplicationLifetime>(services
=> services.GetService<ApplicationLifetime>()!);
_applicationServiceCollection.AddSingleton<Extensions.Hosting.IApplicationLifetime>(services
=> services.GetService<ApplicationLifetime>()!);
#pragma warning restore CS0618 // Type or member is obsolete
#pragma warning restore CS8634 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'class' constraint.
_applicationServiceCollection.AddSingleton<HostedServiceExecutor>();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ public ValueTask<FlushResult> WriteResponseTrailersAsync(long streamId, HttpResp

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

_outgoingFrame.PrepareHeaders();
var buffer = _headerEncodingBuffer.GetSpan(HeaderBufferSize);
var done = QPackHeaderWriter.BeginEncode(statusCode, _headersEnumerator, buffer, ref _headersTotalSize, out var payloadLength);
var done = QPackHeaderWriter.BeginEncodeHeaders(statusCode, _headersEnumerator, buffer, ref _headersTotalSize, out var payloadLength);
FinishWritingHeaders(payloadLength, done);
}
// Any exception from the QPack encoder can leave the dynamic table in a corrupt state.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.Http.QPack;
using System.Text;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.Extensions.Primitives;
Expand Down Expand Up @@ -147,7 +148,89 @@ public void Dispose()

internal static int GetResponseHeaderStaticTableId(KnownHeaderType responseHeaderType)
{
// Not Implemented
return -1;
// 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.Cookie:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider adding the un-"known" response headers given the methodology described in the quickwg QPACK static table appendix.

I also think the following headers only make sense on the request: Cookie, Authorization, UserAgent, Referer, Origin, IfModifiedSince, IfNoneMatch, Method, Accept, AcceptEncoding, AcceptLanguage, Range, IfRange and UpgradeInsecureRequests

Do we think there would be much value in trying to use the static header values too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't add more known headers because there is a limit to how many we can have. Eventually, the bit flags will be too large for 64bits.

I will look at using the static table for values in a future PR. It will require greater changes.

Copy link
Member

@Tratcher Tratcher Nov 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think the following headers only make sense on the request: Cookie, Authorization, UserAgent, Referer, Origin, IfModifiedSince, IfNoneMatch, Method, Accept, AcceptEncoding, AcceptLanguage, Range, IfRange and UpgradeInsecureRequests

Yes, remove request headers from this list, if only to make it easier to reason about.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For HPACK, we have the complete list:

internal static int GetResponseHeaderStaticTableId(KnownHeaderType responseHeaderType)
{
switch (responseHeaderType)
{
case KnownHeaderType.CacheControl:
return H2StaticTable.CacheControl;
case KnownHeaderType.Date:
return H2StaticTable.Date;
case KnownHeaderType.TransferEncoding:
return H2StaticTable.TransferEncoding;
case KnownHeaderType.Via:
return H2StaticTable.Via;
case KnownHeaderType.Allow:
return H2StaticTable.Allow;
case KnownHeaderType.ContentType:
return H2StaticTable.ContentType;
case KnownHeaderType.ContentEncoding:
return H2StaticTable.ContentEncoding;
case KnownHeaderType.ContentLanguage:
return H2StaticTable.ContentLanguage;
case KnownHeaderType.ContentLocation:
return H2StaticTable.ContentLocation;
case KnownHeaderType.ContentRange:
return H2StaticTable.ContentRange;
case KnownHeaderType.Expires:
return H2StaticTable.Expires;
case KnownHeaderType.LastModified:
return H2StaticTable.LastModified;
case KnownHeaderType.AcceptRanges:
return H2StaticTable.AcceptRanges;
case KnownHeaderType.Age:
return H2StaticTable.Age;
case KnownHeaderType.ETag:
return H2StaticTable.ETag;
case KnownHeaderType.Location:
return H2StaticTable.Location;
case KnownHeaderType.ProxyAuthenticate:
return H2StaticTable.ProxyAuthenticate;
case KnownHeaderType.RetryAfter:
return H2StaticTable.RetryAfter;
case KnownHeaderType.Server:
return H2StaticTable.Server;
case KnownHeaderType.SetCookie:
return H2StaticTable.SetCookie;
case KnownHeaderType.Vary:
return H2StaticTable.Vary;
case KnownHeaderType.WWWAuthenticate:
return H2StaticTable.WwwAuthenticate;
case KnownHeaderType.AccessControlAllowOrigin:
return H2StaticTable.AccessControlAllowOrigin;
case KnownHeaderType.ContentLength:
return H2StaticTable.ContentLength;
default:
return -1;
}

Do you want to trim the HPACK list as well? Which items?

Or leave both HPACK and QPACK lists as is?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, ok, leave it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks to me that Http2HeadersEnumerator.GetResponseHeaderStaticTableId() only handles response headers. I don't see request headers like Cookie, Authorization or any of the rest in there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I'll make the change to remove them in another PR.

return H3StaticTable.Cookie;
case KnownHeaderType.ETag:
return H3StaticTable.ETag;
case KnownHeaderType.IfModifiedSince:
return H3StaticTable.IfModifiedSince;
case KnownHeaderType.IfNoneMatch:
return H3StaticTable.IfNoneMatch;
case KnownHeaderType.LastModified:
return H3StaticTable.LastModified;
case KnownHeaderType.Location:
return H3StaticTable.Location;
case KnownHeaderType.Referer:
return H3StaticTable.Referer;
case KnownHeaderType.SetCookie:
return H3StaticTable.SetCookie;
case KnownHeaderType.Method:
return H3StaticTable.MethodConnect;
case KnownHeaderType.Accept:
return H3StaticTable.AcceptAny;
case KnownHeaderType.AcceptEncoding:
return H3StaticTable.AcceptEncodingGzipDeflateBr;
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.Range:
return H3StaticTable.RangeBytes0ToAll;
case KnownHeaderType.Vary:
return H3StaticTable.VaryAcceptEncoding;
case KnownHeaderType.AcceptLanguage:
return H3StaticTable.AcceptLanguage;
case KnownHeaderType.AccessControlAllowCredentials:
return H3StaticTable.AccessControlAllowCredentials;
case KnownHeaderType.AccessControlAllowMethods:
return H3StaticTable.AccessControlAllowMethodsGet;
case KnownHeaderType.AltSvc:
return H3StaticTable.AltSvcClear;
case KnownHeaderType.Authorization:
return H3StaticTable.Authorization;
case KnownHeaderType.IfRange:
return H3StaticTable.IfRange;
case KnownHeaderType.Origin:
return H3StaticTable.Origin;
case KnownHeaderType.Server:
return H3StaticTable.Server;
case KnownHeaderType.UpgradeInsecureRequests:
return H3StaticTable.UpgradeInsecureRequests1;
case KnownHeaderType.UserAgent:
return H3StaticTable.UserAgent;
default:
return -1;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ private async Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext>

try
{
QPackDecoder.Decode(payload, handler: this);
QPackDecoder.Decode(payload, endHeaders: true, handler: this);
QPackDecoder.Reset();
}
catch (QPackDecodingException ex)
Expand Down
45 changes: 30 additions & 15 deletions src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
using System;
using System.Diagnostics;
using System.Net.Http.QPack;
using System.Text;

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

internal static class QPackHeaderWriter
{
public static bool BeginEncode(Http3HeadersEnumerator enumerator, Span<byte> buffer, ref int totalHeaderSize, out int length)
public static bool BeginEncodeHeaders(Http3HeadersEnumerator enumerator, Span<byte> buffer, ref int totalHeaderSize, out int length)
{
bool hasValue = enumerator.MoveNext();
Debug.Assert(hasValue == true);
Expand All @@ -24,40 +25,47 @@ public static bool BeginEncode(Http3HeadersEnumerator enumerator, Span<byte> buf
return doneEncode;
}

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

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

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

bool done = Encode(enumerator, buffer.Slice(statusCodeLength + 2), throwIfNoneEncoded: false, ref totalHeaderSize, out int headersLength);
length = statusCodeLength + headersLength + 2;
if (!headersEnumerator.MoveNext())
{
return true;
}

bool done = Encode(headersEnumerator, buffer.Slice(statusCodeLength + 2), throwIfNoneEncoded: false, ref totalHeaderSize, out int headersLength);
length += headersLength;

return done;
}

public static bool Encode(Http3HeadersEnumerator enumerator, Span<byte> buffer, ref int totalHeaderSize, out int length)
public static bool Encode(Http3HeadersEnumerator headersEnumerator, Span<byte> buffer, ref int totalHeaderSize, out int length)
{
return Encode(enumerator, buffer, throwIfNoneEncoded: true, ref totalHeaderSize, out length);
return Encode(headersEnumerator, buffer, throwIfNoneEncoded: true, ref totalHeaderSize, out length);
}

private static bool Encode(Http3HeadersEnumerator enumerator, Span<byte> buffer, bool throwIfNoneEncoded, ref int totalHeaderSize, out int length)
private static bool Encode(Http3HeadersEnumerator headersEnumerator, Span<byte> buffer, bool throwIfNoneEncoded, ref int totalHeaderSize, out int length)
{
length = 0;

do
{
var current = enumerator.Current;
var valueEncoding = ReferenceEquals(enumerator.EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector)
? null : enumerator.EncodingSelector(current.Key);
var staticTableId = headersEnumerator.QPackStaticTableId;
var name = headersEnumerator.Current.Key;
var value = headersEnumerator.Current.Value;
var valueEncoding = ReferenceEquals(headersEnumerator.EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector)
? null : headersEnumerator.EncodingSelector(name);

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

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

return true;
}

private static bool EncodeHeader(Span<byte> buffer, int staticTableId, string name, string value, Encoding? valueEncoding, out int headerLength)
{
return staticTableId == -1
? QPackEncoder.EncodeLiteralHeaderFieldWithoutNameReference(name, value, valueEncoding, buffer, out headerLength)
: QPackEncoder.EncodeLiteralHeaderFieldWithStaticNameReference(staticTableId, value, valueEncoding, buffer, out headerLength);
}

private static int EncodeStatusCode(int statusCode, Span<byte> buffer)
{
switch (statusCode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ public void CanIterateOverResponseHeaders()

Assert.Equal(new[]
{
CreateHeaderResult(-1, "Date", "Date!"),
CreateHeaderResult(-1, "Accept-Ranges", "AcceptRanges!"),
CreateHeaderResult(-1, "Age", "1"),
CreateHeaderResult(-1, "Age", "2"),
CreateHeaderResult(-1, "Grpc-Encoding", "Identity!"),
CreateHeaderResult(-1, "Content-Length", "9"),
CreateHeaderResult(-1, "Name1", "Value1"),
CreateHeaderResult(-1, "Name2", "Value2-1"),
CreateHeaderResult(-1, "Name2", "Value2-2"),
CreateHeaderResult(-1, "Name3", "Value3"),
}, headers);
CreateHeaderResult(6, "Date", "Date!"),
CreateHeaderResult(32, "Accept-Ranges", "AcceptRanges!"),
CreateHeaderResult(2, "Age", "1"),
CreateHeaderResult(2, "Age", "2"),
CreateHeaderResult(-1, "Grpc-Encoding", "Identity!"),
CreateHeaderResult(4, "Content-Length", "9"),
CreateHeaderResult(-1, "Name1", "Value1"),
CreateHeaderResult(-1, "Name2", "Value2-1"),
CreateHeaderResult(-1, "Name2", "Value2-2"),
CreateHeaderResult(-1, "Name3", "Value3"),
}, headers);
}

[Fact]
Expand All @@ -71,12 +71,12 @@ public void CanIterateOverResponseTrailers()

Assert.Equal(new[]
{
CreateHeaderResult(-1, "ETag", "ETag!"),
CreateHeaderResult(-1, "Name1", "Value1"),
CreateHeaderResult(-1, "Name2", "Value2-1"),
CreateHeaderResult(-1, "Name2", "Value2-2"),
CreateHeaderResult(-1, "Name3", "Value3"),
}, headers);
CreateHeaderResult(7, "ETag", "ETag!"),
CreateHeaderResult(-1, "Name1", "Value1"),
CreateHeaderResult(-1, "Name2", "Value2-1"),
CreateHeaderResult(-1, "Name2", "Value2-2"),
CreateHeaderResult(-1, "Name3", "Value3"),
}, headers);
}

[Fact]
Expand Down
83 changes: 83 additions & 0 deletions src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests;

public class Http3QPackEncoderTests
{
[Fact]
public void BeginEncodeHeaders_StatusWithoutIndexedValue_WriteIndexNameAndFullValue()
{
Span<byte> buffer = new byte[1024 * 16];

var totalHeaderSize = 0;
var headers = new HttpResponseHeaders();
var enumerator = new Http3HeadersEnumerator();
enumerator.Initialize(headers);

Assert.True(QPackHeaderWriter.BeginEncodeHeaders(302, enumerator, buffer, ref totalHeaderSize, out var length));

var result = buffer.Slice(0, length).ToArray();
var hex = BitConverter.ToString(result);
Assert.Equal("00-00-5F-30-03-33-30-32", hex);
}

[Fact]
public void BeginEncodeHeaders_StatusWithIndexedValue_WriteIndex()
{
Span<byte> buffer = new byte[1024 * 16];

var totalHeaderSize = 0;
var headers = new HttpResponseHeaders();
var enumerator = new Http3HeadersEnumerator();
enumerator.Initialize(headers);

Assert.True(QPackHeaderWriter.BeginEncodeHeaders(200, enumerator, buffer, ref totalHeaderSize, out var length));

var result = buffer.Slice(0, length).ToArray();
var hex = BitConverter.ToString(result);
Assert.Equal("00-00-D9", hex);
}

[Fact]
public void BeginEncodeHeaders_NonStaticKey_WriteFullNameAndFullValue()
{
Span<byte> buffer = new byte[1024 * 16];

var headers = (IHeaderDictionary)new HttpResponseHeaders();
headers.Translate = "private";

var totalHeaderSize = 0;
var enumerator = new Http3HeadersEnumerator();
enumerator.Initialize(headers);

Assert.True(QPackHeaderWriter.BeginEncodeHeaders(302, enumerator, buffer, ref totalHeaderSize, out var length));

var result = buffer.Slice(8, length - 8).ToArray();
var hex = BitConverter.ToString(result);
Assert.Equal("37-02-74-72-61-6E-73-6C-61-74-65-07-70-72-69-76-61-74-65", hex);
}

[Fact]
public void BeginEncodeHeaders_NoStatus_NonStaticKey_WriteFullNameAndFullValue()
{
Span<byte> buffer = new byte[1024 * 16];

var headers = (IHeaderDictionary)new HttpResponseHeaders();
headers.Translate = "private";

var totalHeaderSize = 0;
var enumerator = new Http3HeadersEnumerator();
enumerator.Initialize(headers);

Assert.True(QPackHeaderWriter.BeginEncodeHeaders(enumerator, buffer, ref totalHeaderSize, out var length));

var result = buffer.Slice(2, length - 2).ToArray();
var hex = BitConverter.ToString(result);
Assert.Equal("37-02-74-72-61-6E-73-6C-61-74-65-07-70-72-69-76-61-74-65", hex);
}
}
Loading