Skip to content
Open
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
10 changes: 8 additions & 2 deletions Engine/Internal/Protocol/ClientHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO.Pipelines;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;
using GenHTTP.Api.Protocol;
using GenHTTP.Engine.Internal.Context;
using GenHTTP.Engine.Shared.Types;
Expand All @@ -28,7 +29,7 @@ internal sealed class ClientHandler(ClientContext context)

private static readonly TimeSpan KeepAliveTimeout = TimeSpan.FromSeconds(60);

private static readonly ReadOnlyMemory<byte> KeepAliveValue = "Keep-Alive"u8.ToArray();
private static readonly ReadOnlyMemory<byte> KeepAliveValue = "keep-alive"u8.ToArray();

private static readonly ParserLimits Limits = ParserLimits.Default;

Expand Down Expand Up @@ -203,7 +204,12 @@ internal async ValueTask<Connection> HandleRequestAsync(Request request)

var connectionHeader = header.Headers.GetEntry(KnownHeaders.Connection);

var keepAliveRequested = connectionHeader?.Bytes.Span.SequenceEqual(KeepAliveValue.Span) ?? (header.Protocol == HttpProtocol.Http11);
// Connection options are case-insensitive tokens (RFC 9110 7.6.1). Matching them exactly
// read a browser, which sends "keep-alive" in lower case, as asking to close - so every
// browser request got a new connection, and a new TLS handshake with it.
var keepAliveRequested = connectionHeader is { } connection
? Ascii.EqualsIgnoreCase(connection.Bytes.Span, KeepAliveValue.Span)
: header.Protocol == HttpProtocol.Http11;

var response = await context.Server.Handler.HandleAsync(request) ?? throw new InvalidOperationException("The root request handler did not return a response");

Expand Down
97 changes: 97 additions & 0 deletions Engine/InternalH3Experimental/AltSvc.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using GenHTTP.Api.Content;
using GenHTTP.Api.Infrastructure;
using GenHTTP.Api.Protocol;

namespace GenHTTP.Engine.InternalH3Experimental;

/// <summary>
/// Advertises an HTTP/3 endpoint from a server that speaks HTTP/1.1 or HTTP/2.
/// </summary>
/// <remarks>
/// Browsers never start on HTTP/3. They connect over TCP, and only try QUIC once a response has
/// told them where to find it (RFC 7838). Without this header the HTTP/3 endpoint is reachable by
/// clients told to use it explicitly, and by nobody else.
///
/// <para>Two things stop it working, both silently: the advertisement is only honoured when it
/// arrives over TLS, and the certificate on the HTTP/3 port must be valid for the ORIGIN's host
/// name. A wrong port produces no error at all, the browser simply keeps using HTTP/1.1.</para>
/// </remarks>
public sealed class AltSvcConcern : IConcern
{
private readonly ByteString _value;

public IHandler Content { get; }

public AltSvcConcern(IHandler content, ushort port, uint maxAge)
{
Content = content;
_value = new ByteString($"h3=\":{port}\"; ma={maxAge}");
}

public ValueTask PrepareAsync(IServer server) => Content.PrepareAsync(server);

public async ValueTask<IResponse?> HandleAsync(IRequest request)
{
IResponse? response = await Content.HandleAsync(request);

// Pointless on a connection that is already HTTP/3, and ignored by clients anyway.
if (response is not null && request.Header.Protocol != HttpProtocol.Http3)
{
response.Rebuild().Header(AltSvcName, _value);
}

return response;
}

private static readonly ByteString AltSvcName = new("alt-svc");
}

/// <summary>
/// Builder for <see cref="AltSvcConcern"/>.
/// </summary>
public sealed class AltSvcConcernBuilder : IConcernBuilder
{
private ushort _port = 443;

private uint _maxAge = 86400;

/// <summary>
/// The UDP port the HTTP/3 endpoint listens on. Must match what that server bound, or clients
/// silently never upgrade.
/// </summary>
public AltSvcConcernBuilder Port(ushort port)
{
_port = port;
return this;
}

/// <summary>How long a client may cache the advertisement, in seconds.</summary>
public AltSvcConcernBuilder MaxAge(uint seconds)
{
_maxAge = seconds;
return this;
}

public IConcern Build(IHandler content) => new AltSvcConcern(content, _port, _maxAge);
}

/// <summary>
/// Advertises an HTTP/3 endpoint to clients arriving over TCP.
/// </summary>
public static class AltSvc
{

/// <summary>
/// Adds an <c>Alt-Svc</c> header pointing at an HTTP/3 endpoint on the given UDP port.
/// </summary>
/// <example>
/// <code>
/// var h1 = GenHTTP.Engine.Internal.Host.Create()
/// .Handler(app)
/// .Add(AltSvc.To(443))
/// .Bind(IPAddress.Any, 443, certificate);
/// </code>
/// </example>
public static AltSvcConcernBuilder To(ushort port) => new AltSvcConcernBuilder().Port(port);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>

<Description>EXPERIMENTAL HTTP/3 engine for GenHTTP: QUIC via System.Net.Quic (MsQuic), HTTP/3 via Glyph3. Runs alongside another engine that serves HTTP/1.1 and advertises this one with Alt-Svc.</Description>
<PackageTags>GenHTTP HTTP HTTP3 H3 QUIC Webserver Server Library C# Engine Experimental</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>

<!-- System.Net.Quic is annotated as linux/macOS/windows only, and the analyzer cannot see
the QuicListener.IsSupported check this engine makes before touching any of it. -->
<NoWarn>$(NoWarn);CA1416</NoWarn>

</PropertyGroup>

<ItemGroup>

<ProjectReference Include="..\..\API\GenHTTP.Api.csproj" />

<ProjectReference Include="..\Shared\GenHTTP.Engine.Shared.csproj" />

<PackageReference Include="Glyph3" Version="0.12.0" />

</ItemGroup>

<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="" />
</ItemGroup>

</Project>
36 changes: 36 additions & 0 deletions Engine/InternalH3Experimental/Host.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using GenHTTP.Api.Infrastructure;

using GenHTTP.Engine.InternalH3Experimental.Infrastructure;

namespace GenHTTP.Engine.InternalH3Experimental;

/// <summary>
/// Entry point to host an application over HTTP/3.
/// </summary>
/// <remarks>
/// EXPERIMENTAL. QUIC comes from System.Net.Quic, which needs libmsquic present: Windows ships it
/// with the .NET runtime, Linux and macOS install it separately. HTTP/3 comes from Glyph3.
///
/// <para>Browsers do not reach HTTP/3 directly. They connect over HTTP/1.1 or HTTP/2 first and
/// only try QUIC once a server advertises it, so this engine is meant to run beside one that
/// serves TCP. See <see cref="AltSvc"/>.</para>
/// </remarks>
public static class Host
{

/// <summary>
/// Provides a new server host serving HTTP/3 over QUIC.
/// </summary>
/// <param name="qpackDynamicTableCapacity">
/// Bytes of QPACK dynamic table advertised to clients, and the ceiling on what this server will
/// use for its own responses. 0 (the default) switches the mechanism off: headers are encoded
/// with the static table and literals only.
///
/// A nonzero value lets a client compress headers it repeats - cookies and user-agent, mostly -
/// to about two bytes each. Most clients decline: curl, and .NET's own HTTP/3 client, advertise
/// no table at all. Browsers are the ones that may use it.
/// </param>
public static IServerHost Create(int qpackDynamicTableCapacity = 0)
=> new H3ServerHost(qpackDynamicTableCapacity);

}
93 changes: 93 additions & 0 deletions Engine/InternalH3Experimental/Infrastructure/H3Server.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Diagnostics;
using System.Reflection;

using GenHTTP.Api.Content;
using GenHTTP.Api.Infrastructure;

using GenHTTP.Engine.Shared.Infrastructure;
using GenHTTP.Engine.Shared.Types;

using Microsoft.Extensions.Logging;

namespace GenHTTP.Engine.InternalH3Experimental.Infrastructure;

internal sealed class H3Server : IServer
{
private readonly QuicEndPointCollection _endPoints;

private readonly PropertyBag _properties = new();

private readonly ILogger _logger;

public string Version { get; }

public bool Running => !_disposed;

public bool Development => Configuration.DevelopmentMode;

public IHandler Handler { get; }

public IPropertyBag Properties => _properties;

public ILoggerFactory Logging => Configuration.Logging;

public IEndPointCollection EndPoints => _endPoints;

internal ServerConfiguration Configuration { get; }

internal int QpackCapacity { get; }

internal H3Server(ServerConfiguration configuration, IHandler handler, int qpackCapacity)
{
QpackCapacity = qpackCapacity;

Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "(n/a)";

Configuration = configuration;

Handler = handler;

_logger = configuration.Logging.CreateLogger<H3Server>();

_endPoints = new QuicEndPointCollection(this, configuration.EndPoints);
}

public async ValueTask StartAsync()
{
await PrepareHandlerAsync(Handler);

await _endPoints.StartAsync();
}

private async ValueTask PrepareHandlerAsync(IHandler handler)
{
try
{
var start = Stopwatch.GetTimestamp();

await handler.PrepareAsync(this);

var elapsed = Stopwatch.GetElapsedTime(start);

_logger.LogInformation("Prepared handlers in {ElapsedMs:0.##} ms", elapsed.TotalMilliseconds);
}
catch (Exception e)
{
_logger.LogCritical(e, "Failed to prepare the handler chain");
}
}

private bool _disposed;

public ValueTask DisposeAsync()
{
if (!_disposed)
{
_endPoints.Dispose();

_disposed = true;
}

return new();
}
}
18 changes: 18 additions & 0 deletions Engine/InternalH3Experimental/Infrastructure/H3ServerHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using GenHTTP.Api.Content;
using GenHTTP.Api.Infrastructure;

using GenHTTP.Engine.Shared.Hosting;
using GenHTTP.Engine.Shared.Infrastructure;

namespace GenHTTP.Engine.InternalH3Experimental.Infrastructure;

internal sealed class H3ServerHost : ServerHost
{
private readonly int _qpackCapacity;

internal H3ServerHost(int qpackCapacity) => _qpackCapacity = qpackCapacity;

protected override IServer Build(ServerConfiguration config, IHandler handler)
=> new H3Server(config, handler, _qpackCapacity);

}
Loading
Loading