From d1f386d177f2e53e3f47b5be00c6b054a5b6de72 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Mon, 27 Feb 2023 14:34:33 -0800 Subject: [PATCH 1/8] Make TLS & QUIC Pay-for-Play This draft PR is intended as context for a discussion and is not ready to commit. As discussed offline, the way to think about disabling TLS is that Kestrel won't do things automatically for you, but there are things you can do yourself _even without opting in_. I think of it as disabling TLS _configuration_ - Kestrel won't call `UseHttps` on your behalf (e.g. because you have specified an https address) or load certificates from `IConfiguration` or `CertificateManager`. However, you remain free to specify a certificate in code and call `UseHttps` explicitly. This is enough to allow the cryptography types to be trimmed and seems more defensible than having Kestrel throw whenever you get to close to TLS. In contrast, QUIC support is either on or off. Opting out effectively means that Kestrel won't call `UseQuic` on your behalf. Big changes: 1. Introduce `UseKestrelSlim`, which doesn't use QUIC or TLS configuration. 1. Introduce `UseHttpsConfiguration`, which opts in to TLS configuration (QUIC uses the existing `UseQuic` extension method). 1. Split out the TLS portion of `KestrelConfigurationLoader` into a DI component, `ITlsConfigurationLoader`. 1. Break `AddressBinder`'s direct dependency on `UseHttps` (called for addresses that parse as https). 1. Split `TransportManager` into multiplexed and non-multiplexed DI components. 1. Make `KestrelServerImpl` a DI component. 1. Make `ServiceContext` a DI component. 1. Throw when a failure is caused by a missing opt-in. (In particular, distinguish between lack of QUIC support and lack of QUIC opt-in.) Open Questions: 1. Is there a better way to have `UseQuic` and `UseHttpsConfiguration` coordinate across assembly boundaries than introducing types like `MultiplexedConnectionMarkerService`? 1. Is this a sensible place to cut off TLS support? Originally based on @eerhardt's https://github.com/dotnet/aspnetcore/commit/bffa15c0f165b6530d56ebb77f8c839cbda203f0 --- src/DefaultBuilder/src/WebHost.cs | 2 +- .../src/MultiplexedConnectionMarkerService.cs | 18 ++ .../PublicAPI/net462/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 2 + .../netstandard2.0/PublicAPI.Unshipped.txt | 2 + .../netstandard2.1/PublicAPI.Unshipped.txt | 2 + .../Core/src/HttpsConnectionAdapterOptions.cs | 5 + .../Core/src/ITlsConfigurationLoader.cs | 25 ++ .../Kestrel/Core/src/IUseHttpsHelper.cs | 11 + .../Core/src/Internal/AddressBinder.cs | 26 +- .../IMultiplexedTransportManager.cs | 19 ++ .../Infrastructure/ITransportManager.cs | 19 ++ .../MultiplexedTransportManager.cs | 148 ++++++++++ .../Infrastructure/TransportManager.cs | 259 +----------------- .../Infrastructure/TransportManagerBase.cs | 131 +++++++++ .../Core/src/Internal/KestrelServerImpl.cs | 131 +++------ .../src/Internal/KestrelServerOptionsSetup.cs | 8 + .../Core/src/Internal/ServiceContext.cs | 48 ++++ .../Core/src/Internal/SniOptionsSelector.cs | 2 +- .../Kestrel/Core/src/InvalidUseHttpsHelper.cs | 14 + .../Core/src/KestrelConfigurationLoader.cs | 209 +++----------- src/Servers/Kestrel/Core/src/KestrelServer.cs | 12 +- .../Kestrel/Core/src/KestrelServerOptions.cs | 21 +- .../Core/src/ListenOptionsHttpsExtensions.cs | 26 +- .../Middleware/HttpsConnectionMiddleware.cs | 2 +- .../Kestrel/Core/src/PublicAPI.Unshipped.txt | 2 +- .../Core/src/TlsConfigurationLoader.cs | 193 +++++++++++++ .../Kestrel/Core/src/UseHttpsHelper.cs | 14 + .../Kestrel/Core/test/AddressBinderTests.cs | 14 +- .../Kestrel/Core/test/KestrelServerTests.cs | 44 ++- .../Kestrel/src/PublicAPI.Unshipped.txt | 2 + .../src/WebHostBuilderKestrelExtensions.cs | 52 +++- .../test/KestrelConfigurationLoaderTests.cs | 1 + .../src/WebHostBuilderQuicExtensions.cs | 7 + .../test/TransportTestHelpers/TestServer.cs | 7 +- .../HttpsConnectionMiddlewareTests.cs | 2 +- .../TestTransport/TestServer.cs | 11 +- 37 files changed, 922 insertions(+), 571 deletions(-) create mode 100644 src/Servers/Connections.Abstractions/src/MultiplexedConnectionMarkerService.cs create mode 100644 src/Servers/Kestrel/Core/src/ITlsConfigurationLoader.cs create mode 100644 src/Servers/Kestrel/Core/src/IUseHttpsHelper.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Infrastructure/IMultiplexedTransportManager.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITransportManager.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Infrastructure/MultiplexedTransportManager.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManagerBase.cs create mode 100644 src/Servers/Kestrel/Core/src/InvalidUseHttpsHelper.cs create mode 100644 src/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs create mode 100644 src/Servers/Kestrel/Core/src/UseHttpsHelper.cs 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/MultiplexedConnectionMarkerService.cs b/src/Servers/Connections.Abstractions/src/MultiplexedConnectionMarkerService.cs new file mode 100644 index 000000000000..3c3a583f0408 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/MultiplexedConnectionMarkerService.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Connections; + +/// +/// A marker class used to determine if QUIC support was requested, +/// regardless or whether or not it is supported. +/// TODO (acasey): can we make this non-public? +/// +public sealed class MultiplexedConnectionMarkerService +{ +} 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..fd6460b1cd0c 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -3,6 +3,8 @@ Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature.NamedPipe.get -> System.IO.Pipes.NamedPipeServerStream! Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool +Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService +Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService.MultiplexedConnectionMarkerService() -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName) -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName, string! serverName) -> void diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 3ddff4e1b1ec..eeed277ba2fc 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -4,6 +4,8 @@ Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature.NamedPipe. Microsoft.AspNetCore.Connections.Features.ITlsHandshakeFeature.NegotiatedCipherSuite.get -> System.Net.Security.TlsCipherSuite? Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool +Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService +Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService.MultiplexedConnectionMarkerService() -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName) -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName, string! serverName) -> void 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..fd6460b1cd0c 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 @@ -3,6 +3,8 @@ Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature.NamedPipe.get -> System.IO.Pipes.NamedPipeServerStream! Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool +Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService +Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService.MultiplexedConnectionMarkerService() -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName) -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName, string! serverName) -> void 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..fd6460b1cd0c 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 @@ -3,6 +3,8 @@ Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature.NamedPipe.get -> System.IO.Pipes.NamedPipeServerStream! Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool +Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService +Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService.MultiplexedConnectionMarkerService() -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName) -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName, string! serverName) -> void 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..8b3eea5298d6 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,25 @@ 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."); + if (Options.ApplicationServices.GetService(typeof(MultiplexedConnectionMarkerService)) 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 +148,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 +162,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 +178,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 +216,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 +269,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 +325,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..c22b183de1e9 100644 --- a/src/Servers/Kestrel/Core/src/Internal/KestrelServerOptionsSetup.cs +++ b/src/Servers/Kestrel/Core/src/Internal/KestrelServerOptionsSetup.cs @@ -8,8 +8,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; internal sealed class KestrelServerOptionsSetup : IConfigureOptions { private readonly IServiceProvider _services; + private readonly bool _disableDefaultCertificate; public KestrelServerOptionsSetup(IServiceProvider services) + { + _services = services; + _disableDefaultCertificate = true; + } + + public KestrelServerOptionsSetup(IServiceProvider services, ITlsConfigurationLoader _) { _services = services; } @@ -17,5 +24,6 @@ public KestrelServerOptionsSetup(IServiceProvider services) public void Configure(KestrelServerOptions options) { options.ApplicationServices = _services; + options.DisableDefaultCertificate = _disableDefaultCertificate; } } 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..b9bff261a67e 100644 --- a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs +++ b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs @@ -1,21 +1,13 @@ // 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.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 +16,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel; /// public class KestrelConfigurationLoader { + private readonly ITlsConfigurationLoader? _tlsLoader; 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); + _tlsLoader = tlsLoader; } /// @@ -62,19 +49,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 +217,25 @@ internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions) if (defaults.SslProtocols.HasValue) { + if (_tlsLoader 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 (_tlsLoader 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 +273,24 @@ public void Load() ConfigurationReader = new ConfigurationReader(Configuration); - LoadDefaultCert(); + if (_tlsLoader?.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 (_tlsLoader is null) + { + throw new InvalidOperationException("You need to call UseHttpsConfiguration"); // TODO (acasey): message + } + } + else { ConfigurationReader.ThrowIfContainsHttpsOnlyConfiguration(endpoint); } @@ -307,42 +313,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; - } + _tlsLoader!.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 +341,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); - } + _tlsLoader!.UseHttps(listenOptions, endpoint, httpsOptions); } listenOptions.EndpointConfig = endpoint; @@ -411,87 +361,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..a437f7fefd40 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 false, the , if any, and the the + /// will be checked for a default certificate. + /// + /// + /// Defaults to false. + /// + internal bool DisableDefaultCertificate { 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 (DisableDefaultCertificate) { - return; + throw new InvalidOperationException("You need to call UseHttpsConfiguration"); // TODO (acasey): message } if (TestOverrideDefaultCertificate is X509Certificate2 certificateFromTest) @@ -393,10 +405,11 @@ public KestrelConfigurationLoader Configure(IConfiguration config, bool reloadOn } var hostEnvironment = ApplicationServices.GetRequiredService(); - var logger = ApplicationServices.GetRequiredService>(); + var serverLogger = ApplicationServices.GetRequiredService>(); var httpsLogger = ApplicationServices.GetRequiredService>(); + var tlsLoader = ApplicationServices.GetService(); - var loader = new KestrelConfigurationLoader(this, config, hostEnvironment, reloadOnChange, logger, httpsLogger); + var loader = new KestrelConfigurationLoader(this, config, reloadOnChange, tlsLoader); ConfigurationLoader = loader; return loader; } diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index 3463a56828e7..0e5e84483c3e 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -164,32 +164,24 @@ 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..1a2d83d61757 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; @@ -30,6 +31,7 @@ private KestrelServerOptions CreateServerOptions() var serverOptions = new KestrelServerOptions(); serverOptions.ApplicationServices = new ServiceCollection() .AddLogging() + .AddSingleton() .BuildServiceProvider(); return serverOptions; } @@ -282,7 +284,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 +295,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 +309,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 +329,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 +350,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 +375,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 +405,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 +436,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 +471,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 +503,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 +716,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 +1038,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/WebHostBuilderQuicExtensions.cs b/src/Servers/Kestrel/Transport.Quic/src/WebHostBuilderQuicExtensions.cs index ad59ed4f999b..cbba963faa1e 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/WebHostBuilderQuicExtensions.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/WebHostBuilderQuicExtensions.cs @@ -20,6 +20,13 @@ public static class WebHostBuilderQuicExtensions /// The . public static IWebHostBuilder UseQuic(this IWebHostBuilder hostBuilder) { + // In order to be able to provide useful error messages in slim scenarios, we have to be able + // to distinguish between QUIC-was-not-requested and QUIC-is-not-available. + hostBuilder.ConfigureServices(services => + { + services.AddSingleton(); + }); + if (QuicListener.IsSupported) { return hostBuilder.ConfigureServices(services => 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/TestTransport/TestServer.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs index a9e22498e3a1..847800c1c7de 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; @@ -87,14 +88,18 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action(this); services.AddSingleton(context.LoggerFactory); + // TODO (acasey): this feels like a hack and will likely make it hard to test the real feature + services.AddSingleton(); + services.AddSingleton(sp => { context.ServerOptions.ApplicationServices = sp; configureKestrel(context.ServerOptions); return new KestrelServerImpl( - new IConnectionListenerFactory[] { _transportFactory }, - sp.GetServices(), - context); + context, + new UseHttpsHelper(), + new TransportManager(context, new IConnectionListenerFactory[] { _transportFactory }), + new MultiplexedTransportManager(context, sp.GetServices())); }); }); From 8041dd111a5ec0b70d353b2962be368161775ba0 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Wed, 15 Mar 2023 17:54:03 -0700 Subject: [PATCH 2/8] Drop MultiplexedConnectionMarkerService --- .../src/MultiplexedConnectionMarkerService.cs | 18 ------------------ .../PublicAPI/net462/PublicAPI.Unshipped.txt | 4 +--- .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 2 -- .../netstandard2.0/PublicAPI.Unshipped.txt | 4 +--- .../netstandard2.1/PublicAPI.Unshipped.txt | 4 +--- .../Core/src/Internal/KestrelServerImpl.cs | 3 ++- .../Kestrel/Core/test/KestrelServerTests.cs | 1 - .../Transport.Quic/src/QuicTransportFactory.cs | 5 ++++- .../src/WebHostBuilderQuicExtensions.cs | 18 +++--------------- .../TestTransport/TestServer.cs | 3 --- 10 files changed, 12 insertions(+), 50 deletions(-) delete mode 100644 src/Servers/Connections.Abstractions/src/MultiplexedConnectionMarkerService.cs diff --git a/src/Servers/Connections.Abstractions/src/MultiplexedConnectionMarkerService.cs b/src/Servers/Connections.Abstractions/src/MultiplexedConnectionMarkerService.cs deleted file mode 100644 index 3c3a583f0408..000000000000 --- a/src/Servers/Connections.Abstractions/src/MultiplexedConnectionMarkerService.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http.Features; - -namespace Microsoft.AspNetCore.Connections; - -/// -/// A marker class used to determine if QUIC support was requested, -/// regardless or whether or not it is supported. -/// TODO (acasey): can we make this non-public? -/// -public sealed class MultiplexedConnectionMarkerService -{ -} 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 fd6460b1cd0c..751bcc347b73 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -3,8 +3,6 @@ Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature.NamedPipe.get -> System.IO.Pipes.NamedPipeServerStream! Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool -Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService -Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService.MultiplexedConnectionMarkerService() -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName) -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName, string! serverName) -> void @@ -12,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/net8.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt index eeed277ba2fc..3ddff4e1b1ec 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -4,8 +4,6 @@ Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature.NamedPipe. Microsoft.AspNetCore.Connections.Features.ITlsHandshakeFeature.NegotiatedCipherSuite.get -> System.Net.Security.TlsCipherSuite? Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool -Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService -Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService.MultiplexedConnectionMarkerService() -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName) -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName, string! serverName) -> void 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 fd6460b1cd0c..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 @@ -3,8 +3,6 @@ Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature.NamedPipe.get -> System.IO.Pipes.NamedPipeServerStream! Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool -Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService -Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService.MultiplexedConnectionMarkerService() -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName) -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName, string! serverName) -> void @@ -12,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 fd6460b1cd0c..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 @@ -3,8 +3,6 @@ Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature.NamedPipe.get -> System.IO.Pipes.NamedPipeServerStream! Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool -Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService -Microsoft.AspNetCore.Connections.MultiplexedConnectionMarkerService.MultiplexedConnectionMarkerService() -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName) -> void Microsoft.AspNetCore.Connections.NamedPipeEndPoint.NamedPipeEndPoint(string! pipeName, string! serverName) -> void @@ -12,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/Internal/KestrelServerImpl.cs b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs index 8b3eea5298d6..59304ce8191b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs +++ b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs @@ -125,7 +125,8 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok if (hasHttp3 && !haveMultiplexedFactories) { - if (Options.ApplicationServices.GetService(typeof(MultiplexedConnectionMarkerService)) is null) + // 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 } diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index 1a2d83d61757..d86e7c13bc0a 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -31,7 +31,6 @@ private KestrelServerOptions CreateServerOptions() var serverOptions = new KestrelServerOptions(); serverOptions.ApplicationServices = new ServiceCollection() .AddLogging() - .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 cbba963faa1e..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,22 +19,11 @@ public static class WebHostBuilderQuicExtensions /// The . public static IWebHostBuilder UseQuic(this IWebHostBuilder hostBuilder) { - // In order to be able to provide useful error messages in slim scenarios, we have to be able - // to distinguish between QUIC-was-not-requested and QUIC-is-not-available. - hostBuilder.ConfigureServices(services => + return hostBuilder.ConfigureServices(services => { - services.AddSingleton(); + // CanBind will return false if QuicListener.IsSupported is false + services.AddSingleton(); }); - - if (QuicListener.IsSupported) - { - return hostBuilder.ConfigureServices(services => - { - services.AddSingleton(); - }); - } - - return hostBuilder; } /// diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs index 847800c1c7de..f8e03a6283ca 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs @@ -88,9 +88,6 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action(this); services.AddSingleton(context.LoggerFactory); - // TODO (acasey): this feels like a hack and will likely make it hard to test the real feature - services.AddSingleton(); - services.AddSingleton(sp => { context.ServerOptions.ApplicationServices = sp; From 36e2106e66d17f94b0eb14234e530d57227ab124 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Wed, 22 Mar 2023 12:52:19 -0700 Subject: [PATCH 3/8] Add support for UseHttps() without explicit opt-in --- .../src/Internal/KestrelServerOptionsSetup.cs | 8 ------- .../Core/src/KestrelConfigurationLoader.cs | 23 ++++++++++++++++++- .../Kestrel/Core/src/KestrelServerOptions.cs | 20 +++++++++++++++- .../Core/src/ListenOptionsHttpsExtensions.cs | 3 +++ 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/KestrelServerOptionsSetup.cs b/src/Servers/Kestrel/Core/src/Internal/KestrelServerOptionsSetup.cs index c22b183de1e9..8d8c51d4d08c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/KestrelServerOptionsSetup.cs +++ b/src/Servers/Kestrel/Core/src/Internal/KestrelServerOptionsSetup.cs @@ -8,15 +8,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; internal sealed class KestrelServerOptionsSetup : IConfigureOptions { private readonly IServiceProvider _services; - private readonly bool _disableDefaultCertificate; public KestrelServerOptionsSetup(IServiceProvider services) - { - _services = services; - _disableDefaultCertificate = true; - } - - public KestrelServerOptionsSetup(IServiceProvider services, ITlsConfigurationLoader _) { _services = services; } @@ -24,6 +17,5 @@ public KestrelServerOptionsSetup(IServiceProvider services, ITlsConfigurationLoa public void Configure(KestrelServerOptions options) { options.ApplicationServices = _services; - options.DisableDefaultCertificate = _disableDefaultCertificate; } } diff --git a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs index b9bff261a67e..af185417b91f 100644 --- a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs +++ b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs @@ -7,7 +7,10 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; 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; @@ -16,7 +19,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel; /// public class KestrelConfigurationLoader { - private readonly ITlsConfigurationLoader? _tlsLoader; + private ITlsConfigurationLoader? _tlsLoader; private bool _loaded; internal KestrelConfigurationLoader( @@ -33,6 +36,24 @@ internal KestrelConfigurationLoader( _tlsLoader = tlsLoader; } + internal bool IsTlsConfigurationLoadingEnabled => _tlsLoader is not null; + + internal void EnableTlsConfigurationLoading( + IHostEnvironment hostEnvironment, + ILogger serverLogger, + ILogger httpsLogger) + { + if (_tlsLoader is null) + { + _tlsLoader = new TlsConfigurationLoader(hostEnvironment, serverLogger, httpsLogger); + + if (_loaded) + { + Reload(); + } + } + } + /// /// Gets the . /// diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index a437f7fefd40..52c3f19df481 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -262,7 +262,7 @@ internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions) internal void ApplyDefaultCertificate(HttpsConnectionAdapterOptions httpsOptions) { - if (DisableDefaultCertificate) + if (ConfigurationLoader is not null && !ConfigurationLoader.IsTlsConfigurationLoadingEnabled) { throw new InvalidOperationException("You need to call UseHttpsConfiguration"); // TODO (acasey): message } @@ -414,6 +414,24 @@ public KestrelConfigurationLoader Configure(IConfiguration config, bool reloadOn return loader; } + internal void EnableTlsConfigurationLoading() + { + // TODO (acasey): keep it around in case someone calls Configure later? + if (ConfigurationLoader is not null) + { + 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>(); + + ConfigurationLoader.EnableTlsConfigurationLoading(hostEnvironment, serverLogger, httpsLogger); + } + } + /// /// 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 0e5e84483c3e..54def5ad7042 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -163,6 +163,9 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, Action Date: Wed, 22 Mar 2023 14:09:47 -0700 Subject: [PATCH 4/8] Correct check in ApplyDefaultCertificate --- .../Core/src/Internal/KestrelServerOptionsSetup.cs | 8 ++++++++ src/Servers/Kestrel/Core/src/KestrelServerOptions.cs | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) 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/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index 52c3f19df481..2196dd820c61 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -218,13 +218,13 @@ internal bool DisableHttp1LineFeedTerminators } /// - /// If false, the , if any, and the the + /// If true, the , if any, and the the /// will be checked for a default certificate. /// /// /// Defaults to false. /// - internal bool DisableDefaultCertificate { get; set; } + internal bool IsHttpsConfigurationEnabled { get; set; } /// /// Specifies a configuration Action to run for each newly created endpoint. Calling this again will replace @@ -262,7 +262,7 @@ internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions) internal void ApplyDefaultCertificate(HttpsConnectionAdapterOptions httpsOptions) { - if (ConfigurationLoader is not null && !ConfigurationLoader.IsTlsConfigurationLoadingEnabled) + if (IsHttpsConfigurationEnabled || (ConfigurationLoader is not null && !ConfigurationLoader.IsTlsConfigurationLoadingEnabled)) { throw new InvalidOperationException("You need to call UseHttpsConfiguration"); // TODO (acasey): message } From 91e7e553e813a4637971133c13ea18a0eb5c5d6d Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Wed, 22 Mar 2023 16:50:22 -0700 Subject: [PATCH 5/8] Update comments in ListenOptionsHttpsExtensions.cs --- src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index 54def5ad7042..ace0c149323b 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -163,19 +163,16 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, Action Date: Wed, 22 Mar 2023 17:10:37 -0700 Subject: [PATCH 6/8] Retain TLS enablement when KestrelConfigurationLoader is replaced --- .../Kestrel/Core/src/KestrelServerOptions.cs | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index 2196dd820c61..bfb53a0a0b53 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -404,31 +404,46 @@ 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 serverLogger = ApplicationServices.GetRequiredService>(); - var httpsLogger = ApplicationServices.GetRequiredService>(); var tlsLoader = ApplicationServices.GetService(); + // It may seem strange to sometimes pass ITlsConfigurationLoader in via the constructor + // and to sometimes call EnableTlsConfigurationLoading, but Configure may be called in + // cases where ITlsConfigurationLoader is not wanted and EnableTlsConfigurationLoading + // will prevent trimming, so we can't call it here, even conditionally. var loader = new KestrelConfigurationLoader(this, config, reloadOnChange, tlsLoader); + if (tlsLoader is null && _enableTlsConfigurationLoading is not null) + { + _enableTlsConfigurationLoading(loader); + } + ConfigurationLoader = loader; return loader; } + private Action? _enableTlsConfigurationLoading; internal void EnableTlsConfigurationLoading() { - // TODO (acasey): keep it around in case someone calls Configure later? - if (ConfigurationLoader is not null) + if (_enableTlsConfigurationLoading is not null) { - if (ApplicationServices is null) - { - throw new InvalidOperationException($"{nameof(ApplicationServices)} must not be null. This is normally set automatically via {nameof(IConfigureOptions)}."); - } + return; + } - var hostEnvironment = ApplicationServices.GetRequiredService(); - var serverLogger = ApplicationServices.GetRequiredService>(); - var httpsLogger = ApplicationServices.GetRequiredService>(); + if (ApplicationServices is null) + { + throw new InvalidOperationException($"{nameof(ApplicationServices)} must not be null. This is normally set automatically via {nameof(IConfigureOptions)}."); + } - ConfigurationLoader.EnableTlsConfigurationLoading(hostEnvironment, serverLogger, httpsLogger); + var hostEnvironment = ApplicationServices.GetRequiredService(); + var serverLogger = ApplicationServices.GetRequiredService>(); + var httpsLogger = ApplicationServices.GetRequiredService>(); + + // We stash the call in a lambda in case the loader is replaced - the new one should also have TLS configuration enabled + _enableTlsConfigurationLoading = configLoader => configLoader.EnableTlsConfigurationLoading(hostEnvironment, serverLogger, httpsLogger); + + // If there's already a configuration loader, apply the setting now + if (ConfigurationLoader is not null) + { + _enableTlsConfigurationLoading(ConfigurationLoader); } } From a6d600cfc8c0c869cab492ba1bb2051d05104d26 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Wed, 22 Mar 2023 17:34:13 -0700 Subject: [PATCH 7/8] Eliminate Action in favor of storing the ITlsConfigurationLoader --- .../Core/src/KestrelConfigurationLoader.cs | 42 ++++++++----------- .../Kestrel/Core/src/KestrelServerOptions.cs | 27 ++++-------- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs index af185417b91f..58f177f4168d 100644 --- a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs +++ b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs @@ -1,16 +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; using System.Linq; using System.Net; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; 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; @@ -19,7 +17,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel; /// public class KestrelConfigurationLoader { - private ITlsConfigurationLoader? _tlsLoader; + private ITlsConfigurationLoader? _tlsConfigurationLoader; private bool _loaded; internal KestrelConfigurationLoader( @@ -33,24 +31,20 @@ internal KestrelConfigurationLoader( ConfigurationReader = new ConfigurationReader(configuration); ReloadOnChange = reloadOnChange; - _tlsLoader = tlsLoader; + _tlsConfigurationLoader = tlsLoader; } - internal bool IsTlsConfigurationLoadingEnabled => _tlsLoader is not null; - - internal void EnableTlsConfigurationLoading( - IHostEnvironment hostEnvironment, - ILogger serverLogger, - ILogger httpsLogger) + /// + /// A helper for loading TLS-related configuration. + /// + internal ITlsConfigurationLoader? TlsConfigurationLoader { - if (_tlsLoader is null) + get => _tlsConfigurationLoader; + set { - _tlsLoader = new TlsConfigurationLoader(hostEnvironment, serverLogger, httpsLogger); - - if (_loaded) - { - Reload(); - } + Debug.Assert(!_loaded); + Debug.Assert(_tlsConfigurationLoader is null); + _tlsConfigurationLoader = value; } } @@ -238,7 +232,7 @@ internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions) if (defaults.SslProtocols.HasValue) { - if (_tlsLoader is null) + 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. @@ -250,7 +244,7 @@ internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions) if (defaults.ClientCertificateMode.HasValue) { - if (_tlsLoader is null) + 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. @@ -294,7 +288,7 @@ public void Load() ConfigurationReader = new ConfigurationReader(Configuration); - if (_tlsLoader?.LoadDefaultCertificate(ConfigurationReader) is CertificateAndConfig certPair) + if (_tlsConfigurationLoader?.LoadDefaultCertificate(ConfigurationReader) is CertificateAndConfig certPair) { DefaultCertificate = certPair.Certificate; DefaultCertificateConfig = certPair.CertificateConfig; @@ -306,7 +300,7 @@ public void Load() if (https) { - if (_tlsLoader is null) + if (_tlsConfigurationLoader is null) { throw new InvalidOperationException("You need to call UseHttpsConfiguration"); // TODO (acasey): message } @@ -334,7 +328,7 @@ public void Load() if (https) { - _tlsLoader!.ApplyHttpsDefaults(Options, endpoint, httpsOptions, DefaultCertificateConfig, ConfigurationReader); + _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. @@ -364,7 +358,7 @@ public void Load() // EndpointDefaults or configureEndpoint may have added an https adapter. if (https) { - _tlsLoader!.UseHttps(listenOptions, endpoint, httpsOptions); + _tlsConfigurationLoader!.UseHttps(listenOptions, endpoint, httpsOptions); } listenOptions.EndpointConfig = endpoint; diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index bfb53a0a0b53..1c979e5f07a4 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -262,7 +262,7 @@ internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions) internal void ApplyDefaultCertificate(HttpsConnectionAdapterOptions httpsOptions) { - if (IsHttpsConfigurationEnabled || (ConfigurationLoader is not null && !ConfigurationLoader.IsTlsConfigurationLoadingEnabled)) + if (!IsHttpsConfigurationEnabled && ConfigurationLoader?.TlsConfigurationLoader is null) { throw new InvalidOperationException("You need to call UseHttpsConfiguration"); // TODO (acasey): message } @@ -404,26 +404,19 @@ 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 tlsLoader = ApplicationServices.GetService(); - - // It may seem strange to sometimes pass ITlsConfigurationLoader in via the constructor - // and to sometimes call EnableTlsConfigurationLoading, but Configure may be called in - // cases where ITlsConfigurationLoader is not wanted and EnableTlsConfigurationLoading - // will prevent trimming, so we can't call it here, even conditionally. - var loader = new KestrelConfigurationLoader(this, config, reloadOnChange, tlsLoader); - if (tlsLoader is null && _enableTlsConfigurationLoading is not null) - { - _enableTlsConfigurationLoading(loader); - } + _tlsConfigurationLoader ??= ApplicationServices.GetService(); + // 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 Action? _enableTlsConfigurationLoading; + private ITlsConfigurationLoader? _tlsConfigurationLoader; internal void EnableTlsConfigurationLoading() { - if (_enableTlsConfigurationLoading is not null) + if (_tlsConfigurationLoader is not null) { return; } @@ -437,13 +430,11 @@ internal void EnableTlsConfigurationLoading() var serverLogger = ApplicationServices.GetRequiredService>(); var httpsLogger = ApplicationServices.GetRequiredService>(); - // We stash the call in a lambda in case the loader is replaced - the new one should also have TLS configuration enabled - _enableTlsConfigurationLoading = configLoader => configLoader.EnableTlsConfigurationLoading(hostEnvironment, serverLogger, httpsLogger); + _tlsConfigurationLoader = new TlsConfigurationLoader(hostEnvironment, serverLogger, httpsLogger); - // If there's already a configuration loader, apply the setting now if (ConfigurationLoader is not null) { - _enableTlsConfigurationLoading(ConfigurationLoader); + ConfigurationLoader.TlsConfigurationLoader = _tlsConfigurationLoader; } } From 327ebdb8e1598734834735ce3af756b3763e0a01 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Fri, 24 Mar 2023 15:14:44 -0700 Subject: [PATCH 8/8] Fix AltSvc_Http1And2And3EndpointConfigured_NoMultiplexedFactory_NoAltSvcInResponseHeaders --- .../test/InMemory.FunctionalTests/ResponseTests.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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() {