Adding WebTransport Handshake to Kestrel (#41877)

This commit is contained in:
Daniel Genkin 2022-06-03 12:28:43 -07:00 коммит произвёл GitHub
Родитель 69cec6c7ee
Коммит d96a100bdd
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 1260 добавлений и 827 удалений

Просмотреть файл

@ -189,6 +189,9 @@ public static class HeaderNames
/// <summary>Gets the <c>Pragma</c> HTTP header name.</summary>
public static readonly string Pragma = "Pragma";
/// <summary>Gets the <c>Protocol</c> HTTP header name.</summary>
public static readonly string Protocol = ":protocol";
/// <summary>Gets the <c>Proxy-Authenticate</c> HTTP header name.</summary>
public static readonly string ProxyAuthenticate = "Proxy-Authenticate";

Просмотреть файл

@ -1 +1,2 @@
#nullable enable
static readonly Microsoft.Net.Http.Headers.HeaderNames.Protocol -> string!

Просмотреть файл

@ -668,4 +668,13 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="Http3ControlStreamErrorInitializingOutbound" xml:space="preserve">
<value>Error initializing outbound control stream.</value>
</data>
</root>
<data name="Http3DatagramStatusMismatch" xml:space="preserve">
<value>HTTP/3 datagrams negotiation mismatch. Currently client has it '{clientStatus}' and server has it '{serverStatus}'</value>
</data>
<data name="Http3MethodMustBeConnectWhenUsingProtocolPseudoHeader" xml:space="preserve">
<value>Method must be CONNECT when using the :protocol pseudo-header.</value>
</data>
<data name="Http3MissingAuthorityOrPathPseudoHeaders" xml:space="preserve">
<value>The :authority and/or :path pseudo-headers are missing.</value>
</data>
</root>

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -56,6 +56,10 @@ internal sealed class Http3Connection : IHttp3StreamLifetimeHandler, IRequestPro
_serverSettings.HeaderTableSize = (uint)httpLimits.Http3.HeaderTableSize;
_serverSettings.MaxRequestHeaderFieldSectionSize = (uint)httpLimits.MaxRequestHeadersTotalSize;
_serverSettings.EnableWebTransport = Convert.ToUInt32(context.ServiceContext.ServerOptions.EnableWebTransportAndH3Datagrams);
// technically these are 2 different settings so they should have separate values but the Chromium implementation requires
// them to both be 1 to useWebTransport.
_serverSettings.H3Datagram = Convert.ToUInt32(context.ServiceContext.ServerOptions.EnableWebTransportAndH3Datagrams);
}
private void UpdateHighestOpenedRequestStreamId(long streamId)
@ -656,6 +660,12 @@ internal sealed class Http3Connection : IHttp3StreamLifetimeHandler, IRequestPro
break;
case Http3SettingType.QPackBlockedStreams:
break;
case Http3SettingType.EnableWebTransport:
_clientSettings.EnableWebTransport = (uint)value;
break;
case Http3SettingType.H3Datagram:
_clientSettings.H3Datagram = (uint)value;
break;
default:
throw new InvalidOperationException("Unexpected setting: " + type);
}

Просмотреть файл

@ -331,6 +331,8 @@ internal abstract class Http3ControlStream : IHttp3Stream, IThreadPoolWorkItem
case (long)Http3SettingType.QPackMaxTableCapacity:
case (long)Http3SettingType.MaxFieldSectionSize:
case (long)Http3SettingType.QPackBlockedStreams:
case (long)Http3SettingType.EnableWebTransport:
case (long)Http3SettingType.H3Datagram:
_context.StreamLifetimeHandler.OnInboundControlStreamSetting((Http3SettingType)id, value);
break;
default:

Просмотреть файл

@ -8,9 +8,13 @@ internal sealed class Http3PeerSettings
// Note these are protocol defaults, not Kestrel defaults.
public const uint DefaultHeaderTableSize = 0;
public const uint DefaultMaxRequestHeaderFieldSize = uint.MaxValue;
public const uint DefaultEnableWebTransport = 0;
public const uint DefaultH3Datagram = 0;
public uint HeaderTableSize { get; internal set; } = DefaultHeaderTableSize;
public uint MaxRequestHeaderFieldSectionSize { get; internal set; } = DefaultMaxRequestHeaderFieldSize;
public uint EnableWebTransport { get; internal set; } = DefaultEnableWebTransport;
public uint H3Datagram { get; internal set; } = DefaultH3Datagram;
// Gets the settings that are different from the protocol defaults (as opposed to the server defaults).
internal List<Http3PeerSetting> GetNonProtocolDefaults()
@ -29,6 +33,16 @@ internal sealed class Http3PeerSettings
list.Add(new Http3PeerSetting(Http3SettingType.MaxFieldSectionSize, MaxRequestHeaderFieldSectionSize));
}
if (EnableWebTransport != DefaultEnableWebTransport)
{
list.Add(new Http3PeerSetting(Http3SettingType.EnableWebTransport, EnableWebTransport));
}
if (H3Datagram != DefaultH3Datagram)
{
list.Add(new Http3PeerSetting(Http3SettingType.H3Datagram, H3Datagram));
}
return list;
}
}

Просмотреть файл

@ -13,5 +13,18 @@ internal enum Http3SettingType : long
/// </summary>
MaxFieldSectionSize = 0x6,
// https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-5
QPackBlockedStreams = 0x7
QPackBlockedStreams = 0x7,
/// <summary>
/// SETTINGS_ENABLE_WEBTRANSPORT, default is 0 (off)
/// https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-01.html#name-http-3-settings-parameter-r
/// </summary>
EnableWebTransport = 0x2b603742,
/// <summary>
/// H3_DATAGRAM, default is 0 (off)
/// indicates that the server suppprts sending individual datagrams over Http/3
/// rather than just streams.
/// </summary>
H3Datagram = 0xffd277
}

Просмотреть файл

@ -29,6 +29,7 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
private static ReadOnlySpan<byte> AuthorityBytes => ":authority"u8;
private static ReadOnlySpan<byte> MethodBytes => ":method"u8;
private static ReadOnlySpan<byte> PathBytes => ":path"u8;
private static ReadOnlySpan<byte> ProtocolBytes => ":protocol"u8;
private static ReadOnlySpan<byte> SchemeBytes => ":scheme"u8;
private static ReadOnlySpan<byte> StatusBytes => ":status"u8;
private static ReadOnlySpan<byte> ConnectionBytes => "connection"u8;
@ -507,6 +508,10 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
{
return PseudoHeaderFields.Authority;
}
else if (name.SequenceEqual(ProtocolBytes))
{
return PseudoHeaderFields.Protocol;
}
else
{
return PseudoHeaderFields.Unknown;
@ -821,7 +826,25 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
await OnEndStreamReceived();
}
if (!_isMethodConnect && (_parsedPseudoHeaderFields & _mandatoryRequestPseudoHeaderFields) != _mandatoryRequestPseudoHeaderFields)
// https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3/#section-3.3
if (_context.ServiceContext.ServerOptions.EnableWebTransportAndH3Datagrams && HttpRequestHeaders.HeaderProtocol.Count > 0)
{
if (!_isMethodConnect)
{
throw new Http3StreamErrorException(CoreStrings.Http3MethodMustBeConnectWhenUsingProtocolPseudoHeader, Http3ErrorCode.ProtocolError);
}
if (!_parsedPseudoHeaderFields.HasFlag(PseudoHeaderFields.Authority) || !_parsedPseudoHeaderFields.HasFlag(PseudoHeaderFields.Path))
{
throw new Http3StreamErrorException(CoreStrings.Http3MissingAuthorityOrPathPseudoHeaders, Http3ErrorCode.ProtocolError);
}
if (_context.ClientPeerSettings.H3Datagram != _context.ServerPeerSettings.H3Datagram)
{
throw new Http3StreamErrorException(CoreStrings.FormatHttp3DatagramStatusMismatch(_context.ClientPeerSettings.H3Datagram == 1, _context.ServerPeerSettings.H3Datagram == 1), Http3ErrorCode.SettingsError);
}
}
else if (!_isMethodConnect && (_parsedPseudoHeaderFields & _mandatoryRequestPseudoHeaderFields) != _mandatoryRequestPseudoHeaderFields)
{
// All HTTP/3 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header
// fields, unless it is a CONNECT request. An HTTP request that omits mandatory pseudo-header
@ -928,8 +951,8 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
return false;
}
// CONNECT - :scheme and :path must be excluded
if (Method == Http.HttpMethod.Connect)
// CONNECT - :scheme and :path must be excluded=
if (Method == Http.HttpMethod.Connect && HttpRequestHeaders.HeaderProtocol.Count == 0)
{
if (!string.IsNullOrEmpty(RequestHeaders[HeaderNames.Scheme]) || !string.IsNullOrEmpty(RequestHeaders[HeaderNames.Path]))
{
@ -1157,6 +1180,7 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
Path = 0x4,
Scheme = 0x8,
Status = 0x10,
Protocol = 0x20,
Unknown = 0x40000000
}

Просмотреть файл

@ -157,6 +157,24 @@ public class KestrelServerOptions
/// </summary>
internal bool IsDevCertLoaded { get; set; }
/// <summary>
/// Internal AppContext switch to toggle the WebTransport and HTTP/3 datagrams experiemental features.
/// </summary>
private bool? _enableWebTransportAndH3Datagrams;
internal bool EnableWebTransportAndH3Datagrams
{
get
{
if (!_enableWebTransportAndH3Datagrams.HasValue)
{
_enableWebTransportAndH3Datagrams = AppContext.TryGetSwitch("Microsoft.AspNetCore.Server.Kestrel.Experimental.WebTransportAndH3Datagrams", out var enabled) && enabled;
}
return _enableWebTransportAndH3Datagrams.Value;
}
set => _enableWebTransportAndH3Datagrams = value;
}
/// <summary>
/// Specifies a configuration Action to run for each newly created endpoint. Calling this again will replace
/// the prior action.

Просмотреть файл

@ -16,4 +16,9 @@
<Reference Include="Microsoft.Extensions.Hosting" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Quic" />
</ItemGroup>
<ItemGroup>
<!-- Turn on the WebTransport AppContext switch -->
<RuntimeHostConfigurationOption Include="Microsoft.AspNetCore.Server.Kestrel.Experimental.WebTransportAndH3Datagrams" Value="true" />
</ItemGroup>
</Project>

Просмотреть файл

@ -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.Net;
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Internal;
@ -22,25 +24,18 @@ public class Program
})
.ConfigureWebHost(webHost =>
{
var cert = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, false);
webHost.UseKestrel()
.ConfigureKestrel((context, options) =>
{
var cert = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, false);
options.ConfigureHttpsDefaults(httpsOptions =>
{
httpsOptions.ServerCertificate = cert;
// httpsOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
// httpsOptions.AllowAnyClientCertificate();
});
options.ListenAnyIP(5000, listenOptions =>
{
listenOptions.UseConnectionLogging();
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
});
options.ListenAnyIP(5001, listenOptions =>
options.Listen(IPAddress.Any, 5001, listenOptions =>
{
listenOptions.UseHttps();
listenOptions.UseConnectionLogging();
@ -49,8 +44,8 @@ public class Program
options.ListenAnyIP(5002, listenOptions =>
{
listenOptions.UseHttps(StoreName.My, "localhost");
listenOptions.UseConnectionLogging();
listenOptions.UseHttps(StoreName.My, "localhost");
listenOptions.Protocols = HttpProtocols.Http3;
});
@ -108,6 +103,14 @@ public class Program
listenOptions.UseConnectionLogging();
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
});
// Port configured for WebTransport
options.Listen(IPAddress.Any, 5007, listenOptions =>
{
listenOptions.UseHttps(GenerateManualCertificate());
listenOptions.UseConnectionLogging();
listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
});
})
.UseStartup<Startup>();
});
@ -119,4 +122,54 @@ public class Program
host.Run();
}
// Adapted from: https://github.com/wegylexy/webtransport
// We will need to eventually merge this with existing Kestrel certificate generation
// tracked in issue #41762
private static X509Certificate2 GenerateManualCertificate()
{
X509Certificate2 cert = null;
var store = new X509Store("KestrelWebTransportCertificates", StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
if (store.Certificates.Count > 0)
{
cert = store.Certificates[^1];
// rotate key after it expires
if (DateTime.Parse(cert.GetExpirationDateString(), null) < DateTimeOffset.UtcNow)
{
cert = null;
}
}
if (cert == null)
{
// generate a new cert
var now = DateTimeOffset.UtcNow;
SubjectAlternativeNameBuilder sanBuilder = new();
sanBuilder.AddDnsName("localhost");
using var ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);
CertificateRequest req = new("CN=localhost", ec, HashAlgorithmName.SHA256);
// Adds purpose
req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection
{
new("1.3.6.1.5.5.7.3.1") // serverAuth
}, false));
// Adds usage
req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));
// Adds subject alternate names
req.CertificateExtensions.Add(sanBuilder.Build());
// Sign
using var crt = req.CreateSelfSigned(now, now.AddDays(14)); // 14 days is the max duration of a certificate for this
cert = new(crt.Export(X509ContentType.Pfx));
// Save
store.Add(cert);
}
store.Close();
var hash = SHA256.HashData(cert.RawData);
var certStr = Convert.ToBase64String(hash);
Console.WriteLine($"\n\n\n\n\nCertificate: {certStr}\n\n\n\n"); // <-- you will need to put this output into the JS API call to allo wthe connection
return cert;
}
}

Просмотреть файл

@ -27,6 +27,7 @@ public class KnownHeaders
HeaderNames.Connection,
HeaderNames.Scheme,
HeaderNames.Path,
HeaderNames.Protocol,
HeaderNames.Method,
HeaderNames.Authority,
HeaderNames.Host,
@ -45,7 +46,8 @@ public class KnownHeaders
"Method", // :method
"Path", // :path
"Scheme", // :scheme
"Status" // :status
"Status", // :status
"Protocol" // :protocol
};
public static readonly string[] NonApiHeaders =
@ -132,6 +134,7 @@ public class KnownHeaders
HeaderNames.IfRange,
HeaderNames.IfUnmodifiedSince,
HeaderNames.MaxForwards,
HeaderNames.Protocol,
HeaderNames.ProxyAuthorization,
HeaderNames.Referer,
HeaderNames.Range,

Просмотреть файл

@ -0,0 +1,138 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Net;
using System.Net.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
using Microsoft.AspNetCore.Testing;
using Microsoft.Net.Http.Headers;
using Http3SettingType = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3SettingType;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests;
public class WebTransportTests : Http3TestBase
{
[Fact]
public async Task WebTransportHandshake_ClientToServerPasses()
{
_serviceContext.ServerOptions.EnableWebTransportAndH3Datagrams = true;
await Http3Api.InitializeConnectionAsync(_noopApplication);
var controlStream = await Http3Api.CreateControlStream();
var controlStream2 = await Http3Api.GetInboundControlStream();
var settings = new Http3PeerSettings()
{
EnableWebTransport = 1,
H3Datagram = 1,
};
await controlStream.SendSettingsAsync(settings.GetNonProtocolDefaults());
var response1 = await controlStream2.ExpectSettingsAsync();
await Http3Api.ServerReceivedSettingsReader.ReadAsync().DefaultTimeout();
Assert.Equal(1, response1[(long)Http3SettingType.EnableWebTransport]);
var requestStream = await Http3Api.CreateRequestStream();
var headersConnectFrame = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, "CONNECT"),
new KeyValuePair<string, string>(HeaderNames.Protocol, "webtransport"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Authority, "server.example.com"),
new KeyValuePair<string, string>(HeaderNames.Origin, "server.example.com")
};
await requestStream.SendHeadersAsync(headersConnectFrame);
var response2 = await requestStream.ExpectHeadersAsync();
Assert.Equal((int)HttpStatusCode.OK, Convert.ToInt32(response2[HeaderNames.Status], null));
await requestStream.OnDisposedTask.DefaultTimeout();
}
[Theory]
[InlineData(
((long)Http3ErrorCode.ProtocolError),
nameof(CoreStrings.Http3MethodMustBeConnectWhenUsingProtocolPseudoHeader),
nameof(HeaderNames.Method), "GET", // incorrect method (verifies that webtransport doesn't break regular Http/3 get)
nameof(HeaderNames.Protocol), "webtransport",
nameof(HeaderNames.Scheme), "http",
nameof(HeaderNames.Path), "/",
nameof(HeaderNames.Authority), "server.example.com",
nameof(HeaderNames.Origin), "server.example.com")]
[InlineData(
((long)Http3ErrorCode.ProtocolError),
nameof(CoreStrings.Http3MissingAuthorityOrPathPseudoHeaders),
nameof(HeaderNames.Method), "CONNECT",
nameof(HeaderNames.Protocol), "webtransport",
nameof(HeaderNames.Scheme), "http",
nameof(HeaderNames.Authority), "server.example.com",
nameof(HeaderNames.Origin), "server.example.com")] // no path
[InlineData(
((long)Http3ErrorCode.ProtocolError),
nameof(CoreStrings.Http3MissingAuthorityOrPathPseudoHeaders),
nameof(HeaderNames.Method), "CONNECT",
nameof(HeaderNames.Protocol), "webtransport",
nameof(HeaderNames.Scheme), "http",
nameof(HeaderNames.Path), "/",
nameof(HeaderNames.Origin), "server.example.com")] // no authority
public async Task WebTransportHandshake_IncorrectHeadersRejects(long error, string targetErrorMessage, params string[] headers) // todo replace the "" with CoreStrings.... then push (maybe also update the waitforstreamerror function) and resolve stephen's comment
{
_serviceContext.ServerOptions.EnableWebTransportAndH3Datagrams = true;
await Http3Api.InitializeConnectionAsync(_noopApplication);
var controlStream = await Http3Api.CreateControlStream();
var controlStream2 = await Http3Api.GetInboundControlStream();
var settings = new Http3PeerSettings()
{
EnableWebTransport = 1,
H3Datagram = 1,
};
await controlStream.SendSettingsAsync(settings.GetNonProtocolDefaults());
var response1 = await controlStream2.ExpectSettingsAsync();
await Http3Api.ServerReceivedSettingsReader.ReadAsync().DefaultTimeout();
Assert.Equal(1, response1[(long)Http3SettingType.EnableWebTransport]);
var requestStream = await Http3Api.CreateRequestStream();
var headersConnectFrame = new List<KeyValuePair<string, string>>();
for (var i = 0; i < headers.Length; i += 2)
{
headersConnectFrame.Add(new KeyValuePair<string, string>(GetHeaderFromName(headers[i]), headers[i + 1]));
}
await requestStream.SendHeadersAsync(headersConnectFrame);
await requestStream.WaitForStreamErrorAsync((Http3ErrorCode)error, AssertExpectedErrorMessages, GetCoreStringFromName(targetErrorMessage));
}
private static string GetCoreStringFromName(string headerName)
{
return headerName switch
{
nameof(CoreStrings.Http3MissingAuthorityOrPathPseudoHeaders) => CoreStrings.Http3MissingAuthorityOrPathPseudoHeaders,
nameof(CoreStrings.Http3MethodMustBeConnectWhenUsingProtocolPseudoHeader) => CoreStrings.Http3MethodMustBeConnectWhenUsingProtocolPseudoHeader,
_ => throw new Exception("Core string not mapped yet")
};
}
private static string GetHeaderFromName(string coreStringName)
{
return coreStringName switch
{
nameof(HeaderNames.Method) => HeaderNames.Method,
nameof(HeaderNames.Protocol) => HeaderNames.Protocol,
nameof(HeaderNames.Scheme) => HeaderNames.Scheme,
nameof(HeaderNames.Path) => HeaderNames.Path,
nameof(HeaderNames.Authority) => HeaderNames.Authority,
nameof(HeaderNames.Origin) => HeaderNames.Origin,
_ => throw new Exception("Header name not mapped yet")
};
}
}

Просмотреть файл

@ -33,6 +33,19 @@ namespace System.Net.Http
/// The maximum number of request streams that can be blocked waiting for QPack instructions. The default is 0.
/// https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-5
/// </summary>
QPackBlockedStreams = 0x7
QPackBlockedStreams = 0x7,
/// <summary>
/// SETTINGS_ENABLE_WEBTRANSPORT, default is 0 (off)
/// https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-01.html#name-http-3-settings-parameter-r
/// </summary>
EnableWebTransport = 0x2b603742,
/// <summary>
/// H3_DATAGRAM, default is 0 (off)
/// indicates that the server suppprts sending individual datagrams over Http/3
/// rather than just streams.
/// </summary>
H3Datagram = 0xffd277
}
}