diff --git a/src/Servers/Connections.Abstractions/src/Microsoft.AspNetCore.Connections.Abstractions.csproj b/src/Servers/Connections.Abstractions/src/Microsoft.AspNetCore.Connections.Abstractions.csproj index 691b85d69717..039515346f72 100644 --- a/src/Servers/Connections.Abstractions/src/Microsoft.AspNetCore.Connections.Abstractions.csproj +++ b/src/Servers/Connections.Abstractions/src/Microsoft.AspNetCore.Connections.Abstractions.csproj @@ -1,4 +1,4 @@ - + Core components of ASP.NET Core networking protocol stack. @@ -19,6 +19,11 @@ + + + + + callback. + /// + public object? OnConnectionState { get; set; } + + /// + /// Gets or sets a list of ALPN protocols. + /// + public List ApplicationProtocols { get; set; } = default!; +} +#endif diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs index b0729be74947..b1871b6d5ca8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs @@ -3,10 +3,13 @@ #nullable enable +using System.IO.Pipelines; using System.Linq; using System.Net; +using System.Net.Security; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; @@ -53,11 +56,35 @@ public async Task BindAsync(EndPoint endPoint, MultiplexedConnectionDe var features = new FeatureCollection(); - // This should always be set in production, but it's not set for InMemory tests. - // The transport will check if the feature is missing. + // HttpsOptions or HttpsCallbackOptions should always be set in production, but it's not set for InMemory tests. + // The QUIC transport will check if TlsConnectionCallbackOptions is missing. if (listenOptions.HttpsOptions != null) { - features.Set(HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions)); + var sslServerAuthenticationOptions = HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions); + features.Set(new TlsConnectionCallbackOptions + { + ApplicationProtocols = sslServerAuthenticationOptions.ApplicationProtocols ?? new List { SslApplicationProtocol.Http3 }, + OnConnection = (context, cancellationToken) => ValueTask.FromResult(sslServerAuthenticationOptions), + OnConnectionState = null, + }); + } + else if (listenOptions.HttpsCallbackOptions != null) + { + features.Set(new TlsConnectionCallbackOptions + { + ApplicationProtocols = new List { SslApplicationProtocol.Http3 }, + OnConnection = (context, cancellationToken) => + { + return listenOptions.HttpsCallbackOptions.OnConnection(new TlsHandshakeCallbackContext + { + ClientHelloInfo = context.ClientHelloInfo, + CancellationToken = cancellationToken, + State = context.State, + Connection = new ConnectionContextAdapter(context.Connection), + }); + }, + OnConnectionState = listenOptions.HttpsCallbackOptions.OnConnectionState, + }); } var transport = await _multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false); @@ -65,6 +92,49 @@ public async Task BindAsync(EndPoint endPoint, MultiplexedConnectionDe return transport.EndPoint; } + /// + /// TlsHandshakeCallbackContext.Connection is ConnectionContext but QUIC connection only implements BaseConnectionContext. + /// + private sealed class ConnectionContextAdapter : ConnectionContext + { + private readonly BaseConnectionContext _inner; + + public ConnectionContextAdapter(BaseConnectionContext inner) => _inner = inner; + + public override IDuplexPipe Transport + { + get => throw new NotSupportedException("Not supported by HTTP/3 connections."); + set => throw new NotSupportedException("Not supported by HTTP/3 connections."); + } + public override string ConnectionId + { + get => _inner.ConnectionId; + set => _inner.ConnectionId = value; + } + public override IFeatureCollection Features => _inner.Features; + public override IDictionary Items + { + get => _inner.Items; + set => _inner.Items = value; + } + public override EndPoint? LocalEndPoint + { + get => _inner.LocalEndPoint; + set => _inner.LocalEndPoint = value; + } + public override EndPoint? RemoteEndPoint + { + get => _inner.RemoteEndPoint; + set => _inner.RemoteEndPoint = value; + } + public override CancellationToken ConnectionClosed + { + get => _inner.ConnectionClosed; + set => _inner.ConnectionClosed = value; + } + public override ValueTask DisposeAsync() => _inner.DisposeAsync(); + } + private void StartAcceptLoop(IConnectionListener connectionListener, Func connectionDelegate, EndpointConfig? endpointConfig) where T : BaseConnectionContext { var transportConnectionManager = new TransportConnectionManager(_serviceContext.ConnectionManager); diff --git a/src/Servers/Kestrel/Core/src/ListenOptions.cs b/src/Servers/Kestrel/Core/src/ListenOptions.cs index d22777aa6132..e8dabefde6f3 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptions.cs @@ -109,6 +109,7 @@ internal string Scheme internal bool IsTls { get; set; } internal HttpsConnectionAdapterOptions? HttpsOptions { get; set; } + internal TlsHandshakeCallbackOptions? HttpsCallbackOptions { get; set; } /// /// Gets the name of this endpoint to display on command-line when the web server starts. diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index c0c644a1a3b0..8c3cb10d0210 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -254,10 +254,6 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOpt /// The . public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, object state, TimeSpan handshakeTimeout) { - if (listenOptions.Protocols.HasFlag(HttpProtocols.Http3)) - { - throw new NotSupportedException($"{nameof(UseHttps)} with {nameof(ServerOptionsSelectionCallback)} is not supported with HTTP/3."); - } return listenOptions.UseHttps(new TlsHandshakeCallbackOptions() { OnConnection = context => serverOptionsSelectionCallback(context.SslStream, context.ClientHelloInfo, context.State, context.CancellationToken), @@ -285,18 +281,17 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, TlsHandsh throw new ArgumentException($"{nameof(TlsHandshakeCallbackOptions.OnConnection)} must not be null."); } - if (listenOptions.Protocols.HasFlag(HttpProtocols.Http3)) - { - throw new NotSupportedException($"{nameof(UseHttps)} with {nameof(TlsHandshakeCallbackOptions)} is not supported with HTTP/3."); - } - var loggerFactory = listenOptions.KestrelServerOptions?.ApplicationServices.GetRequiredService() ?? NullLoggerFactory.Instance; listenOptions.IsTls = true; + listenOptions.HttpsCallbackOptions = callbackOptions; + listenOptions.Use(next => { - // Set the list of protocols from listen options + // Set the list of protocols from listen options. + // Set it inside Use delegate so Protocols and UseHttps can be called out of order. callbackOptions.HttpProtocols = listenOptions.Protocols; + var middleware = new HttpsConnectionMiddleware(next, callbackOptions, loggerFactory); return middleware.OnConnectionAsync; }); diff --git a/src/Servers/Kestrel/Core/src/LocalhostListenOptions.cs b/src/Servers/Kestrel/Core/src/LocalhostListenOptions.cs index eead334950bd..0836952fdc65 100644 --- a/src/Servers/Kestrel/Core/src/LocalhostListenOptions.cs +++ b/src/Servers/Kestrel/Core/src/LocalhostListenOptions.cs @@ -72,6 +72,7 @@ internal ListenOptions Clone(IPAddress address) DisableAltSvcHeader = DisableAltSvcHeader, IsTls = IsTls, HttpsOptions = HttpsOptions, + HttpsCallbackOptions = HttpsCallbackOptions, EndpointConfig = EndpointConfig }; diff --git a/src/Servers/Kestrel/Kestrel.slnf b/src/Servers/Kestrel/Kestrel.slnf index b40c80aafec6..5841f4e4451f 100644 --- a/src/Servers/Kestrel/Kestrel.slnf +++ b/src/Servers/Kestrel/Kestrel.slnf @@ -2,11 +2,17 @@ "solution": { "path": "..\\..\\..\\AspNetCore.sln", "projects": [ + "src\\DataProtection\\Abstractions\\src\\Microsoft.AspNetCore.DataProtection.Abstractions.csproj", + "src\\DataProtection\\Cryptography.Internal\\src\\Microsoft.AspNetCore.Cryptography.Internal.csproj", + "src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj", + "src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj", "src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj", "src\\Extensions\\Features\\test\\Microsoft.Extensions.Features.Tests.csproj", "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj", + "src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj", + "src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj", "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj", "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", @@ -21,8 +27,12 @@ "src\\Middleware\\HostFiltering\\src\\Microsoft.AspNetCore.HostFiltering.csproj", "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj", "src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj", + "src\\Security\\Authentication\\Core\\src\\Microsoft.AspNetCore.Authentication.csproj", "src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj", + "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj", "src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj", + "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj", + "src\\Servers\\IIS\\IIS\\src\\Microsoft.AspNetCore.Server.IIS.csproj", "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj", "src\\Servers\\Kestrel\\Core\\test\\Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj", "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", @@ -47,7 +57,8 @@ "src\\Servers\\Kestrel\\test\\Sockets.BindTests\\Sockets.BindTests.csproj", "src\\Servers\\Kestrel\\test\\Sockets.FunctionalTests\\Sockets.FunctionalTests.csproj", "src\\Servers\\Kestrel\\tools\\CodeGenerator\\CodeGenerator.csproj", - "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj" + "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj", + "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } } \ No newline at end of file diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs index 669bfb38acba..e4156392afdf 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs @@ -258,6 +258,11 @@ internal bool TryReturnStream(QuicStreamContext stream) return false; } + internal QuicConnection GetInnerConnection() + { + return _connection; + } + private void RemoveExpiredStreams() { lock (_poolLock) diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs index dcded3bb4edc..d42de2170381 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Net; using System.Net.Quic; using System.Net.Security; @@ -16,12 +17,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal; internal sealed class QuicConnectionListener : IMultiplexedConnectionListener, IAsyncDisposable { private readonly ILogger _log; - private bool _disposed; + private readonly TlsConnectionCallbackOptions _tlsConnectionCallbackOptions; private readonly QuicTransportContext _context; - private QuicListener? _listener; private readonly QuicListenerOptions _quicListenerOptions; + private bool _disposed; + private QuicListener? _listener; + private QuicConnectionContext? _currentAcceptingConnection; - public QuicConnectionListener(QuicTransportOptions options, ILogger log, EndPoint endpoint, SslServerAuthenticationOptions sslServerAuthenticationOptions) + public QuicConnectionListener( + QuicTransportOptions options, + ILogger log, + EndPoint endpoint, + TlsConnectionCallbackOptions tlsConnectionCallbackOptions) { if (!QuicListener.IsSupported) { @@ -33,30 +40,55 @@ public QuicConnectionListener(QuicTransportOptions options, ILogger log, EndPoin throw new InvalidOperationException($"QUIC doesn't support listening on the configured endpoint type. Expected {nameof(IPEndPoint)} but got {endpoint.GetType().Name}."); } - if (sslServerAuthenticationOptions.ApplicationProtocols is null || sslServerAuthenticationOptions.ApplicationProtocols.Count == 0) + if (tlsConnectionCallbackOptions.ApplicationProtocols.Count == 0) { throw new InvalidOperationException("No application protocols specified."); } _log = log; + _tlsConnectionCallbackOptions = tlsConnectionCallbackOptions; _context = new QuicTransportContext(_log, options); _quicListenerOptions = new QuicListenerOptions { - ApplicationProtocols = sslServerAuthenticationOptions.ApplicationProtocols, + ApplicationProtocols = _tlsConnectionCallbackOptions.ApplicationProtocols, ListenEndPoint = listenEndPoint, ListenBacklog = options.Backlog, - ConnectionOptionsCallback = (connection, helloInfo, cancellationToken) => + ConnectionOptionsCallback = async (connection, helloInfo, cancellationToken) => { + // Create the connection context inside the callback because it's passed + // to the connection callback. The field is then read once AcceptConnectionAsync + // finishes awaiting. + _currentAcceptingConnection = new QuicConnectionContext(connection, _context); + + var context = new TlsConnectionCallbackContext + { + ClientHelloInfo = helloInfo, + State = _tlsConnectionCallbackOptions.OnConnectionState, + Connection = _currentAcceptingConnection, + }; + var serverAuthenticationOptions = await _tlsConnectionCallbackOptions.OnConnection(context, cancellationToken); + + // If the callback didn't set protocols then use the listener's list of protocols. + if (serverAuthenticationOptions.ApplicationProtocols == null) + { + serverAuthenticationOptions.ApplicationProtocols = _tlsConnectionCallbackOptions.ApplicationProtocols; + } + + // If the SslServerAuthenticationOptions doesn't have a cert or protocols then the + // QUIC connection will fail and the client receives an unhelpful message. + // Validate the options on the server and log issues to improve debugging. + ValidateServerAuthenticationOptions(serverAuthenticationOptions); + var connectionOptions = new QuicServerConnectionOptions { - ServerAuthenticationOptions = sslServerAuthenticationOptions, + ServerAuthenticationOptions = serverAuthenticationOptions, IdleTimeout = options.IdleTimeout, MaxInboundBidirectionalStreams = options.MaxBidirectionalStreamCount, MaxInboundUnidirectionalStreams = options.MaxUnidirectionalStreamCount, DefaultCloseErrorCode = 0, DefaultStreamErrorCode = 0, }; - return ValueTask.FromResult(connectionOptions); + return connectionOptions; } }; @@ -65,13 +97,29 @@ public QuicConnectionListener(QuicTransportOptions options, ILogger log, EndPoin EndPoint = listenEndPoint; } + private void ValidateServerAuthenticationOptions(SslServerAuthenticationOptions serverAuthenticationOptions) + { + if (serverAuthenticationOptions.ServerCertificate == null && + serverAuthenticationOptions.ServerCertificateContext == null && + serverAuthenticationOptions.ServerCertificateSelectionCallback == null) + { + QuicLog.ConnectionListenerCertificateNotSpecified(_log); + } + if (serverAuthenticationOptions.ApplicationProtocols == null || serverAuthenticationOptions.ApplicationProtocols.Count == 0) + { + QuicLog.ConnectionListenerApplicationProtocolsNotSpecified(_log); + } + } + public EndPoint EndPoint { get; set; } public async ValueTask CreateListenerAsync() { _listener = await QuicListener.ListenAsync(_quicListenerOptions); - // Listener endpoint will resolve an ephemeral port, e.g. 127.0.0.1:0, into the actual port. + // EndPoint could be configured with an ephemeral port of 0. + // Listener endpoint will resolve an ephemeral port, e.g. 127.0.0.1:0, into the actual port + // so we need to update the public listener endpoint property. EndPoint = _listener.LocalEndPoint; } @@ -85,7 +133,13 @@ public async ValueTask CreateListenerAsync() try { var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken); - var connectionContext = new QuicConnectionContext(quicConnection, _context); + + // _currentAcceptingConnection is set inside ConnectionOptionsCallback. + var connectionContext = _currentAcceptingConnection; + + // Verify the connection context was created and set correctly. + Debug.Assert(connectionContext != null); + Debug.Assert(connectionContext.GetInnerConnection() == quicConnection); QuicLog.AcceptedConnection(_log, connectionContext); diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs index ea1b4fa485e0..71001f09c2df 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs @@ -1,6 +1,7 @@ // 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.Security; using Microsoft.AspNetCore.Connections; using Microsoft.Extensions.Logging; @@ -195,6 +196,13 @@ public static void StreamReused(ILogger logger, QuicStreamContext streamContext) } } + [LoggerMessage(18, LogLevel.Warning, $"{nameof(SslServerAuthenticationOptions)} must provide a server certificate using {nameof(SslServerAuthenticationOptions.ServerCertificate)}," + + $" {nameof(SslServerAuthenticationOptions.ServerCertificateContext)}, or {nameof(SslServerAuthenticationOptions.ServerCertificateSelectionCallback)}.", EventName = "ConnectionListenerCertificateNotSpecified")] + public static partial void ConnectionListenerCertificateNotSpecified(ILogger logger); + + [LoggerMessage(19, LogLevel.Warning, $"{nameof(SslServerAuthenticationOptions)} must provide at least one application protocol using {nameof(SslServerAuthenticationOptions.ApplicationProtocols)}.", EventName = "ConnectionListenerApplicationProtocolsNotSpecified")] + public static partial void ConnectionListenerApplicationProtocolsNotSpecified(ILogger logger); + private static StreamType GetStreamType(QuicStreamContext streamContext) => streamContext.CanRead && streamContext.CanWrite ? StreamType.Bidirectional diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.FeatureCollection.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.FeatureCollection.cs index ed0807aedffc..057ae8464e11 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.FeatureCollection.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.FeatureCollection.cs @@ -15,11 +15,11 @@ internal sealed partial class QuicStreamContext : IStreamAbortFeature, IStreamClosedFeature { - private readonly record struct CloseAction(Action Callback, object? State); + private readonly record struct OnCloseRegistration(Action Callback, object? State); private IDictionary? _persistentState; private long? _error; - private List? _onClosed; + private List? _onClosedRegistrations; public bool CanRead { get; private set; } public bool CanWrite { get; private set; } @@ -87,11 +87,11 @@ void IStreamClosedFeature.OnClosed(Action callback, object? state) { if (!_streamClosed) { - if (_onClosed == null) + if (_onClosedRegistrations == null) { - _onClosed = new List(); + _onClosedRegistrations = new List(); } - _onClosed.Add(new CloseAction(callback, state)); + _onClosedRegistrations.Add(new OnCloseRegistration(callback, state)); return; } } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs index d725385523d8..f082d25dad86 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs @@ -82,7 +82,7 @@ public void Initialize(QuicStream stream) _stream = stream; _streamClosedTokenSource = null; - _onClosed?.Clear(); + _onClosedRegistrations?.Clear(); InitializeFeatures(); @@ -331,7 +331,7 @@ private void FireStreamClosed() _streamClosed = true; } - var onClosed = _onClosed; + var onClosed = _onClosedRegistrations; if (onClosed != null) { diff --git a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs index f739f684b996..fbc769c79f85 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net; -using System.Net.Security; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal; @@ -50,22 +49,18 @@ public async ValueTask BindAsync(EndPoint endpoi throw new ArgumentNullException(nameof(endpoint)); } - var sslServerAuthenticationOptions = features?.Get(); + var tlsConnectionOptions = features?.Get(); - if (sslServerAuthenticationOptions == null) + if (tlsConnectionOptions == null) { throw new InvalidOperationException("Couldn't find HTTPS configuration for QUIC transport."); } - if (sslServerAuthenticationOptions.ServerCertificate == null - && sslServerAuthenticationOptions.ServerCertificateContext == null - && sslServerAuthenticationOptions.ServerCertificateSelectionCallback == null) + if (tlsConnectionOptions.ApplicationProtocols == null || tlsConnectionOptions.ApplicationProtocols.Count == 0) { - var message = $"{nameof(SslServerAuthenticationOptions)} must provide a server certificate using {nameof(SslServerAuthenticationOptions.ServerCertificate)}," - + $" {nameof(SslServerAuthenticationOptions.ServerCertificateContext)}, or {nameof(SslServerAuthenticationOptions.ServerCertificateSelectionCallback)}."; - throw new InvalidOperationException(message); + throw new InvalidOperationException("No application protocols specified for QUIC transport."); } - var transport = new QuicConnectionListener(_options, _log, endpoint, sslServerAuthenticationOptions); + var transport = new QuicConnectionListener(_options, _log, endpoint, tlsConnectionOptions); await transport.CreateListenerAsync(); return transport; diff --git a/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionListenerTests.cs b/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionListenerTests.cs index b8fde5a346d6..41dce7c8f735 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionListenerTests.cs +++ b/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionListenerTests.cs @@ -8,6 +8,7 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Testing; @@ -112,4 +113,96 @@ public async Task ClientCertificate_Required_NotSent_AcceptedViaCallback() var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint); await using var clientConnection = await QuicConnection.ConnectAsync(options); } + + [ConditionalFact] + [MsQuicSupported] + public async Task AcceptAsync_NoCertificateOrApplicationProtocol_Log() + { + // Arrange + await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory( + new TlsConnectionCallbackOptions + { + ApplicationProtocols = new List { SslApplicationProtocol.Http3 }, + OnConnection = (context, cancellationToken) => + { + var options = new SslServerAuthenticationOptions(); + options.ApplicationProtocols = new List(); + return ValueTask.FromResult(options); + } + }, + LoggerFactory); + + // Act + var acceptTask = connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout(); + + var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint); + + await Assert.ThrowsAsync(() => QuicConnection.ConnectAsync(options).AsTask()); + + // Assert + Assert.Contains(LogMessages, m => m.EventId.Name == "ConnectionListenerCertificateNotSpecified"); + Assert.Contains(LogMessages, m => m.EventId.Name == "ConnectionListenerApplicationProtocolsNotSpecified"); + } + + [ConditionalFact] + [MsQuicSupported] + public async Task AcceptAsync_NoApplicationProtocolsInCallback_DefaultToConnectionProtocols() + { + // Arrange + await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory( + new TlsConnectionCallbackOptions + { + ApplicationProtocols = new List { SslApplicationProtocol.Http3 }, + OnConnection = (context, cancellationToken) => + { + var options = new SslServerAuthenticationOptions(); + options.ServerCertificate = TestResources.GetTestCertificate(); + return ValueTask.FromResult(options); + } + }, + LoggerFactory); + + // Act + var acceptTask = connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout(); + + var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint); + + await using var clientConnection = await QuicConnection.ConnectAsync(options).DefaultTimeout(); + + // Assert + Assert.Equal(SslApplicationProtocol.Http3, clientConnection.NegotiatedApplicationProtocol); + } + + [ConditionalFact] + [MsQuicSupported] + public async Task AcceptAsync_TlsCallback_ConnectionContextInArguments() + { + // Arrange + BaseConnectionContext connectionContext = null; + await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory( + new TlsConnectionCallbackOptions + { + ApplicationProtocols = new List { SslApplicationProtocol.Http3 }, + OnConnection = (context, cancellationToken) => + { + var options = new SslServerAuthenticationOptions(); + options.ServerCertificate = TestResources.GetTestCertificate(); + + connectionContext = context.Connection; + + return ValueTask.FromResult(options); + } + }, + LoggerFactory); + + // Act + var acceptTask = connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout(); + + var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint); + + await using var clientConnection = await QuicConnection.ConnectAsync(options).DefaultTimeout(); + + // Assert + Assert.NotNull(connectionContext); + } } diff --git a/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs b/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs index 984b77e958fe..e99ca250c255 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs +++ b/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs @@ -52,6 +52,18 @@ public static async Task CreateConnectionListenerFactory return (QuicConnectionListener)await transportFactory.BindAsync(endpoint, features, cancellationToken: CancellationToken.None); } + public static async Task CreateConnectionListenerFactory(TlsConnectionCallbackOptions tlsConnectionOptions, ILoggerFactory loggerFactory = null, ISystemClock systemClock = null) + { + var transportFactory = CreateTransportFactory(loggerFactory, systemClock); + + // Use ephemeral port 0. OS will assign unused port. + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); + + var features = new FeatureCollection(); + features.Set(tlsConnectionOptions); + return (QuicConnectionListener)await transportFactory.BindAsync(endpoint, features, cancellationToken: CancellationToken.None); + } + public static FeatureCollection CreateBindAsyncFeatures(bool clientCertificateRequired = false) { var cert = TestResources.GetTestCertificate(); @@ -63,7 +75,11 @@ public static FeatureCollection CreateBindAsyncFeatures(bool clientCertificateRe sslServerAuthenticationOptions.ClientCertificateRequired = clientCertificateRequired; var features = new FeatureCollection(); - features.Set(sslServerAuthenticationOptions); + features.Set(new TlsConnectionCallbackOptions + { + ApplicationProtocols = sslServerAuthenticationOptions.ApplicationProtocols, + OnConnection = (context, cancellationToken) => ValueTask.FromResult(sslServerAuthenticationOptions) + }); return features; } diff --git a/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs b/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs index b9606e5d2d6c..8db0082f85c5 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs +++ b/src/Servers/Kestrel/Transport.Quic/test/QuicTransportFactoryTests.cs @@ -7,6 +7,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Testing; @@ -21,7 +22,7 @@ public class QuicTransportFactoryTests : TestApplicationErrorLoggerLoggedTest { [ConditionalFact] [MsQuicSupported] - public async Task BindAsync_NoFeature_Error() + public async Task BindAsync_NoFeatures_Error() { // Arrange var quicTransportOptions = new QuicTransportOptions(); @@ -36,18 +37,38 @@ public async Task BindAsync_NoFeature_Error() [ConditionalFact] [MsQuicSupported] - public async Task BindAsync_NoServerCertificate_Error() + public async Task BindAsync_NoApplicationProtocols_Error() { // Arrange var quicTransportOptions = new QuicTransportOptions(); var quicTransportFactory = new QuicTransportFactory(NullLoggerFactory.Instance, Options.Create(quicTransportOptions)); var features = new FeatureCollection(); - features.Set(new SslServerAuthenticationOptions()); + features.Set(new TlsConnectionCallbackOptions()); // Act var ex = await Assert.ThrowsAsync(() => quicTransportFactory.BindAsync(new IPEndPoint(0, 0), features: features, cancellationToken: CancellationToken.None).AsTask()).DefaultTimeout(); // Assert - Assert.Equal("SslServerAuthenticationOptions must provide a server certificate using ServerCertificate, ServerCertificateContext, or ServerCertificateSelectionCallback.", ex.Message); + Assert.Equal("No application protocols specified for QUIC transport.", ex.Message); + } + + [ConditionalFact] + [MsQuicSupported] + public async Task BindAsync_SslServerAuthenticationOptions_Success() + { + // Arrange + var quicTransportOptions = new QuicTransportOptions(); + var quicTransportFactory = new QuicTransportFactory(NullLoggerFactory.Instance, Options.Create(quicTransportOptions)); + var features = new FeatureCollection(); + features.Set(new TlsConnectionCallbackOptions + { + ApplicationProtocols = new List + { + SslApplicationProtocol.Http3 + } + }); + + // Act & Assert + await quicTransportFactory.BindAsync(new IPEndPoint(0, 0), features: features, cancellationToken: CancellationToken.None).AsTask().DefaultTimeout(); } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs index 0cba39d6da11..c78429ea1e9a 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -438,9 +439,11 @@ public async Task Http3_UseHttpsNoArgsWithDefaultCertificate_UseDefaultCertifica Assert.NotNull(bindFeatures); - var sslOptions = bindFeatures.Get(); + var sslOptions = bindFeatures.Get(); Assert.NotNull(sslOptions); - Assert.Equal(_x509Certificate2, sslOptions.ServerCertificate); + + var sslServerAuthenticationOptions = await sslOptions.OnConnection(new TlsConnectionCallbackContext(), CancellationToken.None); + Assert.Equal(_x509Certificate2, sslServerAuthenticationOptions.ServerCertificate); } [Fact] @@ -480,9 +483,11 @@ public async Task Http3_ConfigureHttpsDefaults_Works() Assert.NotNull(bindFeatures); - var sslOptions = bindFeatures.Get(); - Assert.NotNull(sslOptions); - Assert.Equal(_x509Certificate2, sslOptions.ServerCertificate); + var tlsOptions = bindFeatures.Get(); + Assert.NotNull(tlsOptions); + + var sslServerAuthenticationOptions = await tlsOptions.OnConnection(new TlsConnectionCallbackContext(), CancellationToken.None); + Assert.Equal(_x509Certificate2, sslServerAuthenticationOptions.ServerCertificate); } [Fact] @@ -520,7 +525,7 @@ public async Task Http1And2And3_NoUseHttps_MultiplexBindNotCalled() } [Fact] - public async Task Http2and3_NoUseHttps_Throws() + public async Task Http3_NoUseHttps_Throws() { var serverOptions = CreateServerOptions(); serverOptions.DefaultCertificate = _x509Certificate2; @@ -556,80 +561,98 @@ public async Task Http2and3_NoUseHttps_Throws() } [Fact] - public async Task Http3_NoUseHttps_Throws() + public async Task Http3_ServerOptionsSelectionCallback_Works() { var serverOptions = CreateServerOptions(); serverOptions.DefaultCertificate = _x509Certificate2; - var bindCalled = false; + IFeatureCollection bindFeatures = null; var multiplexedConnectionListenerFactory = new MockMultiplexedConnectionListenerFactory(); multiplexedConnectionListenerFactory.OnBindAsync = (ep, features) => { - bindCalled = true; + bindFeatures = features; }; + var testState = new object(); var testContext = new TestServiceContext(LoggerFactory); testContext.ServerOptions = serverOptions; - var ex = await Assert.ThrowsAsync(async () => - { - await using var server = new TestServer(context => Task.CompletedTask, - testContext, - serverOptions => + await using (var server = new TestServer(context => Task.CompletedTask, + testContext, + serverOptions => + { + serverOptions.ListenLocalhost(5001, listenOptions => { - serverOptions.ListenLocalhost(5001, listenOptions => + listenOptions.Protocols = HttpProtocols.Http3; + listenOptions.UseHttps((SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) => { - listenOptions.Protocols = HttpProtocols.Http3; - }); - }, - services => - { - services.AddSingleton(multiplexedConnectionListenerFactory); + return ValueTask.FromResult(new SslServerAuthenticationOptions()); + }, state: testState); }); - }); + }, + services => + { + services.AddSingleton(multiplexedConnectionListenerFactory); + })) + { + } - Assert.False(bindCalled); - Assert.Equal("HTTP/3 requires HTTPS.", ex.InnerException.InnerException.Message); - } + Assert.NotNull(bindFeatures); - [Fact] - public void Http3_ServerOptionsSelectionCallback_Throws() - { - var serverOptions = CreateServerOptions(); - serverOptions.DefaultCertificate = _x509Certificate2; + var tlsOptions = bindFeatures.Get(); + Assert.NotNull(tlsOptions); - serverOptions.ListenLocalhost(5001, options => - { - options.Protocols = HttpProtocols.Http3; - var exception = Assert.Throws(() => - options.UseHttps((SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) => - { - return ValueTask.FromResult((new SslServerAuthenticationOptions())); - }, state: null) - ); - Assert.Equal("UseHttps with ServerOptionsSelectionCallback is not supported with HTTP/3.", exception.Message); - }); + Assert.Collection(tlsOptions.ApplicationProtocols, p => Assert.Equal(SslApplicationProtocol.Http3, p)); + + Assert.Equal(testState, tlsOptions.OnConnectionState); } [Fact] - public void Http3_TlsHandshakeCallbackOptions_Throws() + public async Task Http3_TlsHandshakeCallbackOptions_Works() { var serverOptions = CreateServerOptions(); serverOptions.DefaultCertificate = _x509Certificate2; - serverOptions.ListenLocalhost(5001, options => + IFeatureCollection bindFeatures = null; + var multiplexedConnectionListenerFactory = new MockMultiplexedConnectionListenerFactory(); + multiplexedConnectionListenerFactory.OnBindAsync = (ep, features) => { - options.Protocols = HttpProtocols.Http3; - var exception = Assert.Throws(() => - options.UseHttps(new TlsHandshakeCallbackOptions() + bindFeatures = features; + }; + + var testState = new object(); + var testContext = new TestServiceContext(LoggerFactory); + testContext.ServerOptions = serverOptions; + await using (var server = new TestServer(context => Task.CompletedTask, + testContext, + serverOptions => + { + serverOptions.ListenLocalhost(5001, listenOptions => { - OnConnection = context => + listenOptions.Protocols = HttpProtocols.Http3; + listenOptions.UseHttps(new TlsHandshakeCallbackOptions() { - return ValueTask.FromResult(new SslServerAuthenticationOptions()); - } - }) - ); - Assert.Equal("UseHttps with TlsHandshakeCallbackOptions is not supported with HTTP/3.", exception.Message); - }); + OnConnection = context => + { + return ValueTask.FromResult(new SslServerAuthenticationOptions()); + }, + OnConnectionState = testState + }); + }); + }, + services => + { + services.AddSingleton(multiplexedConnectionListenerFactory); + })) + { + } + + Assert.NotNull(bindFeatures); + + var tlsOptions = bindFeatures.Get(); + Assert.Collection(tlsOptions.ApplicationProtocols, p => Assert.Equal(SslApplicationProtocol.Http3, p)); + + Assert.NotNull(tlsOptions.OnConnection); + Assert.Equal(testState, tlsOptions.OnConnectionState); } [Fact] diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs index 2285600181cb..0b72d59f5889 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs @@ -5,6 +5,7 @@ using System.Net; using System.Net.Http; using System.Net.Quic; +using System.Net.Security; using System.Text; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; @@ -13,6 +14,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -1239,6 +1241,58 @@ public async Task GET_ConnectionLoggingConfigured_OutputToLogs() } } + [ConditionalFact] + [MsQuicSupported] + public async Task GET_UseHttpsCallback_ConnectionContextAvailable() + { + // Arrange + BaseConnectionContext connectionContext = null; + var builder = CreateHostBuilder( + context => + { + return Task.CompletedTask; + }, + configureKestrel: kestrel => + { + kestrel.ListenLocalhost(5001, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http3; + listenOptions.UseHttps(new TlsHandshakeCallbackOptions + { + OnConnection = context => + { + connectionContext = context.Connection; + return ValueTask.FromResult(new SslServerAuthenticationOptions + { + ServerCertificate = TestResources.GetTestCertificate() + }); + } + }); + }); + }); + + using (var host = builder.Build()) + using (var client = HttpHelpers.CreateClient()) + { + await host.StartAsync(); + + var port = 5001; + + // Act + var request1 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{port}/"); + request1.Version = HttpVersion.Version30; + request1.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + + var response1 = await client.SendAsync(request1, CancellationToken.None); + response1.EnsureSuccessStatusCode(); + + // Assert + Assert.NotNull(connectionContext); + + await host.StopAsync(); + } + } + [ConditionalFact] [MsQuicSupported] public async Task GET_ClientDisconnected_ConnectionAbortRaised() diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs index b14b3e8e508c..461763e8bd0f 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs @@ -4,6 +4,7 @@ using System.Net; using System.Net.Http; using System.Net.Quic; +using System.Net.Security; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -246,9 +247,11 @@ public async Task ClientCertificate_Allow_NotAvailable_Optional() await host.StopAsync().DefaultTimeout(); } - [ConditionalFact] + [ConditionalTheory] [MsQuicSupported] - public async Task OnAuthenticate_Available_Throws() + [InlineData(HttpProtocols.Http3)] + [InlineData(HttpProtocols.Http1AndHttp2AndHttp3)] + public async Task OnAuthenticate_Available_Throws(HttpProtocols protocols) { var builder = CreateHostBuilder(async context => { @@ -257,7 +260,7 @@ public async Task OnAuthenticate_Available_Throws() { kestrelOptions.ListenAnyIP(0, listenOptions => { - listenOptions.Protocols = HttpProtocols.Http3; + listenOptions.Protocols = protocols; listenOptions.UseHttps(httpsOptions => { httpsOptions.OnAuthenticate = (_, _) => { }; @@ -273,6 +276,57 @@ public async Task OnAuthenticate_Available_Throws() Assert.Equal("The OnAuthenticate callback is not supported with HTTP/3.", exception.Message); } + [ConditionalFact] + [MsQuicSupported] + public async Task TlsHandshakeCallbackOptions_Invoked() + { + var configuredState = new object(); + object callbackState = null; + var builder = CreateHostBuilder(async context => + { + await context.Response.WriteAsync("Hello World"); + }, configureKestrel: kestrelOptions => + { + kestrelOptions.ListenAnyIP(0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http3; + listenOptions.UseHttps(new TlsHandshakeCallbackOptions + { + OnConnection = (context) => + { + callbackState = context.State; + return ValueTask.FromResult(new SslServerAuthenticationOptions + { + ServerCertificate = TestResources.GetTestCertificate(), + ApplicationProtocols = new List { SslApplicationProtocol.Http3 } + }); + }, + OnConnectionState = configuredState + }); + }); + }); + + using var host = builder.Build(); + using var client = HttpHelpers.CreateClient(); + + await host.StartAsync().DefaultTimeout(); + + var request = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/"); + request.Version = HttpVersion.Version30; + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + request.Headers.Host = "testhost"; + + var response = await client.SendAsync(request, CancellationToken.None).DefaultTimeout(); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpVersion.Version30, response.Version); + Assert.Equal("Hello World", result); + + Assert.Equal(configuredState, callbackState); + + await host.StopAsync().DefaultTimeout(); + } + private IHostBuilder CreateHostBuilder(RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action configureKestrel = null) { return HttpHelpers.CreateHostBuilder(AddTestLogging, requestDelegate, protocol, configureKestrel);