Skip to content
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

Implement Http/2 WebSockets #41558

Merged
merged 22 commits into from
Jun 8, 2022
Merged
Show file tree
Hide file tree
Changes from 18 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
41 changes: 0 additions & 41 deletions src/Hosting/TestHost/src/RequestFeature.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/Http/Headers/src/HeaderNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ 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>
/// <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>
Expand Down
27 changes: 27 additions & 0 deletions src/Http/Http.Features/src/IHttpConnectFeature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Http.Features;

/// <summary>
/// Used with protocols that require the Extended Connect handshake such as HTTP/2 WebSockets and WebTransport.
/// https://www.rfc-editor.org/rfc/rfc8441#section-4
/// </summary>
public interface IHttpConnectFeature
{
/// <summary>
/// Indicates if the current request is a CONNECT request that can be transitioned to an opaque connection via AcceptAsync.
/// </summary>
bool IsConnectRequest { get; }

/// <summary>
/// The ':protocol' header included in the request.
/// </summary>
string? Protocol { get; }

/// <summary>
/// Send the response headers with a 200 status code and transition to opaque streaming.
/// </summary>
/// <returns>An opaque bidirectional stream.</returns>
ValueTask<Stream> AcceptAsync();
}
4 changes: 4 additions & 0 deletions src/Http/Http.Features/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#nullable enable
Microsoft.AspNetCore.Http.Features.IHttpConnectFeature
Microsoft.AspNetCore.Http.Features.IHttpConnectFeature.AcceptAsync() -> System.Threading.Tasks.ValueTask<System.IO.Stream!>
Microsoft.AspNetCore.Http.Features.IHttpConnectFeature.IsConnectRequest.get -> bool
Microsoft.AspNetCore.Http.Features.IHttpConnectFeature.Protocol.get -> string?
3 changes: 3 additions & 0 deletions src/Http/Http/src/Features/HttpRequestFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public HttpRequestFeature()
/// <inheritdoc />
public string Protocol { get; set; }

/// <inheritdoc />
public string? ConnectProtocol { get; set; }

/// <inheritdoc />
public string Scheme { get; set; }

Expand Down
2 changes: 2 additions & 0 deletions src/Http/Http/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable
*REMOVED*Microsoft.AspNetCore.Http.StreamResponseBodyFeature.StreamResponseBodyFeature(System.IO.Stream! stream, Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature! priorFeature) -> void
Microsoft.AspNetCore.Http.Features.HttpRequestFeature.ConnectProtocol.get -> string?
Microsoft.AspNetCore.Http.Features.HttpRequestFeature.ConnectProtocol.set -> void
Microsoft.AspNetCore.Http.StreamResponseBodyFeature.StreamResponseBodyFeature(System.IO.Stream! stream, Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature? priorFeature) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"EchoApp": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:5000",
"launchUrl": "https://localhost:5001",
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Expand Down
2 changes: 1 addition & 1 deletion src/Middleware/WebSockets/samples/EchoApp/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF
{
if (context.WebSockets.IsWebSocketRequest)
{
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
var webSocket = await context.WebSockets.AcceptWebSocketAsync(new WebSocketAcceptContext() { DangerousEnableCompression = true });
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
await Echo(context, webSocket, loggerFactory.CreateLogger("Echo"));
}
else
Expand Down
14 changes: 8 additions & 6 deletions src/Middleware/WebSockets/src/HandshakeHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ internal static class HandshakeHelpers
// This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static
private static ReadOnlySpan<byte> EncodedWebSocketKey => "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"u8;

// Verify Method, Upgrade, Connection, version, key, etc..
public static void GenerateResponseHeaders(string key, string? subProtocol, IHeaderDictionary headers)
public static void GenerateResponseHeaders(bool isHttp1, IHeaderDictionary requestHeaders, string? subProtocol, IHeaderDictionary responseHeaders)
{
headers.Connection = HeaderNames.Upgrade;
headers.Upgrade = Constants.Headers.UpgradeWebSocket;
headers.SecWebSocketAccept = CreateResponseKey(key);
if (isHttp1)
{
responseHeaders.Connection = HeaderNames.Upgrade;
responseHeaders.Upgrade = Constants.Headers.UpgradeWebSocket;
responseHeaders.SecWebSocketAccept = CreateResponseKey(requestHeaders.SecWebSocketKey.ToString());
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
}
if (!string.IsNullOrWhiteSpace(subProtocol))
{
headers.SecWebSocketProtocol = subProtocol;
responseHeaders.SecWebSocketProtocol = subProtocol;
}
}

Expand Down
94 changes: 66 additions & 28 deletions src/Middleware/WebSockets/src/WebSocketMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ public Task Invoke(HttpContext context)
{
// Detect if an opaque upgrade is available. If so, add a websocket upgrade.
var upgradeFeature = context.Features.Get<IHttpUpgradeFeature>();
if (upgradeFeature != null && context.Features.Get<IHttpWebSocketFeature>() == null)
var connectFeature = context.Features.Get<IHttpConnectFeature>();
if ((upgradeFeature != null || connectFeature != null) && context.Features.Get<IHttpWebSocketFeature>() == null)
{
var webSocketFeature = new UpgradeHandshake(context, upgradeFeature, _options, _logger);
var webSocketFeature = new WebSocketHandshake(context, upgradeFeature, connectFeature, _options, _logger);
context.Features.Set<IHttpWebSocketFeature>(webSocketFeature);

if (!_anyOriginAllowed)
{
// Check for Origin header
Expand All @@ -88,18 +88,21 @@ public Task Invoke(HttpContext context)
return _next(context);
}

private sealed class UpgradeHandshake : IHttpWebSocketFeature
private sealed class WebSocketHandshake : IHttpWebSocketFeature
{
private readonly HttpContext _context;
private readonly IHttpUpgradeFeature _upgradeFeature;
private readonly IHttpUpgradeFeature? _upgradeFeature;
private readonly IHttpConnectFeature? _connectFeature;
private readonly WebSocketOptions _options;
private readonly ILogger _logger;
private bool? _isWebSocketRequest;
private bool _isH2WebSocket;

public UpgradeHandshake(HttpContext context, IHttpUpgradeFeature upgradeFeature, WebSocketOptions options, ILogger logger)
public WebSocketHandshake(HttpContext context, IHttpUpgradeFeature? upgradeFeature, IHttpConnectFeature? connectFeature, WebSocketOptions options, ILogger logger)
{
_context = context;
_upgradeFeature = upgradeFeature;
_connectFeature = connectFeature;
_options = options;
_logger = logger;
}
Expand All @@ -110,14 +113,19 @@ public bool IsWebSocketRequest
{
if (_isWebSocketRequest == null)
{
if (!_upgradeFeature.IsUpgradableRequest)
if (_connectFeature?.IsConnectRequest == true)
{
_isWebSocketRequest = false;
_isH2WebSocket = CheckSupportedWebSocketRequestH2(_context.Request.Method, _connectFeature!.Protocol, _context.Request.Headers);
_isWebSocketRequest = _isH2WebSocket;
}
else
else if (_upgradeFeature?.IsUpgradableRequest == true)
{
_isWebSocketRequest = CheckSupportedWebSocketRequest(_context.Request.Method, _context.Request.Headers);
}
else
{
_isWebSocketRequest = false;
}
}
return _isWebSocketRequest.Value;
}
Expand All @@ -127,7 +135,7 @@ public async Task<WebSocket> AcceptAsync(WebSocketAcceptContext acceptContext)
{
if (!IsWebSocketRequest)
{
throw new InvalidOperationException("Not a WebSocket request."); // TODO: LOC
throw new InvalidOperationException("Not a WebSocket request.");
}

string? subProtocol = null;
Expand All @@ -154,8 +162,7 @@ public async Task<WebSocket> AcceptAsync(WebSocketAcceptContext acceptContext)
}
}

var key = _context.Request.Headers.SecWebSocketKey.ToString();
HandshakeHelpers.GenerateResponseHeaders(key, subProtocol, _context.Response.Headers);
HandshakeHelpers.GenerateResponseHeaders(!_isH2WebSocket, _context.Request.Headers, subProtocol, _context.Response.Headers);

WebSocketDeflateOptions? deflateOptions = null;
if (enableCompression)
Expand Down Expand Up @@ -187,7 +194,18 @@ public async Task<WebSocket> AcceptAsync(WebSocketAcceptContext acceptContext)
}
}

Stream opaqueTransport = await _upgradeFeature.UpgradeAsync(); // Sets status code to 101
Stream opaqueTransport;
// HTTP/2
if (_isH2WebSocket)
{
// Send the response headers
opaqueTransport = await _connectFeature!.AcceptAsync();
}
// HTTP/1.1
else
{
opaqueTransport = await _upgradeFeature!.UpgradeAsync(); // Sets status code to 101
}

return WebSocket.CreateFromStream(opaqueTransport, new WebSocketCreationOptions()
{
Expand All @@ -205,17 +223,22 @@ public static bool CheckSupportedWebSocketRequest(string method, IHeaderDictiona
return false;
}

if (!CheckWebSocketVersion(requestHeaders))
{
return false;
}

var foundHeader = false;

var values = requestHeaders.GetCommaSeparatedValues(HeaderNames.SecWebSocketVersion);
var values = requestHeaders.GetCommaSeparatedValues(HeaderNames.Upgrade);
foreach (var value in values)
{
if (string.Equals(value, Constants.Headers.SupportedVersion, StringComparison.OrdinalIgnoreCase))
if (string.Equals(value, Constants.Headers.UpgradeWebSocket, StringComparison.OrdinalIgnoreCase))
{
// WebSockets are long lived; so if the header values are valid we switch them out for the interned versions.
if (values.Length == 1)
{
requestHeaders.SecWebSocketVersion = Constants.Headers.SupportedVersion;
requestHeaders.Upgrade = Constants.Headers.UpgradeWebSocket;
}
foundHeader = true;
break;
Expand Down Expand Up @@ -245,28 +268,43 @@ public static bool CheckSupportedWebSocketRequest(string method, IHeaderDictiona
{
return false;
}
foundHeader = false;

values = requestHeaders.GetCommaSeparatedValues(HeaderNames.Upgrade);
return HandshakeHelpers.IsRequestKeyValid(requestHeaders.SecWebSocketKey.ToString());
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
}

// https://datatracker.ietf.org/doc/html/rfc8441
// :method = CONNECT
// :protocol = websocket
// :scheme = https
// :path = /chat
// :authority = server.example.com
// sec-websocket-protocol = chat, superchat
// sec-websocket-extensions = permessage-deflate
// sec-websocket-version = 13
// origin = http://www.example.com
public static bool CheckSupportedWebSocketRequestH2(string method, string? protocol, IHeaderDictionary requestHeaders)
{
return HttpMethods.IsConnect(method)
&& string.Equals(protocol, Constants.Headers.UpgradeWebSocket, StringComparison.OrdinalIgnoreCase)
&& CheckWebSocketVersion(requestHeaders);
}

public static bool CheckWebSocketVersion(IHeaderDictionary requestHeaders)
{
var values = requestHeaders.GetCommaSeparatedValues(HeaderNames.SecWebSocketVersion);
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
foreach (var value in values)
{
if (string.Equals(value, Constants.Headers.UpgradeWebSocket, StringComparison.OrdinalIgnoreCase))
if (string.Equals(value, Constants.Headers.SupportedVersion, StringComparison.OrdinalIgnoreCase))
{
// WebSockets are long lived; so if the header values are valid we switch them out for the interned versions.
if (values.Length == 1)
{
requestHeaders.Upgrade = Constants.Headers.UpgradeWebSocket;
requestHeaders.SecWebSocketVersion = Constants.Headers.SupportedVersion;
}
foundHeader = true;
break;
return true;
}
}
if (!foundHeader)
{
return false;
}

return HandshakeHelpers.IsRequestKeyValid(requestHeaders.SecWebSocketKey.ToString());
return false;
}
}

Expand Down
Loading