From 7843321ff3990435e5e2dff45bd75d222e9c8c44 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Fri, 24 Mar 2023 16:51:36 -0700 Subject: [PATCH 1/9] Make TLS & QUIC pay-for-play `CreateSlimBuilder` now calls `UseKestrelSlim` (name TBD) to create a Kestrel server that doesn't automatically support TLS or QUIC. If you invoke `ListenOptions.UseHttps`, TLS will "just work" but, if you want to enable https using `ASPNETCORE_URLS`, you'll need to invoke the new `WebHostBuilder.UseHttpsConfiguration` extension method. Quic is enabled using the existing `WebHostBuilder.UseQuic` extension method. Squash: Break direct dependency of AddressBinder on ListenOptionsHttpsExtensions.UseHttps Factor out the part of TransportManager that depends on https Introduce KestrelServerOptions.HasServerCertificateOrSelector for convenience Factor TlsConfigurationLoader out of KestrelConfigurationLoader Introduce but don't consume IHttpsConfigurationHelper Consume IHttpsConfigurationHelper - tests failing Fix most tests Fix KestrelServerTests Fix remaining tests Respect IHttpsConfigurationHelper in ApplyDefaultCertificate Introduce UseKestrelSlim Delete unused TryUseHttps Enable HttpsConfiguration when UseHttps is called Introduce UseHttpsConfiguration Drop incomplete test implementation of IHttpsConfigurationHelper Tidy up test diffs Fix AOT trimming by moving enable call out of ctor Fix some tests Simplify HttpsConfigurationHelper ctor for more convenient testing Improve error message Don't declare Enabler transient Fix tests other than KestrelConfigurationLoaderTests Correct HttpsConfigurationHelper Add IEnabler interface to break direct dependency Restore UseKestrel call in WebHost.ConfigureWebDefaults Stop registering an https address in ApiTemplateTest boolean -> bool HttpsConfigurationHelper -> HttpsConfigurationService HttpsConfigurationService.Enable -> Initialize ITlsConfigurationLoader.ApplyHttpsDefaults -> ApplyHttpsConfiguration ITlsConfigurationLoader.UseHttps -> UseHttpsWithSni IHttpsConfigurationService.UseHttps -> UseHttpsWithDefaults Inline ITlsConfigurationLoader in IHttpsConfigurationService Document IHttpsConfigurationService Document new public APIs in WebHostBuilderKestrelExtensions Clean up TODOs Improve error text recommending UseQuic Co-authored-by: James Newton-King Add assert message Clarify comment on assert Fix typo in doc comment Co-authored-by: Aditya Mandaleeka Fix typo in doc comment Co-authored-by: Aditya Mandaleeka Fix typo in doc comment Co-authored-by: Aditya Mandaleeka Don't use regions Correct casing Replace record with readonly struct Test AddressBinder exception Test an endpoint address from config Test certificate loading Bonus: use dynamic ports to improve reliability Test Quic with UseKestrelSlim Test the interaction of UseHttps and UseHttpsConfiguration Test different UseHttps overloads Add more detail to doc comment Set TestOverrideDefaultCertificate in the tests that expect it --- src/DefaultBuilder/src/WebHost.cs | 29 +- src/ProjectTemplates/Shared/Project.cs | 9 +- .../test/Templates.Tests/ApiTemplateTest.cs | 5 +- src/Servers/Kestrel/Core/src/CoreStrings.resx | 5 +- .../Core/src/HttpsConfigurationService.cs | 252 ++++++++++++++++++ .../Core/src/HttpsConnectionAdapterOptions.cs | 5 + .../Core/src/IHttpsConfigurationService.cs | 100 +++++++ .../Core/src/Internal/AddressBinder.cs | 24 +- .../Infrastructure/TransportManager.cs | 82 +----- .../Core/src/Internal/KestrelServerImpl.cs | 12 +- .../Core/src/Internal/SniOptionsSelector.cs | 2 +- .../Core/src/KestrelConfigurationLoader.cs | 187 ++----------- src/Servers/Kestrel/Core/src/KestrelServer.cs | 45 ++++ .../Kestrel/Core/src/KestrelServerOptions.cs | 26 +- .../Core/src/ListenOptionsHttpsExtensions.cs | 21 +- .../Middleware/HttpsConnectionMiddleware.cs | 2 +- .../Core/src/TlsConfigurationLoader.cs | 205 ++++++++++++++ .../Kestrel/Core/test/AddressBinderTests.cs | 22 +- .../Core/test/KestrelServerOptionsTests.cs | 1 + .../Kestrel/Core/test/KestrelServerTests.cs | 20 +- .../Kestrel/src/PublicAPI.Unshipped.txt | 4 + .../src/WebHostBuilderKestrelExtensions.cs | 96 ++++++- .../Kestrel/test/HttpsConfigurationTests.cs | 231 ++++++++++++++++ .../test/KestrelConfigurationLoaderTests.cs | 2 + .../test/TransportTestHelpers/TestServer.cs | 4 +- .../HttpsConnectionMiddlewareTests.cs | 13 +- .../InMemory.FunctionalTests/HttpsTests.cs | 8 + .../TestTransport/TestServer.cs | 3 + .../Http3/Http3TlsTests.cs | 91 +++++++ 29 files changed, 1175 insertions(+), 331 deletions(-) create mode 100644 src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs create mode 100644 src/Servers/Kestrel/Core/src/IHttpsConfigurationService.cs create mode 100644 src/Servers/Kestrel/Core/src/TlsConfigurationLoader.cs create mode 100644 src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index 7e21713ae774..a4d8ad33e886 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.UseKestrelSlim(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..835fe26567da 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -722,4 +722,7 @@ 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 UseHttpsConfiguration() on IWebHostBuilder to enable this functionality. + + diff --git a/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs b/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs new file mode 100644 index 000000000000..d45a86aba45a --- /dev/null +++ b/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs @@ -0,0 +1,252 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +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(); + _tlsConfigurationLoader!.ApplyHttpsConfiguration(httpsOptions, endpoint, serverOptions, defaultCertificateConfig, configurationReader); + } + + /// + public ListenOptions UseHttpsWithSni(ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint) + { + EnsureInitialized(); + return _tlsConfigurationLoader!.UseHttpsWithSni(listenOptions, httpsOptions, endpoint); + } + + /// + public CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader) + { + EnsureInitialized(); + return _tlsConfigurationLoader!.LoadDefaultCertificate(configurationReader); + } + + /// + public void PopulateMultiplexedTransportFeatures(FeatureCollection features, ListenOptions listenOptions) + { + EnsureInitialized(); + _populateMultiplexedTransportFeatures!.Invoke(features, listenOptions); + } + + /// + public ListenOptions UseHttpsWithDefaults(ListenOptions listenOptions) + { + EnsureInitialized(); + return _useHttpsWithDefaults!.Invoke(listenOptions); + } + + /// + /// If this instance has not been initialized, initialize it if possible and throw otherwise. + /// + /// If initialization is not possible. + private void EnsureInitialized() + { + if (!_isInitialized) + { + if (_initializer is not null) + { + _initializer.Initialize(this); + } + else + { + throw new InvalidOperationException(CoreStrings.NeedHttpsConfiguration); + } + } + } + + /// + /// 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 @@ public KestrelServerImpl( 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 @@ public KestrelServerImpl( 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 KestrelServerImpl( _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 @@ 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/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 @@ 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; } = 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..09f69b9b3a07 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..93489194db55 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 @@ private static KestrelServerImpl CreateKestrelServer( 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..864764cdb418 100644 --- a/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ #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! +static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelSlim(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! configureOptions) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelSlim(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! options) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! diff --git a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index f4563b222809..5bc8966b6521 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 UseHttpsConfiguration(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 + .UseKestrelSlim() + .UseHttpsConfiguration() + .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 UseKestrelSlim(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(); @@ -73,6 +115,26 @@ public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder, Actio return hostBuilder.UseKestrel().ConfigureKestrel(options); } + /// + /// 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 instructive error will be reported. + /// + /// + /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder to configure. + /// + /// + /// A callback to configure Kestrel options. + /// + /// + /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder. + /// + public static IWebHostBuilder UseKestrelSlim(this IWebHostBuilder hostBuilder, Action options) + { + return hostBuilder.UseKestrelSlim().ConfigureKestrel(options); + } + /// /// Configures Kestrel options but does not register an IServer. See . /// @@ -109,6 +171,24 @@ public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder, Actio return hostBuilder.UseKestrel().ConfigureKestrel(configureOptions); } + /// + /// 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 instructive error will be reported. + /// + /// + /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder to configure. + /// + /// A callback to configure Kestrel options. + /// + /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder. + /// + public static IWebHostBuilder UseKestrelSlim(this IWebHostBuilder hostBuilder, Action configureOptions) + { + return hostBuilder.UseKestrelSlim().ConfigureKestrel(configureOptions); + } + /// /// Configures Kestrel options but does not register an IServer. See . /// diff --git a/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs b/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs new file mode 100644 index 000000000000..d828f912242e --- /dev/null +++ b/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs @@ -0,0 +1,231 @@ +// 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 useHttpsConfiguration) + { + var hostBuilder = new WebHostBuilder() + .UseKestrelSlim(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 (useHttpsConfiguration) + { + hostBuilder.UseHttpsConfiguration(); + } + + var host = hostBuilder.Build(); + + Assert.Single(host.ServerFeatures.Get().Addresses, address); + + if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useHttpsConfiguration) + { + 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() + .UseKestrelSlim() + .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 useHttpsConfiguration) + { + var hostBuilder = new WebHostBuilder() + .UseKestrelSlim(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 (useHttpsConfiguration) + { + hostBuilder.UseHttpsConfiguration(); + } + + var host = hostBuilder.Build(); + + if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useHttpsConfiguration) + { + Assert.Throws(host.Run); + } + else + { + // Binding succeeds + await host.StartAsync(); + await host.StopAsync(); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task LoadDefaultCertificate(bool useHttpsConfiguration) + { + var hostBuilder = new WebHostBuilder() + .UseKestrelSlim(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 (useHttpsConfiguration) + { + hostBuilder.UseHttpsConfiguration(); + } + + 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 useHttpsConfiguration) + { + var hostBuilder = new WebHostBuilder() + .UseKestrelSlim(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 (useHttpsConfiguration) + { + hostBuilder.UseHttpsConfiguration(); + } + + var host = hostBuilder.Build(); + + if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useHttpsConfiguration) + { + Assert.Throws(host.Run); + } + else + { + // Binding succeeds + await host.StartAsync(); + await host.StopAsync(); + } + } + + [Fact] + public async Task UseHttpsJustWorks() + { + var hostBuilder = new WebHostBuilder() + .UseKestrelSlim(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 UseHttpsMayNotImplyUseHttpsConfiguration() + { + var hostBuilder = new WebHostBuilder() + .UseKestrelSlim(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..7a689709d7f9 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,95 @@ 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 UseKestrelSlim_CodeBased(bool useQuic, bool useHttps, bool useHttpsEnablesHttpsConfiguration) + { + var hostBuilder = new WebHostBuilder() + .UseKestrelSlim(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 `UseHttpsConfiguration` + Assert.Throws(host.Run); + } + } + + [ConditionalTheory] + [MsQuicSupported] + [InlineData(true)] + [InlineData(false)] + public void UseKestrelSlim_ConfigurationBased(bool useQuic) + { + var hostBuilder = new WebHostBuilder() + .UseKestrelSlim(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 `UseHttpsConfiguration` + Assert.Throws(host.Run); + } + private IHostBuilder CreateHostBuilder(RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action configureKestrel = null) { return HttpHelpers.CreateHostBuilder(AddTestLogging, requestDelegate, protocol, configureKestrel); From 7aea16b5665170a83e8f32b599fc4750dd57d880 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Thu, 13 Apr 2023 10:42:58 -0700 Subject: [PATCH 2/9] Improve assert message Co-authored-by: James Newton-King --- src/Servers/Kestrel/Core/src/KestrelServerOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index 09f69b9b3a07..b44e3b33adcf 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -257,7 +257,7 @@ internal void ApplyDefaultCertificate(HttpsConnectionAdapterOptions httpsOptions // 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"); + Debug.Assert(ApplicationServices.GetRequiredService().IsInitialized, "HTTPS configuration should have been enabled"); if (TestOverrideDefaultCertificate is X509Certificate2 certificateFromTest) { From 7b94e52e8146a2e8e9c1533c84131d763fdb89a4 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Thu, 13 Apr 2023 13:39:53 -0700 Subject: [PATCH 3/9] Adopt MemberNotNullAttribute --- .../Kestrel/Core/src/HttpsConfigurationService.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs b/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs index d45a86aba45a..d92cecfef167 100644 --- a/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs +++ b/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; using System.Net; using System.Net.Security; @@ -76,41 +77,42 @@ public void ApplyHttpsConfiguration( ConfigurationReader configurationReader) { EnsureInitialized(); - _tlsConfigurationLoader!.ApplyHttpsConfiguration(httpsOptions, endpoint, serverOptions, defaultCertificateConfig, configurationReader); + _tlsConfigurationLoader.ApplyHttpsConfiguration(httpsOptions, endpoint, serverOptions, defaultCertificateConfig, configurationReader); } /// public ListenOptions UseHttpsWithSni(ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint) { EnsureInitialized(); - return _tlsConfigurationLoader!.UseHttpsWithSni(listenOptions, httpsOptions, endpoint); + return _tlsConfigurationLoader.UseHttpsWithSni(listenOptions, httpsOptions, endpoint); } /// public CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader) { EnsureInitialized(); - return _tlsConfigurationLoader!.LoadDefaultCertificate(configurationReader); + return _tlsConfigurationLoader.LoadDefaultCertificate(configurationReader); } /// public void PopulateMultiplexedTransportFeatures(FeatureCollection features, ListenOptions listenOptions) { EnsureInitialized(); - _populateMultiplexedTransportFeatures!.Invoke(features, listenOptions); + _populateMultiplexedTransportFeatures.Invoke(features, listenOptions); } /// public ListenOptions UseHttpsWithDefaults(ListenOptions listenOptions) { EnsureInitialized(); - return _useHttpsWithDefaults!.Invoke(listenOptions); + 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() { if (!_isInitialized) From a269b08fe086ba27abf3a65a1d85c2ca898a6853 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Thu, 13 Apr 2023 16:33:24 -0700 Subject: [PATCH 4/9] Assert that members are non-null to suppress CS8774 --- src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs b/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs index d92cecfef167..9d7c8d2fb02a 100644 --- a/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs +++ b/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; using System.Net; @@ -126,6 +127,10 @@ private void EnsureInitialized() throw new InvalidOperationException(CoreStrings.NeedHttpsConfiguration); } } + + Debug.Assert(_useHttpsWithDefaults is not null); + Debug.Assert(_tlsConfigurationLoader is not null); + Debug.Assert(_populateMultiplexedTransportFeatures is not null); } /// From 367d0527b0a0fa93bbb86e7d40a079f10a7cfa4c Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Thu, 13 Apr 2023 16:14:54 -0700 Subject: [PATCH 5/9] UseHttpsConfiguration -> UseKestrelHttpsConfiguration --- src/Servers/Kestrel/Core/src/CoreStrings.resx | 2 +- .../Kestrel/src/PublicAPI.Unshipped.txt | 2 +- .../src/WebHostBuilderKestrelExtensions.cs | 6 ++-- .../Kestrel/test/HttpsConfigurationTests.cs | 32 +++++++++---------- .../Http3/Http3TlsTests.cs | 4 +-- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index 835fe26567da..472154909ea1 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -723,6 +723,6 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l Failed to bind to http://[::]:{port} (IPv6Any). - Call UseHttpsConfiguration() on IWebHostBuilder to enable this functionality. + Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable this functionality. diff --git a/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt index 864764cdb418..2123a02627d0 100644 --- a/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable -static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseHttpsConfiguration(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelHttpsConfiguration(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! static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelSlim(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! configureOptions) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelSlim(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! options) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! diff --git a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index 5bc8966b6521..eb58ec39307a 100644 --- a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs +++ b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs @@ -22,7 +22,7 @@ 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 + /// an https:// address, will enable configuration /// of HTTPS on that endpoint. /// /// Has no effect in scenarios. @@ -33,7 +33,7 @@ public static class WebHostBuilderKestrelExtensions /// /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder. /// - public static IWebHostBuilder UseHttpsConfiguration(this IWebHostBuilder hostBuilder) + public static IWebHostBuilder UseKestrelHttpsConfiguration(this IWebHostBuilder hostBuilder) { return hostBuilder.ConfigureServices(services => { @@ -54,7 +54,7 @@ public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder) { return hostBuilder .UseKestrelSlim() - .UseHttpsConfiguration() + .UseKestrelHttpsConfiguration() .UseQuic(options => { // Configure server defaults to match client defaults. diff --git a/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs b/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs index d828f912242e..f143084419d6 100644 --- a/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs @@ -19,7 +19,7 @@ public class HttpsConfigurationTests [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 useHttpsConfiguration) + public async Task BindAddressFromSetting(string address, bool useKestrelHttpsConfiguration) { var hostBuilder = new WebHostBuilder() .UseKestrelSlim(serverOptions => @@ -31,16 +31,16 @@ public async Task BindAddressFromSetting(string address, bool useHttpsConfigurat // This is what ASPNETCORE_URLS would populate hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey, address); - if (useHttpsConfiguration) + if (useKestrelHttpsConfiguration) { - hostBuilder.UseHttpsConfiguration(); + hostBuilder.UseKestrelHttpsConfiguration(); } var host = hostBuilder.Build(); Assert.Single(host.ServerFeatures.Get().Addresses, address); - if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useHttpsConfiguration) + if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useKestrelHttpsConfiguration) { Assert.Throws(host.Run); } @@ -77,7 +77,7 @@ public void NoFallbackToHttpAddress() [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 useHttpsConfiguration) + public async Task BindAddressFromEndpoint(string address, bool useKestrelHttpsConfiguration) { var hostBuilder = new WebHostBuilder() .UseKestrelSlim(serverOptions => @@ -92,14 +92,14 @@ public async Task BindAddressFromEndpoint(string address, bool useHttpsConfigura }) .Configure(app => { }); - if (useHttpsConfiguration) + if (useKestrelHttpsConfiguration) { - hostBuilder.UseHttpsConfiguration(); + hostBuilder.UseKestrelHttpsConfiguration(); } var host = hostBuilder.Build(); - if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useHttpsConfiguration) + if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useKestrelHttpsConfiguration) { Assert.Throws(host.Run); } @@ -114,7 +114,7 @@ public async Task BindAddressFromEndpoint(string address, bool useHttpsConfigura [Theory] [InlineData(true)] [InlineData(false)] - public async Task LoadDefaultCertificate(bool useHttpsConfiguration) + public async Task LoadDefaultCertificate(bool useKestrelHttpsConfiguration) { var hostBuilder = new WebHostBuilder() .UseKestrelSlim(serverOptions => @@ -128,9 +128,9 @@ public async Task LoadDefaultCertificate(bool useHttpsConfiguration) }) .Configure(app => { }); - if (useHttpsConfiguration) + if (useKestrelHttpsConfiguration) { - hostBuilder.UseHttpsConfiguration(); + hostBuilder.UseKestrelHttpsConfiguration(); } var host = hostBuilder.Build(); @@ -145,7 +145,7 @@ public async Task LoadDefaultCertificate(bool useHttpsConfiguration) [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 useHttpsConfiguration) + public async Task LoadEndpointCertificate(string address, bool useKestrelHttpsConfiguration) { var hostBuilder = new WebHostBuilder() .UseKestrelSlim(serverOptions => @@ -160,14 +160,14 @@ public async Task LoadEndpointCertificate(string address, bool useHttpsConfigura }) .Configure(app => { }); - if (useHttpsConfiguration) + if (useKestrelHttpsConfiguration) { - hostBuilder.UseHttpsConfiguration(); + hostBuilder.UseKestrelHttpsConfiguration(); } var host = hostBuilder.Build(); - if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useHttpsConfiguration) + if (address.StartsWith("https", StringComparison.OrdinalIgnoreCase) && !useKestrelHttpsConfiguration) { Assert.Throws(host.Run); } @@ -204,7 +204,7 @@ public async Task UseHttpsJustWorks() } [Fact] - public async Task UseHttpsMayNotImplyUseHttpsConfiguration() + public async Task UseHttpsMayNotImplyUseKestrelHttpsConfiguration() { var hostBuilder = new WebHostBuilder() .UseKestrelSlim(serverOptions => diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs index 7a689709d7f9..0bbe94ba0edd 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs @@ -384,7 +384,7 @@ public async Task UseKestrelSlim_CodeBased(bool useQuic, bool useHttps, bool use } else { - // This *could* work for `useHttps && !useHttpsEnablesHttpsConfiguration` if `UseQuic` implied `UseHttpsConfiguration` + // This *could* work for `useHttps && !useHttpsEnablesHttpsConfiguration` if `UseQuic` implied `UseKestrelHttpsConfiguration` Assert.Throws(host.Run); } } @@ -416,7 +416,7 @@ public void UseKestrelSlim_ConfigurationBased(bool useQuic) var host = hostBuilder.Build(); - // This *could* work (in some cases) if `UseQuic` implied `UseHttpsConfiguration` + // This *could* work (in some cases) if `UseQuic` implied `UseKestrelHttpsConfiguration` Assert.Throws(host.Run); } From 7696f0947742df519ebb2a7c6bf9cac1d9a81efa Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Thu, 13 Apr 2023 16:16:49 -0700 Subject: [PATCH 6/9] UseKestrelSlim -> UseKestrelCore --- src/DefaultBuilder/src/WebHost.cs | 2 +- .../Kestrel/Kestrel/src/PublicAPI.Unshipped.txt | 6 +++--- .../Kestrel/src/WebHostBuilderKestrelExtensions.cs | 14 +++++++------- .../Kestrel/test/HttpsConfigurationTests.cs | 14 +++++++------- .../Interop.FunctionalTests/Http3/Http3TlsTests.cs | 8 ++++---- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index a4d8ad33e886..81642e2e4ae3 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -238,7 +238,7 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder) internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder) { - ConfigureWebDefaultsWorker(builder.UseKestrelSlim(ConfigureKestrel), configureRouting: null); + ConfigureWebDefaultsWorker(builder.UseKestrelCore(ConfigureKestrel), configureRouting: null); } private static void ConfigureKestrel(WebHostBuilderContext builderContext, KestrelServerOptions options) diff --git a/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt index 2123a02627d0..4c1dd1049ac5 100644 --- a/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelHttpsConfiguration(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! -static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelSlim(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! configureOptions) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! -static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelSlim(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! options) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelCore(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelCore(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! configureOptions) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelCore(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! options) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! diff --git a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index eb58ec39307a..ef49db208fd2 100644 --- a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs +++ b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Hosting; public static class WebHostBuilderKestrelExtensions { /// - /// In scenarios, it may be necessary to explicitly + /// 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. @@ -53,7 +53,7 @@ public static IWebHostBuilder UseKestrelHttpsConfiguration(this IWebHostBuilder public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder) { return hostBuilder - .UseKestrelSlim() + .UseKestrelCore() .UseKestrelHttpsConfiguration() .UseQuic(options => { @@ -77,7 +77,7 @@ public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder) /// /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder. /// - public static IWebHostBuilder UseKestrelSlim(this IWebHostBuilder hostBuilder) + public static IWebHostBuilder UseKestrelCore(this IWebHostBuilder hostBuilder) { hostBuilder.ConfigureServices(services => { @@ -130,9 +130,9 @@ public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder, Actio /// /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder. /// - public static IWebHostBuilder UseKestrelSlim(this IWebHostBuilder hostBuilder, Action options) + public static IWebHostBuilder UseKestrelCore(this IWebHostBuilder hostBuilder, Action options) { - return hostBuilder.UseKestrelSlim().ConfigureKestrel(options); + return hostBuilder.UseKestrelCore().ConfigureKestrel(options); } /// @@ -184,9 +184,9 @@ public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder, Actio /// /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder. /// - public static IWebHostBuilder UseKestrelSlim(this IWebHostBuilder hostBuilder, Action configureOptions) + public static IWebHostBuilder UseKestrelCore(this IWebHostBuilder hostBuilder, Action configureOptions) { - return hostBuilder.UseKestrelSlim().ConfigureKestrel(configureOptions); + return hostBuilder.UseKestrelCore().ConfigureKestrel(configureOptions); } /// diff --git a/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs b/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs index f143084419d6..2b29dfb6dedd 100644 --- a/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs @@ -22,7 +22,7 @@ public class HttpsConfigurationTests public async Task BindAddressFromSetting(string address, bool useKestrelHttpsConfiguration) { var hostBuilder = new WebHostBuilder() - .UseKestrelSlim(serverOptions => + .UseKestrelCore(serverOptions => { serverOptions.TestOverrideDefaultCertificate = new X509Certificate2(Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx"), "testPassword"); }) @@ -59,7 +59,7 @@ public void NoFallbackToHttpAddress() const string httpsAddress = "https://localhost:5001"; var hostBuilder = new WebHostBuilder() - .UseKestrelSlim() + .UseKestrelCore() .Configure(app => { }); // This is what ASPNETCORE_URLS would populate @@ -80,7 +80,7 @@ public void NoFallbackToHttpAddress() public async Task BindAddressFromEndpoint(string address, bool useKestrelHttpsConfiguration) { var hostBuilder = new WebHostBuilder() - .UseKestrelSlim(serverOptions => + .UseKestrelCore(serverOptions => { var config = new ConfigurationBuilder().AddInMemoryCollection(new[] { @@ -117,7 +117,7 @@ public async Task BindAddressFromEndpoint(string address, bool useKestrelHttpsCo public async Task LoadDefaultCertificate(bool useKestrelHttpsConfiguration) { var hostBuilder = new WebHostBuilder() - .UseKestrelSlim(serverOptions => + .UseKestrelCore(serverOptions => { var config = new ConfigurationBuilder().AddInMemoryCollection(new[] { @@ -148,7 +148,7 @@ public async Task LoadDefaultCertificate(bool useKestrelHttpsConfiguration) public async Task LoadEndpointCertificate(string address, bool useKestrelHttpsConfiguration) { var hostBuilder = new WebHostBuilder() - .UseKestrelSlim(serverOptions => + .UseKestrelCore(serverOptions => { var config = new ConfigurationBuilder().AddInMemoryCollection(new[] { @@ -183,7 +183,7 @@ public async Task LoadEndpointCertificate(string address, bool useKestrelHttpsCo public async Task UseHttpsJustWorks() { var hostBuilder = new WebHostBuilder() - .UseKestrelSlim(serverOptions => + .UseKestrelCore(serverOptions => { serverOptions.TestOverrideDefaultCertificate = new X509Certificate2(Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx"), "testPassword"); @@ -207,7 +207,7 @@ public async Task UseHttpsJustWorks() public async Task UseHttpsMayNotImplyUseKestrelHttpsConfiguration() { var hostBuilder = new WebHostBuilder() - .UseKestrelSlim(serverOptions => + .UseKestrelCore(serverOptions => { serverOptions.ListenAnyIP(0, listenOptions => { diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs index 0bbe94ba0edd..0c1eab72ac8f 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs @@ -339,10 +339,10 @@ public async Task TlsHandshakeCallbackOptions_Invoked() [InlineData(false, true, true)] [InlineData(false, true, false)] [InlineData(false, false, false)] - public async Task UseKestrelSlim_CodeBased(bool useQuic, bool useHttps, bool useHttpsEnablesHttpsConfiguration) + public async Task UseKestrelCore_CodeBased(bool useQuic, bool useHttps, bool useHttpsEnablesHttpsConfiguration) { var hostBuilder = new WebHostBuilder() - .UseKestrelSlim(serverOptions => + .UseKestrelCore(serverOptions => { serverOptions.ListenAnyIP(0, listenOptions => { @@ -393,10 +393,10 @@ public async Task UseKestrelSlim_CodeBased(bool useQuic, bool useHttps, bool use [MsQuicSupported] [InlineData(true)] [InlineData(false)] - public void UseKestrelSlim_ConfigurationBased(bool useQuic) + public void UseKestrelCore_ConfigurationBased(bool useQuic) { var hostBuilder = new WebHostBuilder() - .UseKestrelSlim(serverOptions => + .UseKestrelCore(serverOptions => { var config = new ConfigurationBuilder().AddInMemoryCollection(new[] { From d798e05e538daacc67e72d940ab96de01330c7cc Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Thu, 13 Apr 2023 16:23:02 -0700 Subject: [PATCH 7/9] Drop convenience overloads of UseKestrelCore --- src/DefaultBuilder/src/WebHost.cs | 2 +- .../Kestrel/src/PublicAPI.Unshipped.txt | 2 - .../src/WebHostBuilderKestrelExtensions.cs | 38 ------------------- .../Kestrel/test/HttpsConfigurationTests.cs | 18 ++++++--- .../Http3/Http3TlsTests.cs | 6 ++- 5 files changed, 17 insertions(+), 49 deletions(-) diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index 81642e2e4ae3..0a9906ea415a 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -238,7 +238,7 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder) internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder) { - ConfigureWebDefaultsWorker(builder.UseKestrelCore(ConfigureKestrel), configureRouting: null); + ConfigureWebDefaultsWorker(builder.UseKestrelCore().ConfigureKestrel(ConfigureKestrel), configureRouting: null); } private static void ConfigureKestrel(WebHostBuilderContext builderContext, KestrelServerOptions options) diff --git a/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt index 4c1dd1049ac5..1b0f75d6addc 100644 --- a/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Kestrel/src/PublicAPI.Unshipped.txt @@ -1,5 +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! -static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelCore(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! configureOptions) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! -static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions.UseKestrelCore(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! hostBuilder, System.Action! options) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! diff --git a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index ef49db208fd2..7186715a6c5f 100644 --- a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs +++ b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs @@ -115,26 +115,6 @@ public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder, Actio return hostBuilder.UseKestrel().ConfigureKestrel(options); } - /// - /// 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 instructive error will be reported. - /// - /// - /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder to configure. - /// - /// - /// A callback to configure Kestrel options. - /// - /// - /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder. - /// - public static IWebHostBuilder UseKestrelCore(this IWebHostBuilder hostBuilder, Action options) - { - return hostBuilder.UseKestrelCore().ConfigureKestrel(options); - } - /// /// Configures Kestrel options but does not register an IServer. See . /// @@ -171,24 +151,6 @@ public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder, Actio return hostBuilder.UseKestrel().ConfigureKestrel(configureOptions); } - /// - /// 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 instructive error will be reported. - /// - /// - /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder to configure. - /// - /// A callback to configure Kestrel options. - /// - /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder. - /// - public static IWebHostBuilder UseKestrelCore(this IWebHostBuilder hostBuilder, Action configureOptions) - { - return hostBuilder.UseKestrelCore().ConfigureKestrel(configureOptions); - } - /// /// Configures Kestrel options but does not register an IServer. See . /// diff --git a/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs b/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs index 2b29dfb6dedd..cb27ce5bc210 100644 --- a/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/HttpsConfigurationTests.cs @@ -22,7 +22,8 @@ public class HttpsConfigurationTests public async Task BindAddressFromSetting(string address, bool useKestrelHttpsConfiguration) { var hostBuilder = new WebHostBuilder() - .UseKestrelCore(serverOptions => + .UseKestrelCore() + .ConfigureKestrel(serverOptions => { serverOptions.TestOverrideDefaultCertificate = new X509Certificate2(Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx"), "testPassword"); }) @@ -80,7 +81,8 @@ public void NoFallbackToHttpAddress() public async Task BindAddressFromEndpoint(string address, bool useKestrelHttpsConfiguration) { var hostBuilder = new WebHostBuilder() - .UseKestrelCore(serverOptions => + .UseKestrelCore() + .ConfigureKestrel(serverOptions => { var config = new ConfigurationBuilder().AddInMemoryCollection(new[] { @@ -117,7 +119,8 @@ public async Task BindAddressFromEndpoint(string address, bool useKestrelHttpsCo public async Task LoadDefaultCertificate(bool useKestrelHttpsConfiguration) { var hostBuilder = new WebHostBuilder() - .UseKestrelCore(serverOptions => + .UseKestrelCore() + .ConfigureKestrel(serverOptions => { var config = new ConfigurationBuilder().AddInMemoryCollection(new[] { @@ -148,7 +151,8 @@ public async Task LoadDefaultCertificate(bool useKestrelHttpsConfiguration) public async Task LoadEndpointCertificate(string address, bool useKestrelHttpsConfiguration) { var hostBuilder = new WebHostBuilder() - .UseKestrelCore(serverOptions => + .UseKestrelCore() + .ConfigureKestrel(serverOptions => { var config = new ConfigurationBuilder().AddInMemoryCollection(new[] { @@ -183,7 +187,8 @@ public async Task LoadEndpointCertificate(string address, bool useKestrelHttpsCo public async Task UseHttpsJustWorks() { var hostBuilder = new WebHostBuilder() - .UseKestrelCore(serverOptions => + .UseKestrelCore() + .ConfigureKestrel(serverOptions => { serverOptions.TestOverrideDefaultCertificate = new X509Certificate2(Path.Combine("shared", "TestCertificates", "aspnetdevcert.pfx"), "testPassword"); @@ -207,7 +212,8 @@ public async Task UseHttpsJustWorks() public async Task UseHttpsMayNotImplyUseKestrelHttpsConfiguration() { var hostBuilder = new WebHostBuilder() - .UseKestrelCore(serverOptions => + .UseKestrelCore() + .ConfigureKestrel(serverOptions => { serverOptions.ListenAnyIP(0, listenOptions => { diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs index 0c1eab72ac8f..8efd8e2789bd 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs @@ -342,7 +342,8 @@ public async Task TlsHandshakeCallbackOptions_Invoked() public async Task UseKestrelCore_CodeBased(bool useQuic, bool useHttps, bool useHttpsEnablesHttpsConfiguration) { var hostBuilder = new WebHostBuilder() - .UseKestrelCore(serverOptions => + .UseKestrelCore() + .ConfigureKestrel(serverOptions => { serverOptions.ListenAnyIP(0, listenOptions => { @@ -396,7 +397,8 @@ public async Task UseKestrelCore_CodeBased(bool useQuic, bool useHttps, bool use public void UseKestrelCore_ConfigurationBased(bool useQuic) { var hostBuilder = new WebHostBuilder() - .UseKestrelCore(serverOptions => + .UseKestrelCore() + .ConfigureKestrel(serverOptions => { var config = new ConfigurationBuilder().AddInMemoryCollection(new[] { From 6236efd4d4a89d23f1f46519d7d4abff4e4021f4 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Thu, 13 Apr 2023 16:58:36 -0700 Subject: [PATCH 8/9] Use more explicit error strings in HttpsConfigurationService.EnsureInitialized --- src/Servers/Kestrel/Core/src/CoreStrings.resx | 13 +++++++++++-- .../Kestrel/Core/src/HttpsConfigurationService.cs | 15 ++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index 472154909ea1..2c7c3e8ef18d 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -722,7 +722,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l Failed to bind to http://[::]:{port} (IPv6Any). - - Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable this functionality. + + 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 index 9d7c8d2fb02a..8e6185d4b642 100644 --- a/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs +++ b/src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs @@ -77,35 +77,36 @@ public void ApplyHttpsConfiguration( CertificateConfig? defaultCertificateConfig, ConfigurationReader configurationReader) { - EnsureInitialized(); + EnsureInitialized(CoreStrings.NeedHttpsConfigurationToApplyHttpsConfiguration); _tlsConfigurationLoader.ApplyHttpsConfiguration(httpsOptions, endpoint, serverOptions, defaultCertificateConfig, configurationReader); } /// public ListenOptions UseHttpsWithSni(ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint) { - EnsureInitialized(); + // 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(); + EnsureInitialized(CoreStrings.NeedHttpsConfigurationToLoadDefaultCertificate); return _tlsConfigurationLoader.LoadDefaultCertificate(configurationReader); } /// public void PopulateMultiplexedTransportFeatures(FeatureCollection features, ListenOptions listenOptions) { - EnsureInitialized(); + EnsureInitialized(CoreStrings.NeedHttpsConfigurationToUseHttp3); _populateMultiplexedTransportFeatures.Invoke(features, listenOptions); } /// public ListenOptions UseHttpsWithDefaults(ListenOptions listenOptions) { - EnsureInitialized(); + EnsureInitialized(CoreStrings.NeedHttpsConfigurationToBindHttpsAddresses); return _useHttpsWithDefaults.Invoke(listenOptions); } @@ -114,7 +115,7 @@ public ListenOptions UseHttpsWithDefaults(ListenOptions listenOptions) /// /// If initialization is not possible. [MemberNotNull(nameof(_useHttpsWithDefaults), nameof(_tlsConfigurationLoader), nameof(_populateMultiplexedTransportFeatures))] - private void EnsureInitialized() + private void EnsureInitialized(string uninitializedError) { if (!_isInitialized) { @@ -124,7 +125,7 @@ private void EnsureInitialized() } else { - throw new InvalidOperationException(CoreStrings.NeedHttpsConfiguration); + throw new InvalidOperationException(uninitializedError); } } From 7d44b2bb1c318faf35da4a5ed7a6267c377a560c Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 14 Apr 2023 08:11:08 +0800 Subject: [PATCH 9/9] Update src/Servers/Kestrel/Core/test/KestrelServerTests.cs --- src/Servers/Kestrel/Core/test/KestrelServerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index 93489194db55..47c56fdb5cf0 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -306,7 +306,7 @@ private static KestrelServerImpl CreateKestrelServer( Options.Create(options), transportFactories, multiplexedFactories, - httpsConfigurationService, + httpsConfigurationService, loggerFactory ?? new LoggerFactory(new[] { new KestrelTestLoggerProvider() }), metrics ?? new KestrelMetrics(new TestMeterFactory())); }