diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index 7e21713ae774..0a9906ea415a 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Hosting.StaticWebAssets; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -223,23 +224,31 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder) } }); - ConfigureWebDefaultsCore(builder, services => - { - services.AddRouting(); - }); + ConfigureWebDefaultsWorker( + builder.UseKestrel(ConfigureKestrel), + services => + { + services.AddRouting(); + }); builder .UseIIS() .UseIISIntegration(); } - internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder, Action? configureRouting = null) + internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder) { - builder.UseKestrel((builderContext, options) => - { - options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true); - }) - .ConfigureServices((hostingContext, services) => + ConfigureWebDefaultsWorker(builder.UseKestrelCore().ConfigureKestrel(ConfigureKestrel), configureRouting: null); + } + + private static void ConfigureKestrel(WebHostBuilderContext builderContext, KestrelServerOptions options) + { + options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true); + } + + private static void ConfigureWebDefaultsWorker(IWebHostBuilder builder, Action? configureRouting) + { + builder.ConfigureServices((hostingContext, services) => { // Fallback services.PostConfigure(options => diff --git a/src/ProjectTemplates/Shared/Project.cs b/src/ProjectTemplates/Shared/Project.cs index 58df70e03eba..6a910c504ac5 100644 --- a/src/ProjectTemplates/Shared/Project.cs +++ b/src/ProjectTemplates/Shared/Project.cs @@ -24,6 +24,7 @@ namespace Templates.Test.Helpers; [DebuggerDisplay("{ToString(),nq}")] public class Project : IDisposable { + private const string _urlsNoHttps = "http://127.0.0.1:0"; private const string _urls = "http://127.0.0.1:0;https://127.0.0.1:0"; public static string ArtifactsLogDir @@ -181,11 +182,11 @@ internal async Task RunDotNetBuildAsync(IDictionary packageOptio Assert.True(0 == result.ExitCode, ErrorMessages.GetFailedProcessMessage("build", this, result)); } - internal AspNetProcess StartBuiltProjectAsync(bool hasListeningUri = true, ILogger logger = null) + internal AspNetProcess StartBuiltProjectAsync(bool hasListeningUri = true, ILogger logger = null, bool noHttps = false) { var environment = new Dictionary { - ["ASPNETCORE_URLS"] = _urls, + ["ASPNETCORE_URLS"] = noHttps ? _urlsNoHttps : _urls, ["ASPNETCORE_ENVIRONMENT"] = "Development", ["ASPNETCORE_Logging__Console__LogLevel__Default"] = "Debug", ["ASPNETCORE_Logging__Console__LogLevel__System"] = "Debug", @@ -197,11 +198,11 @@ internal AspNetProcess StartBuiltProjectAsync(bool hasListeningUri = true, ILogg return new AspNetProcess(DevCert, Output, TemplateOutputDir, projectDll, environment, published: false, hasListeningUri: hasListeningUri, logger: logger); } - internal AspNetProcess StartPublishedProjectAsync(bool hasListeningUri = true, bool usePublishedAppHost = false) + internal AspNetProcess StartPublishedProjectAsync(bool hasListeningUri = true, bool usePublishedAppHost = false, bool noHttps = false) { var environment = new Dictionary { - ["ASPNETCORE_URLS"] = _urls, + ["ASPNETCORE_URLS"] = noHttps ? _urlsNoHttps : _urls, ["ASPNETCORE_Logging__Console__LogLevel__Default"] = "Debug", ["ASPNETCORE_Logging__Console__LogLevel__System"] = "Debug", ["ASPNETCORE_Logging__Console__LogLevel__Microsoft"] = "Debug", diff --git a/src/ProjectTemplates/test/Templates.Tests/ApiTemplateTest.cs b/src/ProjectTemplates/test/Templates.Tests/ApiTemplateTest.cs index 62bf4a7f3ad3..388e51f3a9ee 100644 --- a/src/ProjectTemplates/test/Templates.Tests/ApiTemplateTest.cs +++ b/src/ProjectTemplates/test/Templates.Tests/ApiTemplateTest.cs @@ -82,7 +82,8 @@ private async Task ApiTemplateCore(string languageOverride, string[] args = null await project.RunDotNetBuildAsync(); - using (var aspNetProcess = project.StartBuiltProjectAsync()) + // The minimal/slim/core scenario doesn't include TLS support, so tell `project` not to register an https address + using (var aspNetProcess = project.StartBuiltProjectAsync(noHttps: true)) { Assert.False( aspNetProcess.Process.HasExited, @@ -91,7 +92,7 @@ private async Task ApiTemplateCore(string languageOverride, string[] args = null await AssertEndpoints(aspNetProcess); } - using (var aspNetProcess = project.StartPublishedProjectAsync()) + using (var aspNetProcess = project.StartPublishedProjectAsync(noHttps: true)) { Assert.False( aspNetProcess.Process.HasExited, diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index 1d7f3f60bd85..2c7c3e8ef18d 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -722,4 +722,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l Failed to bind to http://[::]:{port} (IPv6Any). - \ No newline at end of file + + Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable loading HTTPS settings from configuration. + + + Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable loading the default server certificate from configuration. + + + Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable transport layer security for HTTP/3. + + + Call UseKestrelHttpsConfiguration() on IWebHostBuilder to automatically enable HTTPS when an https:// address is used. + + diff --git a/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs b/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs new file mode 100644 index 000000000000..8e6185d4b642 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs @@ -0,0 +1,260 @@ +// 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.Diagnostics.CodeAnalysis; +using System.IO.Pipelines; +using System.Net; +using System.Net.Security; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +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.Core; + +/// +internal sealed class HttpsConfigurationService : IHttpsConfigurationService +{ + private readonly IInitializer? _initializer; + private bool _isInitialized; + + private TlsConfigurationLoader? _tlsConfigurationLoader; + private Action? _populateMultiplexedTransportFeatures; + private Func? _useHttpsWithDefaults; + + /// + /// Create an uninitialized . + /// To initialize it later, call . + /// + public HttpsConfigurationService() + { + } + + /// + /// Create an initialized . + /// + /// + /// In practice, won't be called until it's needed. + /// + public HttpsConfigurationService(IInitializer initializer) + { + _initializer = initializer; + } + + /// + // If there's an initializer, it *can* be initialized, even though it might not be yet. + // Use explicit interface implentation so we don't accidentally call it within this class. + bool IHttpsConfigurationService.IsInitialized => _isInitialized || _initializer is not null; + + /// + public void Initialize( + IHostEnvironment hostEnvironment, + ILogger serverLogger, + ILogger httpsLogger) + { + if (_isInitialized) + { + return; + } + + _isInitialized = true; + + _tlsConfigurationLoader = new TlsConfigurationLoader(hostEnvironment, serverLogger, httpsLogger); + _populateMultiplexedTransportFeatures = PopulateMultiplexedTransportFeaturesWorker; + _useHttpsWithDefaults = UseHttpsWithDefaultsWorker; + } + + /// + public void ApplyHttpsConfiguration( + HttpsConnectionAdapterOptions httpsOptions, + EndpointConfig endpoint, + KestrelServerOptions serverOptions, + CertificateConfig? defaultCertificateConfig, + ConfigurationReader configurationReader) + { + EnsureInitialized(CoreStrings.NeedHttpsConfigurationToApplyHttpsConfiguration); + _tlsConfigurationLoader.ApplyHttpsConfiguration(httpsOptions, endpoint, serverOptions, defaultCertificateConfig, configurationReader); + } + + /// + public ListenOptions UseHttpsWithSni(ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint) + { + // This doesn't get a distinct string since it won't actually throw - it's always called after ApplyHttpsConfiguration + EnsureInitialized(CoreStrings.NeedHttpsConfigurationToApplyHttpsConfiguration); + return _tlsConfigurationLoader.UseHttpsWithSni(listenOptions, httpsOptions, endpoint); + } + + /// + public CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader) + { + EnsureInitialized(CoreStrings.NeedHttpsConfigurationToLoadDefaultCertificate); + return _tlsConfigurationLoader.LoadDefaultCertificate(configurationReader); + } + + /// + public void PopulateMultiplexedTransportFeatures(FeatureCollection features, ListenOptions listenOptions) + { + EnsureInitialized(CoreStrings.NeedHttpsConfigurationToUseHttp3); + _populateMultiplexedTransportFeatures.Invoke(features, listenOptions); + } + + /// + public ListenOptions UseHttpsWithDefaults(ListenOptions listenOptions) + { + EnsureInitialized(CoreStrings.NeedHttpsConfigurationToBindHttpsAddresses); + return _useHttpsWithDefaults.Invoke(listenOptions); + } + + /// + /// If this instance has not been initialized, initialize it if possible and throw otherwise. + /// + /// If initialization is not possible. + [MemberNotNull(nameof(_useHttpsWithDefaults), nameof(_tlsConfigurationLoader), nameof(_populateMultiplexedTransportFeatures))] + private void EnsureInitialized(string uninitializedError) + { + if (!_isInitialized) + { + if (_initializer is not null) + { + _initializer.Initialize(this); + } + else + { + throw new InvalidOperationException(uninitializedError); + } + } + + Debug.Assert(_useHttpsWithDefaults is not null); + Debug.Assert(_tlsConfigurationLoader is not null); + Debug.Assert(_populateMultiplexedTransportFeatures is not null); + } + + /// + /// The initialized implementation of . + /// + internal static void PopulateMultiplexedTransportFeaturesWorker(FeatureCollection features, ListenOptions listenOptions) + { + // 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, + }); + } + } + + /// + /// The initialized implementation of . + /// + internal static ListenOptions UseHttpsWithDefaultsWorker(ListenOptions listenOptions) + { + return listenOptions.UseHttps(); + } + + /// + /// 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(); + } + + /// + /// Register an instance of this type to initialize registered instances of . + /// + internal interface IInitializer + { + /// + /// Invokes , passing appropriate arguments. + /// + void Initialize(IHttpsConfigurationService httpsConfigurationService); + } + + /// + internal sealed class Initializer : IInitializer + { + private readonly IHostEnvironment _hostEnvironment; + private readonly ILogger _serverLogger; + private readonly ILogger _httpsLogger; + + public Initializer( + IHostEnvironment hostEnvironment, + ILogger serverLogger, + ILogger httpsLogger) + { + _hostEnvironment = hostEnvironment; + _serverLogger = serverLogger; + _httpsLogger = httpsLogger; + } + + /// + public void Initialize(IHttpsConfigurationService httpsConfigurationService) + { + httpsConfigurationService.Initialize(_hostEnvironment, _serverLogger, _httpsLogger); + } + } +} + diff --git a/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs b/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs index 8435bfa998a6..48b6629d0762 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; } + /// + /// Convenient shorthand for a common check. + /// + 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/IHttpsConfigurationService.cs b/src/Servers/Kestrel/Core/src/IHttpsConfigurationService.cs new file mode 100644 index 000000000000..7e5a955719fd --- /dev/null +++ b/src/Servers/Kestrel/Core/src/IHttpsConfigurationService.cs @@ -0,0 +1,100 @@ +// 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.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +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.Core; + +/// +/// An abstraction over various things that would prevent us from trimming TLS support in `CreateSlimBuilder` +/// scenarios. In normal usage, it will *always* be registered by only be if the +/// consumer explicitly opts into having HTTPS/TLS support. +/// +internal interface IHttpsConfigurationService +{ + /// + /// If this property returns false, then methods other than will throw. + /// The most obvious way to make this true is to call , but some implementations + /// may offer alternative mechanisms. + /// + bool IsInitialized { get; } + + /// + /// Replaces the implementations off all other methods with functioning (as opposed to throwing) versions. + /// + void Initialize( + IHostEnvironment hostEnvironment, + ILogger serverLogger, + ILogger httpsLogger); + + /// + /// Applies various configuration settings to and . + /// + /// + /// For use during configuration loading (esp in ). + /// + void ApplyHttpsConfiguration( + HttpsConnectionAdapterOptions httpsOptions, + EndpointConfig endpoint, + KestrelServerOptions serverOptions, + CertificateConfig? defaultCertificateConfig, + ConfigurationReader configurationReader); + + /// + /// Calls an appropriate overload of + /// on , with or without SNI, according to how is configured. + /// + /// Updated for convenient chaining. + /// + /// For use during configuration loading (esp in ). + /// + ListenOptions UseHttpsWithSni(ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint); + + /// + /// Retrieves the default or, failing that, developer certificate from . + /// + /// + /// For use during configuration loading (esp in ). + /// + CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader); + + /// + /// Updates with multiplexed transport (i.e. HTTP/3) features based on + /// the configuration of . + /// + /// + /// For use during endpoint binding (esp in ). + /// + void PopulateMultiplexedTransportFeatures(FeatureCollection features, ListenOptions listenOptions); + + /// + /// Calls + /// on . + /// + /// Updated for convenient chaining. + /// + /// For use during address binding (esp in ). + /// + ListenOptions UseHttpsWithDefaults(ListenOptions listenOptions); +} + +/// +/// A - pair. +/// +internal readonly struct CertificateAndConfig +{ + public readonly X509Certificate2 Certificate; + public readonly CertificateConfig CertificateConfig; + + public CertificateAndConfig(X509Certificate2 certificate, CertificateConfig certificateConfig) + { + Certificate = certificate; + CertificateConfig = certificateConfig; + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs b/src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs index a2e872da3080..a9bd269acdd4 100644 --- a/src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs +++ b/src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs @@ -6,7 +6,6 @@ using System.Net; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; @@ -17,12 +16,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, Func useHttps, CancellationToken cancellationToken) { var strategy = CreateStrategy( listenOptions, context.Addresses.ToArray(), - context.ServerAddressesFeature.PreferHostingUrls); + context.ServerAddressesFeature.PreferHostingUrls, + useHttps); // reset options. The actual used options and addresses will be populated // by the address binding feature @@ -32,7 +32,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, Func useHttps) { var hasListenOptions = listenOptions.Length > 0; var hasAddresses = addresses.Length > 0; @@ -41,10 +41,10 @@ private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[] { if (hasListenOptions) { - return new OverrideWithAddressesStrategy(addresses); + return new OverrideWithAddressesStrategy(addresses, useHttps); } - return new AddressesStrategy(addresses); + return new AddressesStrategy(addresses, useHttps); } else if (hasListenOptions) { @@ -58,7 +58,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, useHttps); } else { @@ -162,8 +162,8 @@ public async Task BindAsync(AddressBindContext context, CancellationToken cancel private sealed class OverrideWithAddressesStrategy : AddressesStrategy { - public OverrideWithAddressesStrategy(IReadOnlyCollection addresses) - : base(addresses) + public OverrideWithAddressesStrategy(IReadOnlyCollection addresses, Func useHttps) + : base(addresses, useHttps) { } @@ -216,10 +216,12 @@ public virtual async Task BindAsync(AddressBindContext context, CancellationToke private class AddressesStrategy : IStrategy { protected readonly IReadOnlyCollection _addresses; + private readonly Func _useHttps; - public AddressesStrategy(IReadOnlyCollection addresses) + public AddressesStrategy(IReadOnlyCollection addresses, Func useHttps) { _addresses = addresses; + _useHttps = useHttps; } public virtual async Task BindAsync(AddressBindContext context, CancellationToken cancellationToken) @@ -231,7 +233,7 @@ public virtual async Task BindAsync(AddressBindContext context, CancellationToke if (https && !options.IsTls) { - options.UseHttps(); + _useHttps(options); } await options.BindAsync(context, cancellationToken).ConfigureAwait(false); diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs index 00505ae9e354..c5651c50d2fa 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs @@ -3,13 +3,9 @@ #nullable enable -using System.IO.Pipelines; 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; @@ -19,15 +15,18 @@ internal sealed class TransportManager private readonly List _transportFactories; private readonly List _multiplexedTransportFactories; + private readonly IHttpsConfigurationService _httpsConfigurationService; private readonly ServiceContext _serviceContext; public TransportManager( List transportFactories, List multiplexedTransportFactories, + IHttpsConfigurationService httpsConfigurationService, ServiceContext serviceContext) { _transportFactories = transportFactories; _multiplexedTransportFactories = multiplexedTransportFactories; + _httpsConfigurationService = httpsConfigurationService; _serviceContext = serviceContext; } @@ -72,36 +71,8 @@ public async Task BindAsync(EndPoint endPoint, MultiplexedConnectionDe 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, - }); - } + // Will throw an appropriate error if it's not enabled + _httpsConfigurationService.PopulateMultiplexedTransportFeatures(features, listenOptions); foreach (var multiplexedTransportFactory in _multiplexedTransportFactories) { @@ -124,49 +95,6 @@ private static bool CanBindFactory(EndPoint endPoint, IConnectionListenerFactory 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); diff --git a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs index b8f095bf6454..96973e0fc7d5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs +++ b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs @@ -23,6 +23,7 @@ internal sealed class KestrelServerImpl : IServer private readonly TransportManager _transportManager; private readonly List _transportFactories; private readonly List _multiplexedTransportFactories; + private readonly IHttpsConfigurationService _httpsConfigurationService; private readonly SemaphoreSlim _bindSemaphore = new SemaphoreSlim(initialCount: 1); private bool _hasStarted; @@ -36,9 +37,10 @@ internal sealed class KestrelServerImpl : IServer IOptions options, IEnumerable transportFactories, IEnumerable multiplexedFactories, + IHttpsConfigurationService httpsConfigurationService, ILoggerFactory loggerFactory, KestrelMetrics metrics) - : this(transportFactories, multiplexedFactories, CreateServiceContext(options, loggerFactory, diagnosticSource: null, metrics)) + : this(transportFactories, multiplexedFactories, httpsConfigurationService, CreateServiceContext(options, loggerFactory, diagnosticSource: null, metrics)) { } @@ -47,12 +49,14 @@ internal sealed class KestrelServerImpl : IServer internal KestrelServerImpl( IEnumerable transportFactories, IEnumerable multiplexedFactories, + IHttpsConfigurationService httpsConfigurationService, ServiceContext serviceContext) { ArgumentNullException.ThrowIfNull(transportFactories); _transportFactories = transportFactories.Reverse().ToList(); _multiplexedTransportFactories = multiplexedFactories.Reverse().ToList(); + _httpsConfigurationService = httpsConfigurationService; if (_transportFactories.Count == 0 && _multiplexedTransportFactories.Count == 0) { @@ -65,7 +69,7 @@ internal sealed class KestrelServerImpl : IServer _serverAddresses = new ServerAddressesFeature(); Features.Set(_serverAddresses); - _transportManager = new TransportManager(_transportFactories, _multiplexedTransportFactories, ServiceContext); + _transportManager = new TransportManager(_transportFactories, _multiplexedTransportFactories, _httpsConfigurationService, ServiceContext); } private static ServiceContext CreateServiceContext(IOptions options, ILoggerFactory loggerFactory, DiagnosticSource? diagnosticSource, KestrelMetrics metrics) @@ -167,7 +171,7 @@ 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)) { - throw new InvalidOperationException("This platform doesn't support QUIC or HTTP/3."); + throw new InvalidOperationException("Unable to bind an HTTP/3 endpoint. This could be because QUIC has not been configured using UseQuic, or the platform doesn't support QUIC or HTTP/3."); } // Disable adding alt-svc header if endpoint has configured not to or there is no @@ -298,7 +302,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!, _httpsConfigurationService.UseHttpsWithDefaults, cancellationToken).ConfigureAwait(false); _configChangedRegistration = reloadToken?.RegisterChangeCallback(TriggerRebind, this); } finally 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 @@ internal sealed class 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/KestrelConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs index 6b519fd56c5b..fdc16bd7cc45 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,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel; /// public class KestrelConfigurationLoader { + private readonly IHttpsConfigurationService _httpsConfigurationService; + private bool _loaded; internal KestrelConfigurationLoader( KestrelServerOptions options, IConfiguration configuration, - IHostEnvironment hostEnvironment, - bool reloadOnChange, - ILogger logger, - ILogger httpsLogger) + IHttpsConfigurationService httpsConfigurationService, + bool reloadOnChange) { - 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; ReloadOnChange = reloadOnChange; ConfigurationReader = new ConfigurationReader(configuration); - CertificateConfigLoader = new CertificateConfigLoader(hostEnvironment, logger); + + _httpsConfigurationService = httpsConfigurationService; } /// @@ -62,14 +52,8 @@ public class 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; } = new Dictionary>(0, StringComparer.OrdinalIgnoreCase); @@ -278,7 +262,11 @@ public void Load() ConfigurationReader = new ConfigurationReader(Configuration); - LoadDefaultCert(); + if (_httpsConfigurationService.IsInitialized && _httpsConfigurationService.LoadDefaultCertificate(ConfigurationReader) is CertificateAndConfig certPair) + { + DefaultCertificate = certPair.Certificate; + DefaultCertificateConfig = certPair.CertificateConfig; + } foreach (var endpoint in ConfigurationReader.Endpoints) { @@ -307,42 +295,8 @@ 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; - } + // Throws an appropriate exception if https configuration isn't enabled + _httpsConfigurationService.ApplyHttpsConfiguration(httpsOptions, endpoint, Options, DefaultCertificateConfig, ConfigurationReader); } // Now that defaults have been loaded, we can compare to the currently bound endpoints to see if the config changed. @@ -370,30 +324,12 @@ 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); - } + // This would throw if it were invoked without https configuration having been enabled, + // but that won't happen because ApplyHttpsConfiguration would throw above under those + // circumstances. + _httpsConfigurationService.UseHttpsWithSni(listenOptions, httpsOptions, endpoint); } listenOptions.EndpointConfig = endpoint; @@ -411,87 +347,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 1a31cf04d35f..c65552f9a06f 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServer.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServer.cs @@ -5,7 +5,11 @@ using Microsoft.AspNetCore.Connections; 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.AspNetCore.Server.Kestrel.Https; +using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Metrics; using Microsoft.Extensions.Options; @@ -31,6 +35,7 @@ public KestrelServer(IOptions options, IConnectionListener options, new[] { transportFactory ?? throw new ArgumentNullException(nameof(transportFactory)) }, Array.Empty(), + new SimpleHttpsConfigurationService(), loggerFactory, new KestrelMetrics(new DummyMeterFactory())); } @@ -70,4 +75,44 @@ private sealed class DummyMeterFactory : IMeterFactory public Meter CreateMeter(MeterOptions options) => new Meter(options.Name, options.Version); } + + private sealed class SimpleHttpsConfigurationService : IHttpsConfigurationService + { + public bool IsInitialized => true; + + public void Initialize(IHostEnvironment hostEnvironment, ILogger serverLogger, ILogger httpsLogger) + { + // Already initialized + } + + public void PopulateMultiplexedTransportFeatures(FeatureCollection features, ListenOptions listenOptions) + { + HttpsConfigurationService.PopulateMultiplexedTransportFeaturesWorker(features, listenOptions); + } + + public ListenOptions UseHttpsWithDefaults(ListenOptions listenOptions) + { + return HttpsConfigurationService.UseHttpsWithDefaultsWorker(listenOptions); + } + + public void ApplyHttpsConfiguration( + HttpsConnectionAdapterOptions httpsOptions, + EndpointConfig endpoint, + KestrelServerOptions serverOptions, + CertificateConfig? defaultCertificateConfig, + ConfigurationReader configurationReader) + { + throw new NotImplementedException(); // Not actually required by this impl + } + + public ListenOptions UseHttpsWithSni(ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint) + { + throw new NotImplementedException(); // Not actually required by this impl + } + + public CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader) + { + throw new NotImplementedException(); // Not actually required by this impl + } + } } diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index 6bfeaef62115..b44e3b33adcf 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -250,11 +250,15 @@ internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions) internal void ApplyDefaultCertificate(HttpsConnectionAdapterOptions httpsOptions) { - if (httpsOptions.ServerCertificate != null || httpsOptions.ServerCertificateSelector != null) + if (httpsOptions.HasServerCertificateOrSelector) { return; } + // It's important (and currently true) that we don't reach here with https configuration uninitialized because + // we might incorrectly favor the development certificate over one specified by the user. + Debug.Assert(ApplicationServices.GetRequiredService().IsInitialized, "HTTPS configuration should have been enabled"); + if (TestOverrideDefaultCertificate is X509Certificate2 certificateFromTest) { httpsOptions.ServerCertificate = certificateFromTest; @@ -278,6 +282,19 @@ internal void ApplyDefaultCertificate(HttpsConnectionAdapterOptions httpsOptions httpsOptions.ServerCertificate = DevelopmentCertificate; } + internal void EnableHttpsConfiguration() + { + var httpsConfigurationService = ApplicationServices.GetRequiredService(); + + if (!httpsConfigurationService.IsInitialized) + { + var hostEnvironment = ApplicationServices.GetRequiredService(); + var logger = ApplicationServices.GetRequiredService>(); + var httpsLogger = ApplicationServices.GetRequiredService>(); + httpsConfigurationService.Initialize(hostEnvironment, logger, httpsLogger); + } + } + internal void Serialize(Utf8JsonWriter writer) { writer.WritePropertyName(nameof(AllowSynchronousIO)); @@ -392,11 +409,8 @@ public KestrelConfigurationLoader Configure(IConfiguration config, bool reloadOn throw new InvalidOperationException($"{nameof(ApplicationServices)} must not be null. This is normally set automatically via {nameof(IConfigureOptions)}."); } - var hostEnvironment = ApplicationServices.GetRequiredService(); - var logger = ApplicationServices.GetRequiredService>(); - var httpsLogger = ApplicationServices.GetRequiredService>(); - - var loader = new KestrelConfigurationLoader(this, config, hostEnvironment, reloadOnChange, logger, httpsLogger); + var httpsConfigurationService = ApplicationServices.GetRequiredService(); + var loader = new KestrelConfigurationLoader(this, config, httpsConfigurationService, reloadOnChange); ConfigurationLoader = loader; return loader; } diff --git a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs index c69c37c71b13..2ea73a318584 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs @@ -163,12 +163,15 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, Action /// Configure Kestrel to use HTTPS. This does not use default certificates or other defaults specified via config or /// . diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs index de065dc3e9be..8cf4f843798a 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs @@ -57,7 +57,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/TlsConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs new file mode 100644 index 000000000000..087b06639483 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs @@ -0,0 +1,205 @@ +// 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.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.Core; + +/// +/// An abstraction over the parts of that would prevent us from trimming TLS support +/// in `CreateSlimBuilder` scenarios. Managed by . +/// +internal sealed class TlsConfigurationLoader +{ + 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; + } + + /// + /// Applies various configuration settings to and . + /// + public void ApplyHttpsConfiguration( + HttpsConnectionAdapterOptions httpsOptions, + EndpointConfig endpoint, + KestrelServerOptions serverOptions, + 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; + } + } + + /// + /// Calls an appropriate overload of + /// on , with or without SNI, according to how is configured. + /// + /// Updated for convenient chaining. + public ListenOptions UseHttpsWithSni( + ListenOptions listenOptions, + HttpsConnectionAdapterOptions httpsOptions, + EndpointConfig endpoint) + { + if (listenOptions.IsTls) + { + return listenOptions; + } + + if (endpoint.Sni.Count == 0) + { + if (!httpsOptions.HasServerCertificateOrSelector) + { + throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound); + } + + return listenOptions.UseHttps(httpsOptions); + } + + 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, + }; + + return listenOptions.UseHttps(tlsCallbackOptions); + } + + /// + /// Retrieves the default or, failing that, developer certificate from . + /// + 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/test/AddressBinderTests.cs b/src/Servers/Kestrel/Core/test/AddressBinderTests.cs index 4a0cd1602315..6946da8f91cc 100644 --- a/src/Servers/Kestrel/Core/test/AddressBinderTests.cs +++ b/src/Servers/Kestrel/Core/test/AddressBinderTests.cs @@ -1,28 +1,22 @@ // 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.Collections.Generic; -using System.IO; using System.Net; using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; public class AddressBinderTests { + private readonly Func _noopUseHttps = l => l; + [Theory] [InlineData("http://10.10.10.10:5000/", "10.10.10.10", 5000)] [InlineData("http://[::1]:5000", "::1", 5000)] @@ -172,7 +166,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, _noopUseHttps, CancellationToken.None)); } [Fact] @@ -193,7 +187,7 @@ public void LogsWarningWhenHostingAddressesAreOverridden() logger, endpoint => Task.CompletedTask); - var bindTask = AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None); + var bindTask = AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, _noopUseHttps, CancellationToken.None); Assert.True(bindTask.IsCompletedSuccessfully); var log = Assert.Single(logger.Messages); @@ -221,7 +215,7 @@ public void LogsInformationWhenKestrelAddressesAreOverridden() addressBindContext.ServerAddressesFeature.PreferHostingUrls = true; - var bindTask = AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None); + var bindTask = AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, _noopUseHttps, CancellationToken.None); Assert.True(bindTask.IsCompletedSuccessfully); var log = Assert.Single(logger.Messages); @@ -247,7 +241,7 @@ public async Task FlowsCancellationTokenToCreateBinddingCallback() }); await Assert.ThrowsAsync(() => - AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, new CancellationToken(true))); + AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, _noopUseHttps, new CancellationToken(true))); } [Theory] @@ -284,7 +278,7 @@ public async Task FallbackToIPv4WhenIPv6AnyBindFails(string address) return Task.CompletedTask; }); - await AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None); + await AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, _noopUseHttps, 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 +309,7 @@ public async Task DefaultAddressBinderBindsToHttpPort5000() return Task.CompletedTask; }); - await AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, CancellationToken.None); + await AddressBinder.BindAsync(options.GetListenOptions(), addressBindContext, _noopUseHttps, CancellationToken.None); Assert.Contains(endpoints, e => e.IPEndPoint.Port == 5000 && !e.IsTls); } diff --git a/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs index d213bdc2e5e5..624022316441 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs @@ -84,6 +84,7 @@ public void CanCallListenAfterConfigure() serviceCollection.AddSingleton(Mock.Of()); serviceCollection.AddSingleton(Mock.Of>()); serviceCollection.AddSingleton(Mock.Of>()); + serviceCollection.AddSingleton(Mock.Of()); options.ApplicationServices = serviceCollection.BuildServiceProvider(); options.Configure(); diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index cd296073337a..47c56fdb5cf0 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -28,9 +28,15 @@ public class KestrelServerTests { private KestrelServerOptions CreateServerOptions() { + // It's not actually going to be used - we just need to satisfy the check in ApplyDefaultCertificate + var mockHttpsConfig = new Mock(); + mockHttpsConfig.Setup(m => m.IsInitialized).Returns(true); + var serverOptions = new KestrelServerOptions(); serverOptions.ApplicationServices = new ServiceCollection() .AddSingleton(new KestrelMetrics(new TestMeterFactory())) + .AddSingleton(Mock.Of()) + .AddSingleton(mockHttpsConfig.Object) .AddLogging() .BuildServiceProvider(); return serverOptions; @@ -287,10 +293,20 @@ public void ConstructorWithNullTransportFactoryThrows() ILoggerFactory loggerFactory = null, KestrelMetrics metrics = null) { + var httpsConfigurationService = new HttpsConfigurationService(); + if (options?.ApplicationServices is IServiceProvider serviceProvider) + { + httpsConfigurationService.Initialize( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService>(), + serviceProvider.GetRequiredService>()); + } + return new KestrelServerImpl( Options.Create(options), transportFactories, multiplexedFactories, + httpsConfigurationService, loggerFactory ?? new LoggerFactory(new[] { new KestrelTestLoggerProvider() }), metrics ?? new KestrelMetrics(new TestMeterFactory())); } @@ -713,7 +729,7 @@ public void StartingServerInitializesHeartbeat() DebuggerWrapper.Singleton, testContext.Log); - using (var server = new KestrelServerImpl(new[] { new MockTransportFactory() }, Array.Empty(), testContext)) + using (var server = new KestrelServerImpl(new[] { new MockTransportFactory() }, Array.Empty(), new HttpsConfigurationService(), testContext)) { Assert.Null(testContext.DateHeaderValueManager.GetDateHeaderValues()); @@ -767,6 +783,7 @@ public async Task ReloadsOnConfigurationChangeWhenOptedIn() serviceCollection.AddSingleton(Mock.Of()); serviceCollection.AddSingleton(Mock.Of>()); serviceCollection.AddSingleton(Mock.Of>()); + serviceCollection.AddSingleton(Mock.Of()); var options = new KestrelServerOptions { @@ -904,6 +921,7 @@ public async Task DoesNotReloadOnConfigurationChangeByDefault() serviceCollection.AddSingleton(Mock.Of()); serviceCollection.AddSingleton(Mock.Of>()); serviceCollection.AddSingleton(Mock.Of>()); + serviceCollection.AddSingleton(Mock.Of()); var options = new KestrelServerOptions { diff --git a/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..1b0f75d6addc 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.UseKestrelHttpsConfiguration(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelCore(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 f4563b222809..7186715a6c5f 100644 --- a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs +++ b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs @@ -19,6 +19,28 @@ namespace Microsoft.AspNetCore.Hosting; /// public static class WebHostBuilderKestrelExtensions { + /// + /// In scenarios, it may be necessary to explicitly + /// opt in to certain HTTPS functionality. For example, if ASPNETCORE_URLS includes + /// an https:// address, will enable configuration + /// of HTTPS on that endpoint. + /// + /// Has no effect in scenarios. + /// + /// + /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder to configure. + /// + /// + /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder. + /// + public static IWebHostBuilder UseKestrelHttpsConfiguration(this IWebHostBuilder hostBuilder) + { + return hostBuilder.ConfigureServices(services => + { + services.AddSingleton(); + }); + } + /// /// Specify Kestrel as the server to be used by the web host. /// @@ -29,6 +51,33 @@ public static class WebHostBuilderKestrelExtensions /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder. /// public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder) + { + return hostBuilder + .UseKestrelCore() + .UseKestrelHttpsConfiguration() + .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; + }); + } + + /// + /// Specify Kestrel as the server to be used by the web host. + /// Includes less automatic functionality than to make trimming more effective + /// (e.g. for Native AOT scenarios). If the host ends up depending on + /// some of the absent functionality, a best-effort attempt will be made to enable it on-demand. Failing that, an + /// exception with an informative error message will be raised when the host is started. + /// + /// + /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder to configure. + /// + /// + /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder. + /// + public static IWebHostBuilder UseKestrelCore(this IWebHostBuilder hostBuilder) { hostBuilder.ConfigureServices(services => { @@ -36,18 +85,11 @@ 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; - }); - if (OperatingSystem.IsWindows()) { hostBuilder.UseNamedPipes(); diff --git a/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs b/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs new file mode 100644 index 000000000000..cb27ce5bc210 --- /dev/null +++ b/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs @@ -0,0 +1,237 @@ +// 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.Connections; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Server.Kestrel.Tests; + +public class HttpsConfigurationTests +{ + [Theory] + [InlineData("http://127.0.0.1:0", true)] + [InlineData("http://127.0.0.1:0", false)] + [InlineData("https://127.0.0.1:0", true)] + [InlineData("https://127.0.0.1:0", false)] + public async Task BindAddressFromSetting(string address, bool useKestrelHttpsConfiguration) + { + var hostBuilder = new WebHostBuilder() + .UseKestrelCore() + .ConfigureKestrel(serverOptions => + { + serverOptions.TestOverrideDefaultCertificate = new X509Certificate2(Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx"), "testPassword"); + }) + .Configure(app => { }); + + // This is what ASPNETCORE_URLS would populate + hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey, address); + + if (useKestrelHttpsConfiguration) + { + hostBuilder.UseKestrelHttpsConfiguration(); + } + + var host = hostBuilder.Build(); + + Assert.Single(host.ServerFeatures.Get().Addresses, address); + + if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useKestrelHttpsConfiguration) + { + Assert.Throws(host.Run); + } + else + { + // Binding succeeds + await host.StartAsync(); + await host.StopAsync(); + } + } + + [Fact] + public void NoFallbackToHttpAddress() + { + const string httpAddress = "http://127.0.0.1:0"; + const string httpsAddress = "https://localhost:5001"; + + var hostBuilder = new WebHostBuilder() + .UseKestrelCore() + .Configure(app => { }); + + // This is what ASPNETCORE_URLS would populate + hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey, $"{httpAddress};{httpsAddress}"); + + var host = hostBuilder.Build(); + + Assert.Equal(new[] { httpAddress, httpsAddress }, host.ServerFeatures.Get().Addresses); + + Assert.Throws(host.Run); + } + + [Theory] + [InlineData("http://127.0.0.1:0", true)] + [InlineData("http://127.0.0.1:0", false)] + [InlineData("https://127.0.0.1:0", true)] + [InlineData("https://127.0.0.1:0", false)] + public async Task BindAddressFromEndpoint(string address, bool useKestrelHttpsConfiguration) + { + var hostBuilder = new WebHostBuilder() + .UseKestrelCore() + .ConfigureKestrel(serverOptions => + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:end1:Url", address), + new KeyValuePair("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx")), + new KeyValuePair("Certificates:Default:Password", "testPassword"), + }).Build(); + serverOptions.Configure(config); + }) + .Configure(app => { }); + + if (useKestrelHttpsConfiguration) + { + hostBuilder.UseKestrelHttpsConfiguration(); + } + + var host = hostBuilder.Build(); + + if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useKestrelHttpsConfiguration) + { + Assert.Throws(host.Run); + } + else + { + // Binding succeeds + await host.StartAsync(); + await host.StopAsync(); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task LoadDefaultCertificate(bool useKestrelHttpsConfiguration) + { + var hostBuilder = new WebHostBuilder() + .UseKestrelCore() + .ConfigureKestrel(serverOptions => + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx")), + new KeyValuePair("Certificates:Default:Password", "testPassword"), + }).Build(); + serverOptions.Configure(config); + }) + .Configure(app => { }); + + if (useKestrelHttpsConfiguration) + { + hostBuilder.UseKestrelHttpsConfiguration(); + } + + var host = hostBuilder.Build(); + + // There's no exception for specifying a default cert when https config is enabled + await host.StartAsync(); + await host.StopAsync(); + } + + [Theory] + [InlineData("http://127.0.0.1:0", true)] + [InlineData("http://127.0.0.1:0", false)] + [InlineData("https://127.0.0.1:0", true)] + [InlineData("https://127.0.0.1:0", false)] + public async Task LoadEndpointCertificate(string address, bool useKestrelHttpsConfiguration) + { + var hostBuilder = new WebHostBuilder() + .UseKestrelCore() + .ConfigureKestrel(serverOptions => + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:end1:Url", address), + new KeyValuePair("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx")), + new KeyValuePair("Certificates:Default:Password", "testPassword"), + }).Build(); + serverOptions.Configure(config); + }) + .Configure(app => { }); + + if (useKestrelHttpsConfiguration) + { + hostBuilder.UseKestrelHttpsConfiguration(); + } + + var host = hostBuilder.Build(); + + if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useKestrelHttpsConfiguration) + { + Assert.Throws(host.Run); + } + else + { + // Binding succeeds + await host.StartAsync(); + await host.StopAsync(); + } + } + + [Fact] + public async Task UseHttpsJustWorks() + { + var hostBuilder = new WebHostBuilder() + .UseKestrelCore() + .ConfigureKestrel(serverOptions => + { + serverOptions.TestOverrideDefaultCertificate = new X509Certificate2(Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx"), "testPassword"); + + serverOptions.ListenAnyIP(0, listenOptions => + { + listenOptions.UseHttps(); + }); + }) + .Configure(app => { }); + + var host = hostBuilder.Build(); + + // Binding succeeds + await host.StartAsync(); + await host.StopAsync(); + + Assert.True(host.Services.GetRequiredService().IsInitialized); + } + + [Fact] + public async Task UseHttpsMayNotImplyUseKestrelHttpsConfiguration() + { + var hostBuilder = new WebHostBuilder() + .UseKestrelCore() + .ConfigureKestrel(serverOptions => + { + serverOptions.ListenAnyIP(0, listenOptions => + { + listenOptions.UseHttps(new HttpsConnectionAdapterOptions() + { + ServerCertificate = new X509Certificate2(Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx"), "testPassword"), + }); + }); + }) + .Configure(app => { }); + + var host = hostBuilder.Build(); + + // Binding succeeds + await host.StartAsync(); + await host.StopAsync(); + + // This is more documentary than normative + Assert.False(host.Services.GetRequiredService().IsInitialized); + } +} diff --git a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs index 8f1f535d381d..5b033b4952ef 100644 --- a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs @@ -27,6 +27,8 @@ private KestrelServerOptions CreateServerOptions() .AddLogging() .AddSingleton(env) .AddSingleton(new KestrelMetrics(new TestMeterFactory())) + .AddSingleton() + .AddSingleton() .BuildServiceProvider(); return serverOptions; } diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs index 5d73e667c5a3..27c6eac5b731 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs @@ -84,6 +84,8 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action(this); services.AddSingleton(context.LoggerFactory); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(sp => { // Manually configure options on the TestServiceContext. @@ -94,7 +96,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action(), Array.Empty(), context); + return new KestrelServerImpl(sp.GetServices(), Array.Empty(), sp.GetRequiredService(), context); }); configureServices(services); }) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index 4557297b8a67..b5c62a11a3b6 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -35,9 +35,15 @@ public class HttpsConnectionMiddlewareTests : LoggedTest private static KestrelServerOptions CreateServerOptions() { + var env = new Mock(); + env.SetupGet(e => e.ContentRootPath).Returns(Directory.GetCurrentDirectory()); + var serverOptions = new KestrelServerOptions(); serverOptions.ApplicationServices = new ServiceCollection() .AddLogging() + .AddSingleton() + .AddSingleton() + .AddSingleton(env.Object) .AddSingleton(new KestrelMetrics(new TestMeterFactory())) .BuildServiceProvider(); return serverOptions; @@ -73,14 +79,9 @@ public async Task CanReadAndWriteWithHttpsConnectionMiddlewareWithPemCertificate ["Certificates:Default:Password"] = "aspnetcore", }).Build(); - var env = new Mock(); - env.SetupGet(e => e.ContentRootPath).Returns(Directory.GetCurrentDirectory()); - var options = CreateServerOptions(); - var logger = options.ApplicationServices.GetRequiredService>(); - var httpsLogger = options.ApplicationServices.GetRequiredService>(); - var loader = new KestrelConfigurationLoader(options, configuration, env.Object, reloadOnChange: false, logger, httpsLogger); + var loader = new KestrelConfigurationLoader(options, configuration, options.ApplicationServices.GetRequiredService(), reloadOnChange: false); 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/HttpsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs index 6d39bff9dfb7..819def76e5a2 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs @@ -21,10 +21,12 @@ using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Metrics; using Microsoft.Extensions.Options; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -35,9 +37,15 @@ public class HttpsTests : LoggedTest private static KestrelServerOptions CreateServerOptions() { + // It's not actually going to be used - we just need to satisfy the check in ApplyDefaultCertificate + var mockHttpsConfig = new Mock(); + mockHttpsConfig.Setup(m => m.IsInitialized).Returns(true); + var serverOptions = new KestrelServerOptions(); serverOptions.ApplicationServices = new ServiceCollection() .AddLogging() + .AddSingleton(mockHttpsConfig.Object) + .AddSingleton(Mock.Of()) .AddSingleton(new KestrelMetrics(new TestMeterFactory())) .BuildServiceProvider(); return serverOptions; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs index ee9e1efa529d..21349b65de6a 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs @@ -87,6 +87,8 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action(this); services.AddSingleton(context.LoggerFactory); services.AddSingleton(context.Metrics); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(sp => { @@ -95,6 +97,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action(), + sp.GetRequiredService(), context); }); }); diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs index 6226bf1660c0..8efd8e2789bd 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs @@ -10,6 +10,8 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Testing; +using Microsoft.CSharp.RuntimeBinder; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Xunit; @@ -329,6 +331,97 @@ public async Task TlsHandshakeCallbackOptions_Invoked() await host.StopAsync().DefaultTimeout(); } + [ConditionalTheory] + [MsQuicSupported] + [InlineData(true, true, true)] + [InlineData(true, true, false)] + [InlineData(true, false, false)] + [InlineData(false, true, true)] + [InlineData(false, true, false)] + [InlineData(false, false, false)] + public async Task UseKestrelCore_CodeBased(bool useQuic, bool useHttps, bool useHttpsEnablesHttpsConfiguration) + { + var hostBuilder = new WebHostBuilder() + .UseKestrelCore() + .ConfigureKestrel(serverOptions => + { + serverOptions.ListenAnyIP(0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http3; + if (useHttps) + { + if (useHttpsEnablesHttpsConfiguration) + { + listenOptions.UseHttps(httpsOptions => + { + httpsOptions.ServerCertificate = TestResources.GetTestCertificate(); + }); + } + else + { + // Specifically choose an overload that doesn't enable https configuration + listenOptions.UseHttps(new HttpsConnectionAdapterOptions + { + ServerCertificate = TestResources.GetTestCertificate() + }); + } + } + }); + }) + .Configure(app => { }); + + if (useQuic) + { + hostBuilder.UseQuic(); + } + + var host = hostBuilder.Build(); + + if (useHttps && useHttpsEnablesHttpsConfiguration && useQuic) + { + // Binding succeeds + await host.StartAsync(); + await host.StopAsync(); + } + else + { + // This *could* work for `useHttps && !useHttpsEnablesHttpsConfiguration` if `UseQuic` implied `UseKestrelHttpsConfiguration` + Assert.Throws(host.Run); + } + } + + [ConditionalTheory] + [MsQuicSupported] + [InlineData(true)] + [InlineData(false)] + public void UseKestrelCore_ConfigurationBased(bool useQuic) + { + var hostBuilder = new WebHostBuilder() + .UseKestrelCore() + .ConfigureKestrel(serverOptions => + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:end1:Url", "https://127.0.0.1:0"), + new KeyValuePair("Endpoints:end1:Protocols", "Http3"), + new KeyValuePair("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx")), + new KeyValuePair("Certificates:Default:Password", "testPassword"), + }).Build(); + serverOptions.Configure(config); + }) + .Configure(app => { }); + + if (useQuic) + { + hostBuilder.UseQuic(); + } + + var host = hostBuilder.Build(); + + // This *could* work (in some cases) if `UseQuic` implied `UseKestrelHttpsConfiguration` + Assert.Throws(host.Run); + } + private IHostBuilder CreateHostBuilder(RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action configureKestrel = null) { return HttpHelpers.CreateHostBuilder(AddTestLogging, requestDelegate, protocol, configureKestrel);