diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index 7e21713ae774..6f00b33a2ee5 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -235,7 +235,7 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder) internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder, Action? configureRouting = null) { - builder.UseKestrel((builderContext, options) => + builder.UseKestrelSlim().ConfigureKestrel((builderContext, options) => { options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true); }) diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt index 4a75aa0ff2d0..751bcc347b73 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -10,4 +10,4 @@ Microsoft.AspNetCore.Connections.NamedPipeEndPoint.PipeName.get -> string! Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ServerName.get -> string! override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.Equals(object? obj) -> bool override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.GetHashCode() -> int -override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ToString() -> string! \ No newline at end of file +override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ToString() -> string! diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 4a75aa0ff2d0..751bcc347b73 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -10,4 +10,4 @@ Microsoft.AspNetCore.Connections.NamedPipeEndPoint.PipeName.get -> string! Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ServerName.get -> string! override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.Equals(object? obj) -> bool override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.GetHashCode() -> int -override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ToString() -> string! \ No newline at end of file +override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ToString() -> string! diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt index 4a75aa0ff2d0..751bcc347b73 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt @@ -10,4 +10,4 @@ Microsoft.AspNetCore.Connections.NamedPipeEndPoint.PipeName.get -> string! Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ServerName.get -> string! override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.Equals(object? obj) -> bool override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.GetHashCode() -> int -override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ToString() -> string! \ No newline at end of file +override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ToString() -> string! diff --git a/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs b/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs index 8435bfa998a6..1f65ab555f04 100644 --- a/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs +++ b/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs @@ -55,6 +55,11 @@ public HttpsConnectionAdapterOptions() /// public Func? ServerCertificateSelector { get; set; } + /// + /// A convenience property for checking whether a server certificate or selector has been set. + /// + internal bool HasServerCertificateOrSelector => ServerCertificate is not null || ServerCertificateSelector is not null; + /// /// Specifies the client certificate requirements for a HTTPS connection. Defaults to . /// diff --git a/src/Servers/Kestrel/Core/src/ITlsConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/ITlsConfigurationLoader.cs new file mode 100644 index 000000000000..d0173b9dd60f --- /dev/null +++ b/src/Servers/Kestrel/Core/src/ITlsConfigurationLoader.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Https; + +namespace Microsoft.AspNetCore.Server.Kestrel; + +internal interface ITlsConfigurationLoader +{ + void ApplyHttpsDefaults( + KestrelServerOptions serverOptions, + EndpointConfig endpoint, + HttpsConnectionAdapterOptions httpsOptions, + CertificateConfig? defaultCertificateConfig, + ConfigurationReader configurationReader); + + CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader); + + void UseHttps(ListenOptions listenOptions, EndpointConfig endpoint, HttpsConnectionAdapterOptions httpsOptions); +} + +internal record CertificateAndConfig(X509Certificate2 Certificate, CertificateConfig CertificateConfig); diff --git a/src/Servers/Kestrel/Core/src/IUseHttpsHelper.cs b/src/Servers/Kestrel/Core/src/IUseHttpsHelper.cs new file mode 100644 index 000000000000..c9b3fb52f109 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/IUseHttpsHelper.cs @@ -0,0 +1,11 @@ +// 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.Server.Kestrel.Core; + +namespace Microsoft.AspNetCore.Hosting; + +internal interface IUseHttpsHelper +{ + ListenOptions UseHttps(ListenOptions listenOptions); +} diff --git a/src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs b/src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs index a2e872da3080..092614536b4e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs +++ b/src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs @@ -17,12 +17,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; internal sealed class AddressBinder { // note this doesn't copy the ListenOptions[], only call this with an array that isn't mutated elsewhere - public static async Task BindAsync(ListenOptions[] listenOptions, AddressBindContext context, CancellationToken cancellationToken) + public static async Task BindAsync(ListenOptions[] listenOptions, AddressBindContext context, IUseHttpsHelper useHttpsHelper, CancellationToken cancellationToken) { var strategy = CreateStrategy( listenOptions, context.Addresses.ToArray(), - context.ServerAddressesFeature.PreferHostingUrls); + context.ServerAddressesFeature.PreferHostingUrls, + useHttpsHelper); // reset options. The actual used options and addresses will be populated // by the address binding feature @@ -32,7 +33,7 @@ public static async Task BindAsync(ListenOptions[] listenOptions, AddressBindCon await strategy.BindAsync(context, cancellationToken).ConfigureAwait(false); } - private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[] addresses, bool preferAddresses) + private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[] addresses, bool preferAddresses, IUseHttpsHelper useHttpsHelper) { var hasListenOptions = listenOptions.Length > 0; var hasAddresses = addresses.Length > 0; @@ -41,10 +42,10 @@ private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[] { if (hasListenOptions) { - return new OverrideWithAddressesStrategy(addresses); + return new OverrideWithAddressesStrategy(addresses, useHttpsHelper); } - return new AddressesStrategy(addresses); + return new AddressesStrategy(addresses, useHttpsHelper); } else if (hasListenOptions) { @@ -58,7 +59,7 @@ private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[] else if (hasAddresses) { // If no endpoints are configured directly using KestrelServerOptions, use those configured via the IServerAddressesFeature. - return new AddressesStrategy(addresses); + return new AddressesStrategy(addresses, useHttpsHelper); } else { @@ -71,6 +72,9 @@ private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[] /// Returns an for the given host an port. /// If the host parameter isn't "localhost" or an IP address, use IPAddress.Any. /// + /// + /// Internal for testing. + /// internal static bool TryCreateIPEndPoint(BindingAddress address, [NotNullWhen(true)] out IPEndPoint? endpoint) { if (!IPAddress.TryParse(address.Host, out var ip)) @@ -162,8 +166,8 @@ public async Task BindAsync(AddressBindContext context, CancellationToken cancel private sealed class OverrideWithAddressesStrategy : AddressesStrategy { - public OverrideWithAddressesStrategy(IReadOnlyCollection addresses) - : base(addresses) + public OverrideWithAddressesStrategy(IReadOnlyCollection addresses, IUseHttpsHelper useHttpsHelper) + : base(addresses, useHttpsHelper) { } @@ -216,10 +220,12 @@ public virtual async Task BindAsync(AddressBindContext context, CancellationToke private class AddressesStrategy : IStrategy { protected readonly IReadOnlyCollection _addresses; + private readonly IUseHttpsHelper _useHttpsHelper; - public AddressesStrategy(IReadOnlyCollection addresses) + public AddressesStrategy(IReadOnlyCollection addresses, IUseHttpsHelper useHttpsHelper) { _addresses = addresses; + _useHttpsHelper = useHttpsHelper; } public virtual async Task BindAsync(AddressBindContext context, CancellationToken cancellationToken) @@ -231,7 +237,7 @@ public virtual async Task BindAsync(AddressBindContext context, CancellationToke if (https && !options.IsTls) { - options.UseHttps(); + _useHttpsHelper.UseHttps(options); } await options.BindAsync(context, cancellationToken).ConfigureAwait(false); diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IMultiplexedTransportManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IMultiplexedTransportManager.cs new file mode 100644 index 000000000000..4a5b856b78c2 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IMultiplexedTransportManager.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Net; +using Microsoft.AspNetCore.Connections; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; + +internal interface IMultiplexedTransportManager +{ + bool HasFactories { get; } + + Task BindAsync(EndPoint endPoint, MultiplexedConnectionDelegate multiplexedConnectionDelegate, ListenOptions listenOptions, CancellationToken cancellationToken); + + Task StopAsync(CancellationToken cancellationToken); + Task StopEndpointsAsync(List endpointsToStop, CancellationToken cancellationToken); +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITransportManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITransportManager.cs new file mode 100644 index 000000000000..16155277a1e3 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITransportManager.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Net; +using Microsoft.AspNetCore.Connections; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; + +internal interface ITransportManager +{ + bool HasFactories { get; } + + Task BindAsync(EndPoint endPoint, ConnectionDelegate connectionDelegate, EndpointConfig? endpointConfig, CancellationToken cancellationToken); + + Task StopAsync(CancellationToken cancellationToken); + Task StopEndpointsAsync(List endpointsToStop, CancellationToken cancellationToken); +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/MultiplexedTransportManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/MultiplexedTransportManager.cs new file mode 100644 index 000000000000..4683db52d73a --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/MultiplexedTransportManager.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#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; + +internal sealed class MultiplexedTransportManager : TransportManagerBase, IMultiplexedTransportManager +{ + private readonly List _factories; + + public MultiplexedTransportManager( + ServiceContext serviceContext, + IEnumerable factories) + : base(serviceContext) + { + _factories = factories.Reverse().ToList(); + } + + public override bool HasFactories => _factories.Count > 0; + + public async Task BindAsync(EndPoint endPoint, MultiplexedConnectionDelegate multiplexedConnectionDelegate, ListenOptions listenOptions, CancellationToken cancellationToken) + { + if (!HasFactories) + { + throw new InvalidOperationException($"Cannot bind with {nameof(MultiplexedConnectionDelegate)} no {nameof(IMultiplexedConnectionListenerFactory)} is registered."); + } + + var features = new FeatureCollection(); + + // 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) + { + 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, + }); + } + + foreach (var factory in _factories) + { + var selector = factory as IConnectionListenerFactorySelector; + if (CanBindFactory(endPoint, selector)) + { + var transport = await factory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false); + StartAcceptLoop(new GenericMultiplexedConnectionListener(transport), c => multiplexedConnectionDelegate(c), listenOptions.EndpointConfig); + return transport.EndPoint; + } + } + + throw new InvalidOperationException($"No registered {nameof(IMultiplexedConnectionListenerFactory)} supports endpoint {endPoint.GetType().Name}: {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 sealed class GenericMultiplexedConnectionListener : IConnectionListener + { + private readonly IMultiplexedConnectionListener _multiplexedConnectionListener; + + public GenericMultiplexedConnectionListener(IMultiplexedConnectionListener multiplexedConnectionListener) + { + _multiplexedConnectionListener = multiplexedConnectionListener; + } + + public EndPoint EndPoint => _multiplexedConnectionListener.EndPoint; + + public ValueTask AcceptAsync(CancellationToken cancellationToken = default) + => _multiplexedConnectionListener.AcceptAsync(features: null, cancellationToken); + + public ValueTask UnbindAsync(CancellationToken cancellationToken = default) + => _multiplexedConnectionListener.UnbindAsync(cancellationToken); + + public ValueTask DisposeAsync() + => _multiplexedConnectionListener.DisposeAsync(); + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs index 00505ae9e354..5e28e4e4079d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs @@ -3,50 +3,39 @@ #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; -internal sealed class TransportManager +internal sealed class TransportManager : TransportManagerBase, ITransportManager { - private readonly List _transports = new List(); - - private readonly List _transportFactories; - private readonly List _multiplexedTransportFactories; - private readonly ServiceContext _serviceContext; + private readonly List _factories; public TransportManager( - List transportFactories, - List multiplexedTransportFactories, - ServiceContext serviceContext) + ServiceContext serviceContext, + IEnumerable factories) + : base(serviceContext) { - _transportFactories = transportFactories; - _multiplexedTransportFactories = multiplexedTransportFactories; - _serviceContext = serviceContext; + _factories = factories.Reverse().ToList(); } - private ConnectionManager ConnectionManager => _serviceContext.ConnectionManager; - private KestrelTrace Trace => _serviceContext.Log; + public override bool HasFactories => _factories.Count > 0; public async Task BindAsync(EndPoint endPoint, ConnectionDelegate connectionDelegate, EndpointConfig? endpointConfig, CancellationToken cancellationToken) { - if (_transportFactories.Count == 0) + if (!HasFactories) { throw new InvalidOperationException($"Cannot bind with {nameof(ConnectionDelegate)} no {nameof(IConnectionListenerFactory)} is registered."); } - foreach (var transportFactory in _transportFactories) + foreach (var factory in _factories) { - var selector = transportFactory as IConnectionListenerFactorySelector; + var selector = factory as IConnectionListenerFactorySelector; if (CanBindFactory(endPoint, selector)) { - var transport = await transportFactory.BindAsync(endPoint, cancellationToken).ConfigureAwait(false); + var transport = await factory.BindAsync(endPoint, cancellationToken).ConfigureAwait(false); StartAcceptLoop(new GenericConnectionListener(transport), c => connectionDelegate(c), endpointConfig); return transport.EndPoint; } @@ -63,209 +52,6 @@ public async Task BindAsync(EndPoint endPoint, ConnectionDelegate conn throw new InvalidOperationException($"No registered {nameof(IConnectionListenerFactory)} supports endpoint {endPoint.GetType().Name}: {endPoint}"); } - public async Task BindAsync(EndPoint endPoint, MultiplexedConnectionDelegate multiplexedConnectionDelegate, ListenOptions listenOptions, CancellationToken cancellationToken) - { - if (_multiplexedTransportFactories.Count == 0) - { - throw new InvalidOperationException($"Cannot bind with {nameof(MultiplexedConnectionDelegate)} no {nameof(IMultiplexedConnectionListenerFactory)} is registered."); - } - - var features = new FeatureCollection(); - - // 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) - { - 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, - }); - } - - foreach (var multiplexedTransportFactory in _multiplexedTransportFactories) - { - var selector = multiplexedTransportFactory as IConnectionListenerFactorySelector; - if (CanBindFactory(endPoint, selector)) - { - var transport = await multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false); - StartAcceptLoop(new GenericMultiplexedConnectionListener(transport), c => multiplexedConnectionDelegate(c), listenOptions.EndpointConfig); - return transport.EndPoint; - } - } - - throw new InvalidOperationException($"No registered {nameof(IMultiplexedConnectionListenerFactory)} supports endpoint {endPoint.GetType().Name}: {endPoint}"); - } - - private static bool CanBindFactory(EndPoint endPoint, IConnectionListenerFactorySelector? selector) - { - // By default, the last registered factory binds to the endpoint. - // A factory can implement IConnectionListenerFactorySelector to decide whether it can bind to the endpoint. - return selector?.CanBind(endPoint) ?? true; - } - - /// - /// 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); - var connectionDispatcher = new ConnectionDispatcher(_serviceContext, connectionDelegate, transportConnectionManager); - var acceptLoopTask = connectionDispatcher.StartAcceptingConnections(connectionListener); - - _transports.Add(new ActiveTransport(connectionListener, acceptLoopTask, transportConnectionManager, endpointConfig)); - } - - public Task StopEndpointsAsync(List endpointsToStop, CancellationToken cancellationToken) - { - var transportsToStop = new List(); - foreach (var t in _transports) - { - if (t.EndpointConfig is not null && endpointsToStop.Contains(t.EndpointConfig)) - { - transportsToStop.Add(t); - } - } - return StopTransportsAsync(transportsToStop, cancellationToken); - } - - public Task StopAsync(CancellationToken cancellationToken) - { - return StopTransportsAsync(new List(_transports), cancellationToken); - } - - private async Task StopTransportsAsync(List transportsToStop, CancellationToken cancellationToken) - { - var tasks = new Task[transportsToStop.Count]; - - for (int i = 0; i < transportsToStop.Count; i++) - { - tasks[i] = transportsToStop[i].UnbindAsync(cancellationToken); - } - - await Task.WhenAll(tasks).ConfigureAwait(false); - - async Task StopTransportConnection(ActiveTransport transport) - { - if (!await transport.TransportConnectionManager.CloseAllConnectionsAsync(cancellationToken).ConfigureAwait(false)) - { - Trace.NotAllConnectionsClosedGracefully(); - - if (!await transport.TransportConnectionManager.AbortAllConnectionsAsync().ConfigureAwait(false)) - { - Trace.NotAllConnectionsAborted(); - } - } - } - - for (int i = 0; i < transportsToStop.Count; i++) - { - tasks[i] = StopTransportConnection(transportsToStop[i]); - } - - await Task.WhenAll(tasks).ConfigureAwait(false); - - for (int i = 0; i < transportsToStop.Count; i++) - { - tasks[i] = transportsToStop[i].DisposeAsync().AsTask(); - } - - await Task.WhenAll(tasks).ConfigureAwait(false); - - foreach (var transport in transportsToStop) - { - _transports.Remove(transport); - } - } - - private sealed class ActiveTransport : IAsyncDisposable - { - public ActiveTransport(IConnectionListenerBase transport, Task acceptLoopTask, TransportConnectionManager transportConnectionManager, EndpointConfig? endpointConfig = null) - { - ConnectionListener = transport; - AcceptLoopTask = acceptLoopTask; - TransportConnectionManager = transportConnectionManager; - EndpointConfig = endpointConfig; - } - - public IConnectionListenerBase ConnectionListener { get; } - public Task AcceptLoopTask { get; } - public TransportConnectionManager TransportConnectionManager { get; } - - public EndpointConfig? EndpointConfig { get; } - - public async Task UnbindAsync(CancellationToken cancellationToken) - { - await ConnectionListener.UnbindAsync(cancellationToken).ConfigureAwait(false); - await AcceptLoopTask.ConfigureAwait(false); - } - - public ValueTask DisposeAsync() - { - return ConnectionListener.DisposeAsync(); - } - } - private sealed class GenericConnectionListener : IConnectionListener { private readonly IConnectionListener _connectionListener; @@ -286,25 +72,4 @@ public ValueTask UnbindAsync(CancellationToken cancellationToken = default) public ValueTask DisposeAsync() => _connectionListener.DisposeAsync(); } - - private sealed class GenericMultiplexedConnectionListener : IConnectionListener - { - private readonly IMultiplexedConnectionListener _multiplexedConnectionListener; - - public GenericMultiplexedConnectionListener(IMultiplexedConnectionListener multiplexedConnectionListener) - { - _multiplexedConnectionListener = multiplexedConnectionListener; - } - - public EndPoint EndPoint => _multiplexedConnectionListener.EndPoint; - - public ValueTask AcceptAsync(CancellationToken cancellationToken = default) - => _multiplexedConnectionListener.AcceptAsync(features: null, cancellationToken); - - public ValueTask UnbindAsync(CancellationToken cancellationToken = default) - => _multiplexedConnectionListener.UnbindAsync(cancellationToken); - - public ValueTask DisposeAsync() - => _multiplexedConnectionListener.DisposeAsync(); - } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManagerBase.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManagerBase.cs new file mode 100644 index 000000000000..e25c62926a83 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManagerBase.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Net; +using Microsoft.AspNetCore.Connections; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; + +internal abstract class TransportManagerBase +{ + private readonly List _transports = new List(); + + private readonly ServiceContext _serviceContext; + + protected TransportManagerBase(ServiceContext serviceContext) + { + _serviceContext = serviceContext; + } + + protected KestrelTrace Trace => _serviceContext.Log; + + public abstract bool HasFactories { get; } + + protected static bool CanBindFactory(EndPoint endPoint, IConnectionListenerFactorySelector? selector) + { + // By default, the last registered factory binds to the endpoint. + // A factory can implement IConnectionListenerFactorySelector to decide whether it can bind to the endpoint. + return selector?.CanBind(endPoint) ?? true; + } + + protected void StartAcceptLoop(IConnectionListener connectionListener, Func connectionDelegate, EndpointConfig? endpointConfig) where T : BaseConnectionContext + { + var transportConnectionManager = new TransportConnectionManager(_serviceContext.ConnectionManager); + var connectionDispatcher = new ConnectionDispatcher(_serviceContext, connectionDelegate, transportConnectionManager); + var acceptLoopTask = connectionDispatcher.StartAcceptingConnections(connectionListener); + + _transports.Add(new ActiveTransport(connectionListener, acceptLoopTask, transportConnectionManager, endpointConfig)); + } + + public Task StopEndpointsAsync(List endpointsToStop, CancellationToken cancellationToken) + { + var transportsToStop = new List(); + foreach (var t in _transports) + { + if (t.EndpointConfig is not null && endpointsToStop.Contains(t.EndpointConfig)) + { + transportsToStop.Add(t); + } + } + return StopTransportsAsync(transportsToStop, cancellationToken); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return StopTransportsAsync(new List(_transports), cancellationToken); + } + + private async Task StopTransportsAsync(List transportsToStop, CancellationToken cancellationToken) + { + var tasks = new Task[transportsToStop.Count]; + + for (int i = 0; i < transportsToStop.Count; i++) + { + tasks[i] = transportsToStop[i].UnbindAsync(cancellationToken); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + + async Task StopTransportConnection(ActiveTransport transport) + { + if (!await transport.TransportConnectionManager.CloseAllConnectionsAsync(cancellationToken).ConfigureAwait(false)) + { + Trace.NotAllConnectionsClosedGracefully(); + + if (!await transport.TransportConnectionManager.AbortAllConnectionsAsync().ConfigureAwait(false)) + { + Trace.NotAllConnectionsAborted(); + } + } + } + + for (int i = 0; i < transportsToStop.Count; i++) + { + tasks[i] = StopTransportConnection(transportsToStop[i]); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + + for (int i = 0; i < transportsToStop.Count; i++) + { + tasks[i] = transportsToStop[i].DisposeAsync().AsTask(); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + + foreach (var transport in transportsToStop) + { + _transports.Remove(transport); + } + } + + private sealed class ActiveTransport : IAsyncDisposable + { + public ActiveTransport(IConnectionListenerBase transport, Task acceptLoopTask, TransportConnectionManager transportConnectionManager, EndpointConfig? endpointConfig = null) + { + ConnectionListener = transport; + AcceptLoopTask = acceptLoopTask; + TransportConnectionManager = transportConnectionManager; + EndpointConfig = endpointConfig; + } + + public IConnectionListenerBase ConnectionListener { get; } + public Task AcceptLoopTask { get; } + public TransportConnectionManager TransportConnectionManager { get; } + + public EndpointConfig? EndpointConfig { get; } + + public async Task UnbindAsync(CancellationToken cancellationToken) + { + await ConnectionListener.UnbindAsync(cancellationToken).ConfigureAwait(false); + await AcceptLoopTask.ConfigureAwait(false); + } + + public ValueTask DisposeAsync() + { + return ConnectionListener.DisposeAsync(); + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs index 1861d46434f3..59304ce8191b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs +++ b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs @@ -2,17 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.IO.Pipelines; using System.Linq; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Server.Kestrel.Core; @@ -20,9 +18,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core; internal sealed class KestrelServerImpl : IServer { private readonly ServerAddressesFeature _serverAddresses; - private readonly TransportManager _transportManager; - private readonly List _transportFactories; - private readonly List _multiplexedTransportFactories; + + private readonly IUseHttpsHelper _useHttpsHelper; + + private readonly ITransportManager _transportManager; + private readonly IMultiplexedTransportManager? _multiplexedTransportManager; private readonly SemaphoreSlim _bindSemaphore = new SemaphoreSlim(initialCount: 1); private bool _hasStarted; @@ -33,93 +33,34 @@ internal sealed class KestrelServerImpl : IServer private IDisposable? _configChangedRegistration; public KestrelServerImpl( - IOptions options, - IEnumerable transportFactories, - ILoggerFactory loggerFactory) - : this(transportFactories, Array.Empty(), CreateServiceContext(options, loggerFactory, null)) - { - } - - public KestrelServerImpl( - IOptions options, - IEnumerable transportFactories, - IEnumerable multiplexedFactories, - ILoggerFactory loggerFactory) - : this(transportFactories, multiplexedFactories, CreateServiceContext(options, loggerFactory, null)) + ServiceContext serviceContext, + IUseHttpsHelper useHttpsHelper, + ITransportManager transportManager) + : this(serviceContext, useHttpsHelper, transportManager, multiplexedTransportManager: null) { } public KestrelServerImpl( - IOptions options, - IEnumerable transportFactories, - IEnumerable multiplexedFactories, - ILoggerFactory loggerFactory, - DiagnosticSource diagnosticSource) - : this(transportFactories, multiplexedFactories, CreateServiceContext(options, loggerFactory, diagnosticSource)) - { - } - - // For testing - - internal KestrelServerImpl( - IEnumerable transportFactories, - IEnumerable multiplexedFactories, - ServiceContext serviceContext) + ServiceContext serviceContext, + IUseHttpsHelper useHttpsHelper, + ITransportManager transportManager, + IMultiplexedTransportManager? multiplexedTransportManager) { - ArgumentNullException.ThrowIfNull(transportFactories); - - _transportFactories = transportFactories.Reverse().ToList(); - _multiplexedTransportFactories = multiplexedFactories.Reverse().ToList(); - - if (_transportFactories.Count == 0 && _multiplexedTransportFactories.Count == 0) + if (!transportManager.HasFactories && (multiplexedTransportManager is null || !multiplexedTransportManager.HasFactories)) { throw new InvalidOperationException(CoreStrings.TransportNotFound); } ServiceContext = serviceContext; + _useHttpsHelper = useHttpsHelper; + _transportManager = transportManager; + _multiplexedTransportManager = multiplexedTransportManager; - Features = new FeatureCollection(); _serverAddresses = new ServerAddressesFeature(); Features.Set(_serverAddresses); - - _transportManager = new TransportManager(_transportFactories, _multiplexedTransportFactories, ServiceContext); } - private static ServiceContext CreateServiceContext(IOptions options, ILoggerFactory loggerFactory, DiagnosticSource? diagnosticSource) - { - ArgumentNullException.ThrowIfNull(options); - ArgumentNullException.ThrowIfNull(loggerFactory); - - var serverOptions = options.Value ?? new KestrelServerOptions(); - var trace = new KestrelTrace(loggerFactory); - var connectionManager = new ConnectionManager( - trace, - serverOptions.Limits.MaxConcurrentUpgradedConnections); - - var heartbeatManager = new HeartbeatManager(connectionManager); - var dateHeaderValueManager = new DateHeaderValueManager(); - - var heartbeat = new Heartbeat( - new IHeartbeatHandler[] { dateHeaderValueManager, heartbeatManager }, - new SystemClock(), - DebuggerWrapper.Singleton, - trace); - - return new ServiceContext - { - Log = trace, - Scheduler = PipeScheduler.ThreadPool, - HttpParser = new HttpParser(trace.IsEnabled(LogLevel.Information), serverOptions.DisableHttp1LineFeedTerminators), - SystemClock = heartbeatManager, - DateHeaderValueManager = dateHeaderValueManager, - ConnectionManager = connectionManager, - Heartbeat = heartbeat, - ServerOptions = serverOptions, - DiagnosticSource = diagnosticSource - }; - } - - public IFeatureCollection Features { get; } + public IFeatureCollection Features { get; } = new FeatureCollection(); public KestrelServerOptions Options => ServiceContext.ServerOptions; @@ -180,15 +121,26 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok } } - // Quic isn't registered if it's not supported, throw if we can't fall back to 1 or 2 - if (hasHttp3 && _multiplexedTransportFactories.Count == 0 && !(hasHttp1 || hasHttp2)) + bool haveMultiplexedFactories = _multiplexedTransportManager?.HasFactories == true; + + if (hasHttp3 && !haveMultiplexedFactories) { - throw new InvalidOperationException("This platform doesn't support QUIC or HTTP/3."); + // There will be a IMultiplexedConnectionListenerFactory iff UseQuic was called + if (options.ProtocolsSetExplicitly && Options.ApplicationServices.GetService(typeof(IMultiplexedConnectionListenerFactory)) is null) + { + throw new InvalidOperationException("You need to call UseQuic"); // TODO (acasey): message + } + + // Quic isn't registered if it's not supported, throw if we can't fall back to 1 or 2 + if (!(hasHttp1 || hasHttp2)) + { + throw new InvalidOperationException("This platform doesn't support QUIC or HTTP/3."); + } } // Disable adding alt-svc header if endpoint has configured not to or there is no // multiplexed transport factory, which happens if QUIC isn't supported. - var addAltSvcHeader = !options.DisableAltSvcHeader && _multiplexedTransportFactories.Count > 0; + var addAltSvcHeader = !options.DisableAltSvcHeader && haveMultiplexedFactories; var configuredEndpoint = options.EndPoint; @@ -197,7 +149,7 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok || options.Protocols == HttpProtocols.None) // TODO a test fails because it doesn't throw an exception in the right place // when there is no HttpProtocols in KestrelServer, can we remove/change the test? { - if (_transportFactories.Count == 0) + if (!_transportManager.HasFactories) { throw new InvalidOperationException($"Cannot start HTTP/1.x or HTTP/2 server if no {nameof(IConnectionListenerFactory)} is registered."); } @@ -211,7 +163,7 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok options.EndPoint = await _transportManager.BindAsync(configuredEndpoint, connectionDelegate, options.EndpointConfig, onBindCancellationToken).ConfigureAwait(false); } - if (hasHttp3 && _multiplexedTransportFactories.Count > 0) + if (hasHttp3 && haveMultiplexedFactories) { // Check if a previous transport has changed the endpoint. If it has then the endpoint is dynamic and we can't guarantee it will work for other transports. // For more details, see https://github.com/dotnet/aspnetcore/issues/42982 @@ -227,7 +179,7 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok // Add the connection limit middleware multiplexedConnectionDelegate = EnforceConnectionLimit(multiplexedConnectionDelegate, Options.Limits.MaxConcurrentConnections, Trace); - options.EndPoint = await _transportManager.BindAsync(configuredEndpoint, multiplexedConnectionDelegate, options, onBindCancellationToken).ConfigureAwait(false); + options.EndPoint = await _multiplexedTransportManager!.BindAsync(configuredEndpoint, multiplexedConnectionDelegate, options, onBindCancellationToken).ConfigureAwait(false); } } } @@ -265,6 +217,10 @@ public async Task StopAsync(CancellationToken cancellationToken) try { await _transportManager.StopAsync(cancellationToken).ConfigureAwait(false); + if (_multiplexedTransportManager is not null) + { + await _multiplexedTransportManager.StopAsync(cancellationToken).ConfigureAwait(false); + } } catch (Exception ex) { @@ -314,7 +270,7 @@ private async Task BindAsync(CancellationToken cancellationToken) Options.ConfigurationLoader?.Load(); - await AddressBinder.BindAsync(Options.GetListenOptions(), AddressBindContext!, cancellationToken).ConfigureAwait(false); + await AddressBinder.BindAsync(Options.GetListenOptions(), AddressBindContext!, _useHttpsHelper, cancellationToken).ConfigureAwait(false); _configChangedRegistration = reloadToken?.RegisterChangeCallback(TriggerRebind, this); } finally @@ -370,6 +326,10 @@ private async Task RebindAsync() configsToStop.Add(lo.EndpointConfig!); } await _transportManager.StopEndpointsAsync(configsToStop, combinedCts.Token).ConfigureAwait(false); + if (_multiplexedTransportManager is not null) + { + await _multiplexedTransportManager.StopEndpointsAsync(configsToStop, combinedCts.Token).ConfigureAwait(false); + } foreach (var listenOption in endpointsToStop) { diff --git a/src/Servers/Kestrel/Core/src/Internal/KestrelServerOptionsSetup.cs b/src/Servers/Kestrel/Core/src/Internal/KestrelServerOptionsSetup.cs index 8d8c51d4d08c..fe6ff062d2ec 100644 --- a/src/Servers/Kestrel/Core/src/Internal/KestrelServerOptionsSetup.cs +++ b/src/Servers/Kestrel/Core/src/Internal/KestrelServerOptionsSetup.cs @@ -8,14 +8,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; internal sealed class KestrelServerOptionsSetup : IConfigureOptions { private readonly IServiceProvider _services; + private readonly bool _isHttpsConfigurationEnabled; public KestrelServerOptionsSetup(IServiceProvider services) { _services = services; } + public KestrelServerOptionsSetup(IServiceProvider services, ITlsConfigurationLoader _) + { + _services = services; + _isHttpsConfigurationEnabled = true; + } + public void Configure(KestrelServerOptions options) { options.ApplicationServices = _services; + options.IsHttpsConfigurationEnabled = _isHttpsConfigurationEnabled; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/ServiceContext.cs b/src/Servers/Kestrel/Core/src/Internal/ServiceContext.cs index 689ece3dbf8c..5007dd832394 100644 --- a/src/Servers/Kestrel/Core/src/Internal/ServiceContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/ServiceContext.cs @@ -5,6 +5,8 @@ using System.IO.Pipelines; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; @@ -15,6 +17,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; internal class ServiceContext #pragma warning restore CA1852 // Seal internal types { + // For test subtypes + internal ServiceContext() + { + } + + public ServiceContext( + IOptions options, + ILoggerFactory loggerFactory) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(loggerFactory); + + var serverOptions = options.Value ?? new KestrelServerOptions(); + var trace = new KestrelTrace(loggerFactory); + var connectionManager = new ConnectionManager( + trace, + serverOptions.Limits.MaxConcurrentUpgradedConnections); + + var heartbeatManager = new HeartbeatManager(connectionManager); + var dateHeaderValueManager = new DateHeaderValueManager(); + + var heartbeat = new Heartbeat( + new IHeartbeatHandler[] { dateHeaderValueManager, heartbeatManager }, + new SystemClock(), + DebuggerWrapper.Singleton, + trace); + + Log = trace; + Scheduler = PipeScheduler.ThreadPool; + HttpParser = new HttpParser(trace.IsEnabled(LogLevel.Information), serverOptions.DisableHttp1LineFeedTerminators); + SystemClock = heartbeatManager; + DateHeaderValueManager = dateHeaderValueManager; + ConnectionManager = connectionManager; + Heartbeat = heartbeat; + ServerOptions = serverOptions; + } + + public ServiceContext( + IOptions options, + ILoggerFactory loggerFactory, + DiagnosticSource diagnosticSource) + : this(options, loggerFactory) + { + DiagnosticSource = diagnosticSource; + } + public KestrelTrace Log { get; set; } = default!; public PipeScheduler Scheduler { get; set; } = default!; diff --git a/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs b/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs index 650d3112142e..c1606c0bb743 100644 --- a/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs +++ b/src/Servers/Kestrel/Core/src/Internal/SniOptionsSelector.cs @@ -54,7 +54,7 @@ public SniOptionsSelector( if (sslOptions.ServerCertificate is null) { - if (fallbackHttpsOptions.ServerCertificate is null && _fallbackServerCertificateSelector is null) + if (!fallbackHttpsOptions.HasServerCertificateOrSelector) { throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound); } diff --git a/src/Servers/Kestrel/Core/src/InvalidUseHttpsHelper.cs b/src/Servers/Kestrel/Core/src/InvalidUseHttpsHelper.cs new file mode 100644 index 000000000000..bca1eb5e3919 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/InvalidUseHttpsHelper.cs @@ -0,0 +1,14 @@ +// 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.Server.Kestrel.Core; + +namespace Microsoft.AspNetCore.Hosting; + +internal sealed class InvalidUseHttpsHelper : IUseHttpsHelper // TODO (acasey): name +{ + public ListenOptions UseHttps(ListenOptions listenOptions) + { + throw new InvalidOperationException("You need to call UseHttpsConfiguration"); // TODO (acasey): message + } +} diff --git a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs index 6b519fd56c5b..58f177f4168d 100644 --- a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs +++ b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs @@ -1,21 +1,14 @@ // 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.CodeAnalysis; +using System.Diagnostics; using System.Linq; using System.Net; -using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using Microsoft.AspNetCore.Certificates.Generation; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates; using Microsoft.AspNetCore.Server.Kestrel.Https; -using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel; @@ -24,26 +17,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel; /// public class KestrelConfigurationLoader { + private ITlsConfigurationLoader? _tlsConfigurationLoader; private bool _loaded; internal KestrelConfigurationLoader( KestrelServerOptions options, IConfiguration configuration, - IHostEnvironment hostEnvironment, bool reloadOnChange, - ILogger logger, - ILogger httpsLogger) + ITlsConfigurationLoader? tlsLoader) { - Options = options ?? throw new ArgumentNullException(nameof(options)); - Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - HostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - HttpsLogger = httpsLogger ?? throw new ArgumentNullException(nameof(logger)); - + Options = options; + Configuration = configuration; + ConfigurationReader = new ConfigurationReader(configuration); ReloadOnChange = reloadOnChange; - ConfigurationReader = new ConfigurationReader(configuration); - CertificateConfigLoader = new CertificateConfigLoader(hostEnvironment, logger); + _tlsConfigurationLoader = tlsLoader; + } + + /// + /// A helper for loading TLS-related configuration. + /// + internal ITlsConfigurationLoader? TlsConfigurationLoader + { + get => _tlsConfigurationLoader; + set + { + Debug.Assert(!_loaded); + Debug.Assert(_tlsConfigurationLoader is null); + _tlsConfigurationLoader = value; + } } /// @@ -62,19 +64,13 @@ internal KestrelConfigurationLoader( /// internal bool ReloadOnChange { get; } - private IHostEnvironment HostEnvironment { get; } - private ILogger Logger { get; } - private ILogger HttpsLogger { get; } - private ConfigurationReader ConfigurationReader { get; set; } - private ICertificateConfigLoader CertificateConfigLoader { get; } - - private IDictionary> EndpointConfigurations { get; } + private Dictionary> EndpointConfigurations { get; } = new Dictionary>(0, StringComparer.OrdinalIgnoreCase); // Actions that will be delayed until Load so that they aren't applied if the configuration loader is replaced. - private IList EndpointsToAdd { get; } = new List(); + private List EndpointsToAdd { get; } = new List(); private CertificateConfig? DefaultCertificateConfig { get; set; } internal X509Certificate2? DefaultCertificate { get; set; } @@ -236,11 +232,25 @@ internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions) if (defaults.SslProtocols.HasValue) { + if (_tlsConfigurationLoader is null) + { + // There's no trimming benefit to disabling this, but it would be a confusing + // user experience to allow this but not certificates. + throw new InvalidOperationException("You need to call UseHttpsConfiguration"); // TODO (acasey): message + } + httpsOptions.SslProtocols = defaults.SslProtocols.Value; } if (defaults.ClientCertificateMode.HasValue) { + if (_tlsConfigurationLoader is null) + { + // There's no trimming benefit to disabling this, but it would be a confusing + // user experience to allow this but not certificates. + throw new InvalidOperationException("You need to call UseHttpsConfiguration"); // TODO (acasey): message + } + httpsOptions.ClientCertificateMode = defaults.ClientCertificateMode.Value; } } @@ -278,13 +288,24 @@ public void Load() ConfigurationReader = new ConfigurationReader(Configuration); - LoadDefaultCert(); + if (_tlsConfigurationLoader?.LoadDefaultCertificate(ConfigurationReader) is CertificateAndConfig certPair) + { + DefaultCertificate = certPair.Certificate; + DefaultCertificateConfig = certPair.CertificateConfig; + } foreach (var endpoint in ConfigurationReader.Endpoints) { var listenOptions = AddressBinder.ParseAddress(endpoint.Url, out var https); - if (!https) + if (https) + { + if (_tlsConfigurationLoader is null) + { + throw new InvalidOperationException("You need to call UseHttpsConfiguration"); // TODO (acasey): message + } + } + else { ConfigurationReader.ThrowIfContainsHttpsOnlyConfiguration(endpoint); } @@ -307,42 +328,7 @@ public void Load() if (https) { - // Defaults - Options.ApplyHttpsDefaults(httpsOptions); - - if (endpoint.SslProtocols.HasValue) - { - httpsOptions.SslProtocols = endpoint.SslProtocols.Value; - } - else - { - // Ensure endpoint is reloaded if it used the default protocol and the SslProtocols changed. - endpoint.SslProtocols = ConfigurationReader.EndpointDefaults.SslProtocols; - } - - if (endpoint.ClientCertificateMode.HasValue) - { - httpsOptions.ClientCertificateMode = endpoint.ClientCertificateMode.Value; - } - else - { - // Ensure endpoint is reloaded if it used the default mode and the ClientCertificateMode changed. - endpoint.ClientCertificateMode = ConfigurationReader.EndpointDefaults.ClientCertificateMode; - } - - // A cert specified directly on the endpoint overrides any defaults. - var (serverCert, fullChain) = CertificateConfigLoader.LoadCertificate(endpoint.Certificate, endpoint.Name); - httpsOptions.ServerCertificate = serverCert ?? httpsOptions.ServerCertificate; - httpsOptions.ServerCertificateChain = fullChain ?? httpsOptions.ServerCertificateChain; - - if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null) - { - // Fallback - Options.ApplyDefaultCertificate(httpsOptions); - - // Ensure endpoint is reloaded if it used the default certificate and the certificate changed. - endpoint.Certificate = DefaultCertificateConfig; - } + _tlsConfigurationLoader!.ApplyHttpsDefaults(Options, endpoint, httpsOptions, DefaultCertificateConfig, ConfigurationReader); } // Now that defaults have been loaded, we can compare to the currently bound endpoints to see if the config changed. @@ -370,30 +356,9 @@ public void Load() } // EndpointDefaults or configureEndpoint may have added an https adapter. - if (https && !listenOptions.IsTls) + if (https) { - if (endpoint.Sni.Count == 0) - { - if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null) - { - throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound); - } - - listenOptions.UseHttps(httpsOptions); - } - else - { - var sniOptionsSelector = new SniOptionsSelector(endpoint.Name, endpoint.Sni, CertificateConfigLoader, - httpsOptions, listenOptions.Protocols, HttpsLogger); - var tlsCallbackOptions = new TlsHandshakeCallbackOptions() - { - OnConnection = SniOptionsSelector.OptionsCallback, - HandshakeTimeout = httpsOptions.HandshakeTimeout, - OnConnectionState = sniOptionsSelector, - }; - - listenOptions.UseHttps(tlsCallbackOptions); - } + _tlsConfigurationLoader!.UseHttps(listenOptions, endpoint, httpsOptions); } listenOptions.EndpointConfig = endpoint; @@ -411,87 +376,4 @@ public void Load() return (endpointsToStop, endpointsToStart); } - - private void LoadDefaultCert() - { - if (ConfigurationReader.Certificates.TryGetValue("Default", out var defaultCertConfig)) - { - var (defaultCert, _ /* cert chain */) = CertificateConfigLoader.LoadCertificate(defaultCertConfig, "Default"); - if (defaultCert != null) - { - DefaultCertificateConfig = defaultCertConfig; - DefaultCertificate = defaultCert; - } - } - else - { - var (certificate, certificateConfig) = FindDeveloperCertificateFile(); - if (certificate != null) - { - Logger.LocatedDevelopmentCertificate(certificate); - DefaultCertificateConfig = certificateConfig; - DefaultCertificate = certificate; - } - } - } - - private (X509Certificate2?, CertificateConfig?) FindDeveloperCertificateFile() - { - string? certificatePath = null; - if (ConfigurationReader.Certificates.TryGetValue("Development", out var certificateConfig) && - certificateConfig.Path == null && - certificateConfig.Password != null && - TryGetCertificatePath(out certificatePath) && - File.Exists(certificatePath)) - { - try - { - var certificate = new X509Certificate2(certificatePath, certificateConfig.Password); - - if (IsDevelopmentCertificate(certificate)) - { - return (certificate, certificateConfig); - } - } - catch (CryptographicException) - { - Logger.FailedToLoadDevelopmentCertificate(certificatePath); - } - } - else if (!string.IsNullOrEmpty(certificatePath)) - { - Logger.FailedToLocateDevelopmentCertificateFile(certificatePath); - } - - return (null, null); - } - - private static bool IsDevelopmentCertificate(X509Certificate2 certificate) - { - if (!string.Equals(certificate.Subject, "CN=localhost", StringComparison.Ordinal)) - { - return false; - } - - foreach (var ext in certificate.Extensions) - { - if (string.Equals(ext.Oid?.Value, CertificateManager.AspNetHttpsOid, StringComparison.Ordinal)) - { - return true; - } - } - - return false; - } - - private bool TryGetCertificatePath([NotNullWhen(true)] out string? path) - { - // See https://github.com/aspnet/Hosting/issues/1294 - var appData = Environment.GetEnvironmentVariable("APPDATA"); - var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var basePath = appData != null ? Path.Combine(appData, "ASP.NET", "https") : null; - basePath = basePath ?? (home != null ? Path.Combine(home, ".aspnet", "https") : null); - path = basePath != null ? Path.Combine(basePath, $"{HostEnvironment.ApplicationName}.pfx") : null; - return path != null; - } } diff --git a/src/Servers/Kestrel/Core/src/KestrelServer.cs b/src/Servers/Kestrel/Core/src/KestrelServer.cs index f403143c2569..c844cca1a806 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServer.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServer.cs @@ -2,8 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -24,10 +27,13 @@ public class KestrelServer : IServer /// The . public KestrelServer(IOptions options, IConnectionListenerFactory transportFactory, ILoggerFactory loggerFactory) { + ArgumentNullException.ThrowIfNull(transportFactory); + + var serviceContext = new ServiceContext(options, loggerFactory); _innerKestrelServer = new KestrelServerImpl( - options, - new[] { transportFactory ?? throw new ArgumentNullException(nameof(transportFactory)) }, - loggerFactory); + serviceContext, + new UseHttpsHelper(), + new TransportManager(serviceContext, new[] { transportFactory })); } /// diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index 6bfeaef62115..1c979e5f07a4 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -217,6 +217,15 @@ internal bool DisableHttp1LineFeedTerminators set => _disableHttp1LineFeedTerminators = value; } + /// + /// If true, the , if any, and the the + /// will be checked for a default certificate. + /// + /// + /// Defaults to false. + /// + internal bool IsHttpsConfigurationEnabled { get; set; } + /// /// Specifies a configuration Action to run for each newly created endpoint. Calling this again will replace /// the prior action. @@ -244,15 +253,18 @@ public void ConfigureHttpsDefaults(Action configu internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions) { + // If there is a configuration loader, the configuration provides defaults, + // and the loader does not support https, it will throw. + // Otherwise, we should be fine using whatever the user configured. ConfigurationLoader?.ApplyHttpsDefaults(httpsOptions); HttpsDefaults(httpsOptions); } internal void ApplyDefaultCertificate(HttpsConnectionAdapterOptions httpsOptions) { - if (httpsOptions.ServerCertificate != null || httpsOptions.ServerCertificateSelector != null) + if (!IsHttpsConfigurationEnabled && ConfigurationLoader?.TlsConfigurationLoader is null) { - return; + throw new InvalidOperationException("You need to call UseHttpsConfiguration"); // TODO (acasey): message } if (TestOverrideDefaultCertificate is X509Certificate2 certificateFromTest) @@ -392,15 +404,40 @@ public KestrelConfigurationLoader Configure(IConfiguration config, bool reloadOn throw new InvalidOperationException($"{nameof(ApplicationServices)} must not be null. This is normally set automatically via {nameof(IConfigureOptions)}."); } - var hostEnvironment = ApplicationServices.GetRequiredService(); - var logger = ApplicationServices.GetRequiredService>(); - var httpsLogger = ApplicationServices.GetRequiredService>(); + _tlsConfigurationLoader ??= ApplicationServices.GetService(); - var loader = new KestrelConfigurationLoader(this, config, hostEnvironment, reloadOnChange, logger, httpsLogger); + // Why not reuse EnableTlsConfigurationLoading here? Because it constructs a TlsConfigurationLoader, + // which prevents trimming of TLS support code, so we can't call it here, even conditionally. + var loader = new KestrelConfigurationLoader(this, config, reloadOnChange, _tlsConfigurationLoader); ConfigurationLoader = loader; return loader; } + private ITlsConfigurationLoader? _tlsConfigurationLoader; + internal void EnableTlsConfigurationLoading() + { + if (_tlsConfigurationLoader is not null) + { + return; + } + + if (ApplicationServices is null) + { + throw new InvalidOperationException($"{nameof(ApplicationServices)} must not be null. This is normally set automatically via {nameof(IConfigureOptions)}."); + } + + var hostEnvironment = ApplicationServices.GetRequiredService(); + var serverLogger = ApplicationServices.GetRequiredService>(); + var httpsLogger = ApplicationServices.GetRequiredService>(); + + _tlsConfigurationLoader = new TlsConfigurationLoader(hostEnvironment, serverLogger, httpsLogger); + + if (ConfigurationLoader is not null) + { + ConfigurationLoader.TlsConfigurationLoader = _tlsConfigurationLoader; + } + } + /// /// Bind to the given IP address and port. /// diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index 3463a56828e7..ace0c149323b 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -163,33 +163,25 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, Action diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs index 6b70b379e191..f0fea7b93c1c 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs @@ -56,7 +56,7 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter { ArgumentNullException.ThrowIfNull(options); - if (options.ServerCertificate == null && options.ServerCertificateSelector == null) + if (!options.HasServerCertificateOrSelector) { throw new ArgumentException(CoreStrings.ServerCertificateRequired, nameof(options)); } diff --git a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt index 5a2c7ffb53b1..a91eb782e434 100644 --- a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt @@ -3,4 +3,4 @@ Microsoft.AspNetCore.Server.Kestrel.Core.Features.ISslStreamFeature Microsoft.AspNetCore.Server.Kestrel.Core.Features.ISslStreamFeature.SslStream.get -> System.Net.Security.SslStream! Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ListenNamedPipe(string! pipeName) -> void Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ListenNamedPipe(string! pipeName, System.Action! configure) -> void -Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions.PipeName.get -> string? \ No newline at end of file +Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions.PipeName.get -> string? diff --git a/src/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs new file mode 100644 index 000000000000..49ea6e48b6ad --- /dev/null +++ b/src/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs @@ -0,0 +1,193 @@ +// 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.CodeAnalysis; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Certificates.Generation; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.Kestrel; + +internal sealed class TlsConfigurationLoader : ITlsConfigurationLoader +{ + private readonly ICertificateConfigLoader _certificateConfigLoader; + private readonly string _applicationName; + private readonly ILogger _serverLogger; + private readonly ILogger _httpsLogger; + + public TlsConfigurationLoader( + IHostEnvironment hostEnvironment, + ILogger serverLogger, + ILogger httpsLogger) + { + _certificateConfigLoader = new CertificateConfigLoader(hostEnvironment, serverLogger); + _applicationName = hostEnvironment.ApplicationName; + _serverLogger = serverLogger; + _httpsLogger = httpsLogger; + } + + public void ApplyHttpsDefaults( // TODO (acasey): name + KestrelServerOptions serverOptions, + EndpointConfig endpoint, + HttpsConnectionAdapterOptions httpsOptions, + CertificateConfig? defaultCertificateConfig, + ConfigurationReader configurationReader) + { + serverOptions.ApplyHttpsDefaults(httpsOptions); + + if (endpoint.SslProtocols.HasValue) + { + httpsOptions.SslProtocols = endpoint.SslProtocols.Value; + } + else + { + // Ensure endpoint is reloaded if it used the default protocol and the SslProtocols changed. + endpoint.SslProtocols = configurationReader.EndpointDefaults.SslProtocols; + } + + if (endpoint.ClientCertificateMode.HasValue) + { + httpsOptions.ClientCertificateMode = endpoint.ClientCertificateMode.Value; + } + else + { + // Ensure endpoint is reloaded if it used the default mode and the ClientCertificateMode changed. + endpoint.ClientCertificateMode = configurationReader.EndpointDefaults.ClientCertificateMode; + } + + // A cert specified directly on the endpoint overrides any defaults. + var (serverCert, fullChain) = _certificateConfigLoader.LoadCertificate(endpoint.Certificate, endpoint.Name); + httpsOptions.ServerCertificate = serverCert ?? httpsOptions.ServerCertificate; + httpsOptions.ServerCertificateChain = fullChain ?? httpsOptions.ServerCertificateChain; + + if (!httpsOptions.HasServerCertificateOrSelector) + { + // Fallback + serverOptions.ApplyDefaultCertificate(httpsOptions); + + // Ensure endpoint is reloaded if it used the default certificate and the certificate changed. + endpoint.Certificate = defaultCertificateConfig; + } + } + + public void UseHttps( // TODO (acasey): name + ListenOptions listenOptions, + EndpointConfig endpoint, + HttpsConnectionAdapterOptions httpsOptions) + { + if (listenOptions.IsTls) + { + return; + } + + if (endpoint.Sni.Count == 0) + { + if (!httpsOptions.HasServerCertificateOrSelector) + { + throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound); + } + + listenOptions.UseHttps(httpsOptions); + } + else + { + var sniOptionsSelector = new SniOptionsSelector(endpoint.Name, endpoint.Sni, _certificateConfigLoader, + httpsOptions, listenOptions.Protocols, _httpsLogger); + var tlsCallbackOptions = new TlsHandshakeCallbackOptions() + { + OnConnection = SniOptionsSelector.OptionsCallback, + HandshakeTimeout = httpsOptions.HandshakeTimeout, + OnConnectionState = sniOptionsSelector, + }; + + listenOptions.UseHttps(tlsCallbackOptions); + } + } + + public CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader) + { + if (configurationReader.Certificates.TryGetValue("Default", out var defaultCertConfig)) + { + var (defaultCert, _ /* cert chain */) = _certificateConfigLoader.LoadCertificate(defaultCertConfig, "Default"); + if (defaultCert != null) + { + return new CertificateAndConfig(defaultCert, defaultCertConfig); + } + } + else if (FindDeveloperCertificateFile(configurationReader) is CertificateAndConfig pair) + { + _serverLogger.LocatedDevelopmentCertificate(pair.Certificate); + return pair; + } + + return null; + } + + private CertificateAndConfig? FindDeveloperCertificateFile(ConfigurationReader configurationReader) + { + string? certificatePath = null; + if (configurationReader.Certificates.TryGetValue("Development", out var certificateConfig) && + certificateConfig.Path == null && + certificateConfig.Password != null && + TryGetCertificatePath(_applicationName, out certificatePath) && + File.Exists(certificatePath)) + { + try + { + var certificate = new X509Certificate2(certificatePath, certificateConfig.Password); + + if (IsDevelopmentCertificate(certificate)) + { + return new CertificateAndConfig(certificate, certificateConfig); + } + } + catch (CryptographicException) + { + _serverLogger.FailedToLoadDevelopmentCertificate(certificatePath); + } + } + else if (!string.IsNullOrEmpty(certificatePath)) + { + _serverLogger.FailedToLocateDevelopmentCertificateFile(certificatePath); + } + + return null; + } + + private static bool IsDevelopmentCertificate(X509Certificate2 certificate) + { + if (!string.Equals(certificate.Subject, "CN=localhost", StringComparison.Ordinal)) + { + return false; + } + + foreach (var ext in certificate.Extensions) + { + if (string.Equals(ext.Oid?.Value, CertificateManager.AspNetHttpsOid, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + private static bool TryGetCertificatePath(string applicationName, [NotNullWhen(true)] out string? path) + { + // See https://github.com/aspnet/Hosting/issues/1294 + var appData = Environment.GetEnvironmentVariable("APPDATA"); + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var basePath = appData != null ? Path.Combine(appData, "ASP.NET", "https") : null; + basePath = basePath ?? (home != null ? Path.Combine(home, ".aspnet", "https") : null); + path = basePath != null ? Path.Combine(basePath, $"{applicationName}.pfx") : null; + return path != null; + } +} diff --git a/src/Servers/Kestrel/Core/src/UseHttpsHelper.cs b/src/Servers/Kestrel/Core/src/UseHttpsHelper.cs new file mode 100644 index 000000000000..4aef86374cbb --- /dev/null +++ b/src/Servers/Kestrel/Core/src/UseHttpsHelper.cs @@ -0,0 +1,14 @@ +// 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.Server.Kestrel.Core; + +namespace Microsoft.AspNetCore.Hosting; + +internal sealed class UseHttpsHelper : IUseHttpsHelper // TODO (acasey): name +{ + public ListenOptions UseHttps(ListenOptions listenOptions) + { + return listenOptions.UseHttps(); + } +} diff --git a/src/Servers/Kestrel/Core/test/AddressBinderTests.cs b/src/Servers/Kestrel/Core/test/AddressBinderTests.cs index 4a0cd1602315..f62fa7b3b916 100644 --- a/src/Servers/Kestrel/Core/test/AddressBinderTests.cs +++ b/src/Servers/Kestrel/Core/test/AddressBinderTests.cs @@ -23,6 +23,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; public class AddressBinderTests { + private static readonly IUseHttpsHelper UseHttpsHelper = new UseHttpsHelper(); + [Theory] [InlineData("http://10.10.10.10:5000/", "10.10.10.10", 5000)] [InlineData("http://[::1]:5000", "::1", 5000)] @@ -172,7 +174,7 @@ public async Task WrapsAddressInUseExceptionAsIOException() endpoint => throw new AddressInUseException("already in use")); await Assert.ThrowsAsync(() => - AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None)); + AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, UseHttpsHelper, CancellationToken.None)); } [Fact] @@ -193,7 +195,7 @@ public void LogsWarningWhenHostingAddressesAreOverridden() logger, endpoint => Task.CompletedTask); - var bindTask = AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None); + var bindTask = AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, UseHttpsHelper, CancellationToken.None); Assert.True(bindTask.IsCompletedSuccessfully); var log = Assert.Single(logger.Messages); @@ -221,7 +223,7 @@ public void LogsInformationWhenKestrelAddressesAreOverridden() addressBindContext.ServerAddressesFeature.PreferHostingUrls = true; - var bindTask = AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None); + var bindTask = AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, UseHttpsHelper, CancellationToken.None); Assert.True(bindTask.IsCompletedSuccessfully); var log = Assert.Single(logger.Messages); @@ -247,7 +249,7 @@ public async Task FlowsCancellationTokenToCreateBinddingCallback() }); await Assert.ThrowsAsync(() => - AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, new CancellationToken(true))); + AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, UseHttpsHelper, new CancellationToken(true))); } [Theory] @@ -284,7 +286,7 @@ public async Task FallbackToIPv4WhenIPv6AnyBindFails(string address) return Task.CompletedTask; }); - await AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None); + await AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, UseHttpsHelper, CancellationToken.None); Assert.True(ipV4Attempt, "Should have attempted to bind to IPAddress.Any"); Assert.True(ipV6Attempt, "Should have attempted to bind to IPAddress.IPv6Any"); @@ -315,7 +317,7 @@ public async Task DefaultAddressBinderBindsToHttpPort5000() return Task.CompletedTask; }); - await AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None); + await AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, UseHttpsHelper, CancellationToken.None); Assert.Contains(endpoints, e => e.IPEndPoint.Port == 5000 && !e.IsTls); } diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index 911fc0524109..d86e7c13bc0a 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; @@ -282,7 +283,7 @@ public void ConstructorWithNullTransportFactoryThrows() public void ConstructorWithNoTransportFactoriesThrows() { var exception = Assert.Throws(() => - new KestrelServerImpl( + MakeKestrelServerImpl( Options.Create(null), new List(), new LoggerFactory(new[] { new KestrelTestLoggerProvider() }))); @@ -293,7 +294,7 @@ public void ConstructorWithNoTransportFactoriesThrows() [Fact] public void StartWithMultipleTransportFactoriesDoesNotThrow() { - using var server = new KestrelServerImpl( + using var server = MakeKestrelServerImpl( Options.Create(CreateServerOptions()), new List() { new ThrowingTransportFactory(), new MockTransportFactory() }, new LoggerFactory(new[] { new KestrelTestLoggerProvider() })); @@ -307,7 +308,7 @@ public async Task StartWithNoValidTransportFactoryThrows() var serverOptions = CreateServerOptions(); serverOptions.Listen(new IPEndPoint(IPAddress.Loopback, 0)); - var server = new KestrelServerImpl( + var server = MakeKestrelServerImpl( Options.Create(serverOptions), new List { new NonBindableTransportFactory() }, new LoggerFactory(new[] { new KestrelTestLoggerProvider() })); @@ -327,7 +328,7 @@ public async Task StartWithMultipleTransportFactories_UseSupported() var transportFactory = new MockTransportFactory(); - var server = new KestrelServerImpl( + var server = MakeKestrelServerImpl( Options.Create(serverOptions), new List { transportFactory, new NonBindableTransportFactory() }, new LoggerFactory(new[] { new KestrelTestLoggerProvider() })); @@ -348,7 +349,7 @@ public async Task StartWithNoValidTransportFactoryThrows_Http3() c.UseHttps(TestResources.GetTestCertificate()); }); - var server = new KestrelServerImpl( + var server = MakeKestrelServerImpl( Options.Create(serverOptions), new List(), new List { new NonBindableMultiplexedTransportFactory() }, @@ -373,7 +374,7 @@ public async Task StartWithMultipleTransportFactories_Http3_UseSupported() var transportFactory = new MockMultiplexedTransportFactory(); - var server = new KestrelServerImpl( + var server = MakeKestrelServerImpl( Options.Create(serverOptions), new List(), new List { transportFactory, new NonBindableMultiplexedTransportFactory() }, @@ -403,7 +404,7 @@ public async Task ListenWithCustomEndpoint_DoesNotThrow() var mockTransportFactory = new MockTransportFactory(); var mockMultiplexedTransportFactory = new MockMultiplexedTransportFactory(); - using var server = new KestrelServerImpl( + using var server = MakeKestrelServerImpl( Options.Create(options), new List() { mockTransportFactory }, new List() { mockMultiplexedTransportFactory }, @@ -434,7 +435,7 @@ public async Task ListenIPWithStaticPort_TransportsGetIPv6Any() var mockTransportFactory = new MockTransportFactory(); var mockMultiplexedTransportFactory = new MockMultiplexedTransportFactory(); - using var server = new KestrelServerImpl( + using var server = MakeKestrelServerImpl( Options.Create(options), new List() { mockTransportFactory }, new List() { mockMultiplexedTransportFactory }, @@ -469,7 +470,7 @@ public async Task ListenIPWithEphemeralPort_TransportsGetIPv6Any() var mockTransportFactory = new MockTransportFactory(); var mockMultiplexedTransportFactory = new MockMultiplexedTransportFactory(); - using var server = new KestrelServerImpl( + using var server = MakeKestrelServerImpl( Options.Create(options), new List() { mockTransportFactory }, new List() { mockMultiplexedTransportFactory }, @@ -501,7 +502,7 @@ public async Task ListenIPWithEphemeralPort_MultiplexedTransportsGetIPv6Any() var mockTransportFactory = new MockTransportFactory(); var mockMultiplexedTransportFactory = new MockMultiplexedTransportFactory(); - using var server = new KestrelServerImpl( + using var server = MakeKestrelServerImpl( Options.Create(options), new List() { mockTransportFactory }, new List() { mockMultiplexedTransportFactory }, @@ -714,7 +715,7 @@ public void StartingServerInitializesHeartbeat() DebuggerWrapper.Singleton, testContext.Log); - using (var server = new KestrelServerImpl(new[] { new MockTransportFactory() }, Array.Empty(), testContext)) + using (var server = new KestrelServerImpl(testContext, new UseHttpsHelper(), new TransportManager(testContext, new[] { new MockTransportFactory() }))) { Assert.Null(testContext.DateHeaderValueManager.GetDateHeaderValues()); @@ -1036,5 +1037,25 @@ public ValueTask BindAsync(EndPoint endpoint, IF } } + private static KestrelServerImpl MakeKestrelServerImpl( + IOptions options, + IEnumerable factories, + ILoggerFactory loggerFactory) + { + return MakeKestrelServerImpl(options, factories, multiplexedFactories: null, loggerFactory); + } + + private static KestrelServerImpl MakeKestrelServerImpl( + IOptions options, + IEnumerable factories, + IEnumerable multiplexedFactories, + ILoggerFactory loggerFactory) + { + var serviceContext = new ServiceContext(options, loggerFactory); + var transportManager = new TransportManager(serviceContext, factories); + var multiplexedTransportManager = new MultiplexedTransportManager(serviceContext, multiplexedFactories ?? Array.Empty()); + return new KestrelServerImpl(serviceContext, new UseHttpsHelper(), transportManager, multiplexedTransportManager); + } + private record BindDetail(EndPoint OriginalEndPoint, EndPoint BoundEndPoint); } diff --git a/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..c5e2c49c027b 100644 --- a/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseHttpsConfiguration(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelSlim(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! diff --git a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index 7d2b4d5bfb7e..8822c4fad849 100644 --- a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs +++ b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs @@ -4,8 +4,10 @@ using System.Net.Http; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -18,6 +20,21 @@ namespace Microsoft.AspNetCore.Hosting; /// public static class WebHostBuilderKestrelExtensions { + /// + /// TODO (acasey): doc + /// + /// + /// + public static IWebHostBuilder UseHttpsConfiguration(this IWebHostBuilder hostBuilder) + { + return hostBuilder.ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }); + } + /// /// Specify Kestrel as the server to be used by the web host. /// @@ -28,6 +45,30 @@ public static class WebHostBuilderKestrelExtensions /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder. /// public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder) + { + return UseKestrelSlim(hostBuilder) + .UseQuic(options => + { + // Configure server defaults to match client defaults. + // https://github.com/dotnet/runtime/blob/a5f3676cc71e176084f0f7f1f6beeecd86fbeafc/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs#L118-L119 + options.DefaultStreamErrorCode = (long)Http3ErrorCode.RequestCancelled; + options.DefaultCloseErrorCode = (long)Http3ErrorCode.NoError; + }) + .UseHttpsConfiguration(); + } + + // TODO (acasey): comment + /// + /// Specify Kestrel as the server to be used by the web host. + /// Quic support will not be configured, regardless of other settings. + /// + /// + /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder to configure. + /// + /// + /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder. + /// + public static IWebHostBuilder UseKestrelSlim(this IWebHostBuilder hostBuilder) { hostBuilder.ConfigureServices(services => { @@ -35,15 +76,12 @@ public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder) services.TryAddSingleton(); services.AddTransient, KestrelServerOptionsSetup>(); + + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); - }); - hostBuilder.UseQuic(options => - { - // Configure server defaults to match client defaults. - // https://github.com/dotnet/runtime/blob/a5f3676cc71e176084f0f7f1f6beeecd86fbeafc/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs#L118-L119 - options.DefaultStreamErrorCode = (long)Http3ErrorCode.RequestCancelled; - options.DefaultCloseErrorCode = (long)Http3ErrorCode.NoError; + services.AddSingleton(); }); if (OperatingSystem.IsWindows()) diff --git a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs index c6a373cf7f88..926ad192b21d 100644 --- a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs @@ -24,6 +24,7 @@ private KestrelServerOptions CreateServerOptions() serverOptions.ApplicationServices = new ServiceCollection() .AddLogging() .AddSingleton(env) + .AddSingleton() .BuildServiceProvider(); return serverOptions; } diff --git a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs index deb0ed40e823..23d2ea137841 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs @@ -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.Diagnostics; using System.Net; +using System.Net.Quic; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal; @@ -38,6 +40,7 @@ public QuicTransportFactory(ILoggerFactory loggerFactory, IOptions BindAsync(EndPoint endpoint, IFeatureCollection? features = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(endpoint); + Debug.Assert(CanBind(endpoint)); var tlsConnectionOptions = features?.Get(); @@ -58,6 +61,6 @@ public async ValueTask BindAsync(EndPoint endpoi public bool CanBind(EndPoint endpoint) { - return endpoint is IPEndPoint; + return endpoint is IPEndPoint && QuicListener.IsSupported; } } diff --git a/src/Servers/Kestrel/Transport.Quic/src/WebHostBuilderQuicExtensions.cs b/src/Servers/Kestrel/Transport.Quic/src/WebHostBuilderQuicExtensions.cs index ad59ed4f999b..2f4b4cfc546b 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/WebHostBuilderQuicExtensions.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/WebHostBuilderQuicExtensions.cs @@ -1,7 +1,6 @@ // 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.Quic; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic; using Microsoft.Extensions.DependencyInjection; @@ -20,15 +19,11 @@ public static class WebHostBuilderQuicExtensions /// The . public static IWebHostBuilder UseQuic(this IWebHostBuilder hostBuilder) { - if (QuicListener.IsSupported) + return hostBuilder.ConfigureServices(services => { - return hostBuilder.ConfigureServices(services => - { - services.AddSingleton(); - }); - } - - return hostBuilder; + // CanBind will return false if QuicListener.IsSupported is false + services.AddSingleton(); + }); } /// diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs index 5d73e667c5a3..4ba713116dcf 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -94,7 +95,11 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action(), Array.Empty(), context); + return new KestrelServerImpl( + context, + new UseHttpsHelper(), + new TransportManager(context, sp.GetServices()), + new MultiplexedTransportManager(context, Array.Empty())); }); configureServices(services); }) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index 749f06be5fe4..3d4e48e0c3dd 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -70,7 +70,7 @@ public async Task CanReadAndWriteWithHttpsConnectionMiddlewareWithPemCertificate var logger = serviceProvider.GetRequiredService>(); var httpsLogger = serviceProvider.GetRequiredService>(); - var loader = new KestrelConfigurationLoader(options, configuration, env.Object, reloadOnChange: false, logger, httpsLogger); + var loader = new KestrelConfigurationLoader(options, configuration, reloadOnChange: false, new TlsConfigurationLoader(env.Object, logger, httpsLogger)); options.ConfigurationLoader = loader; // Since we're constructing it explicitly, we have to hook it up explicitly loader.Load(); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs index 27f51aed3faa..58816af21101 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs @@ -4397,7 +4397,7 @@ await connection.Receive( } [Fact] - public async Task AltSvc_Http1And2And3EndpointConfigured_NoMultiplexedFactory_NoAltSvcInResponseHeaders() + public async Task AltSvc_Http1And2And3EndpointConfigured_NonBindableMultiplexedFactory_NoAltSvcInResponseHeaders() { await using (var server = new TestServer( httpContext => Task.CompletedTask, @@ -4410,7 +4410,9 @@ public async Task AltSvc_Http1And2And3EndpointConfigured_NoMultiplexedFactory_No IsTls = true }); }, - services => { })) + services => { + services.AddSingleton(new NonBindableMultiplexedTransportFactory()); + })) { using (var connection = server.CreateConnection()) { @@ -4429,6 +4431,14 @@ await connection.Receive( } } + private class NonBindableMultiplexedTransportFactory : IMultiplexedConnectionListenerFactory, IConnectionListenerFactorySelector + { + ValueTask IMultiplexedConnectionListenerFactory.BindAsync(EndPoint endpoint, IFeatureCollection features, CancellationToken cancellationToken) => + throw new InvalidOperationException(); + + bool IConnectionListenerFactorySelector.CanBind(EndPoint endpoint) => false; + } + [Fact] public async Task AltSvc_Http1_NoAltSvcInResponseHeaders() { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs index a9e22498e3a1..f8e03a6283ca 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -92,9 +93,10 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action(), - context); + context, + new UseHttpsHelper(), + new TransportManager(context, new IConnectionListenerFactory[] { _transportFactory }), + new MultiplexedTransportManager(context, sp.GetServices())); }); });