Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make TLS & QUIC Pay-for-Play (Redux) #47454

Merged
merged 9 commits into from Apr 14, 2023
29 changes: 19 additions & 10 deletions src/DefaultBuilder/src/WebHost.cs
Expand Up @@ -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;
Expand Down Expand Up @@ -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<IServiceCollection>? configureRouting = null)
internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder)
{
builder.UseKestrel((builderContext, options) =>
{
options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true);
})
.ConfigureServices((hostingContext, services) =>
ConfigureWebDefaultsWorker(builder.UseKestrelCore().ConfigureKestrel(ConfigureKestrel), configureRouting: null);
}

private static void ConfigureKestrel(WebHostBuilderContext builderContext, KestrelServerOptions options)
{
options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true);
}

private static void ConfigureWebDefaultsWorker(IWebHostBuilder builder, Action<IServiceCollection>? configureRouting)
{
builder.ConfigureServices((hostingContext, services) =>
{
// Fallback
services.PostConfigure<HostFilteringOptions>(options =>
Expand Down
9 changes: 5 additions & 4 deletions src/ProjectTemplates/Shared/Project.cs
Expand Up @@ -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
Expand Down Expand Up @@ -181,11 +182,11 @@ internal async Task RunDotNetBuildAsync(IDictionary<string, string> 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<string, string>
{
["ASPNETCORE_URLS"] = _urls,
["ASPNETCORE_URLS"] = noHttps ? _urlsNoHttps : _urls,
["ASPNETCORE_ENVIRONMENT"] = "Development",
["ASPNETCORE_Logging__Console__LogLevel__Default"] = "Debug",
["ASPNETCORE_Logging__Console__LogLevel__System"] = "Debug",
Expand All @@ -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<string, string>
{
["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",
Expand Down
5 changes: 3 additions & 2 deletions src/ProjectTemplates/test/Templates.Tests/ApiTemplateTest.cs
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
14 changes: 13 additions & 1 deletion src/Servers/Kestrel/Core/src/CoreStrings.resx
Expand Up @@ -722,4 +722,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="FailedToBindToIPv6Any" xml:space="preserve">
<value>Failed to bind to http://[::]:{port} (IPv6Any).</value>
</data>
</root>
<data name="NeedHttpsConfigurationToApplyHttpsConfiguration" xml:space="preserve">
<value>Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable loading HTTPS settings from configuration.</value>
</data>
<data name="NeedHttpsConfigurationToLoadDefaultCertificate" xml:space="preserve">
<value>Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable loading the default server certificate from configuration.</value>
</data>
<data name="NeedHttpsConfigurationToUseHttp3" xml:space="preserve">
<value>Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable transport layer security for HTTP/3.</value>
</data>
<data name="NeedHttpsConfigurationToBindHttpsAddresses" xml:space="preserve">
<value>Call UseKestrelHttpsConfiguration() on IWebHostBuilder to automatically enable HTTPS when an https:// address is used.</value>
</data>
</root>
260 changes: 260 additions & 0 deletions src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs
@@ -0,0 +1,260 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO.Pipelines;
using System.Net;
using System.Net.Security;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Server.Kestrel.Core;

/// <inheritdoc />
internal sealed class HttpsConfigurationService : IHttpsConfigurationService
{
private readonly IInitializer? _initializer;
private bool _isInitialized;

private TlsConfigurationLoader? _tlsConfigurationLoader;
private Action<FeatureCollection, ListenOptions>? _populateMultiplexedTransportFeatures;
private Func<ListenOptions, ListenOptions>? _useHttpsWithDefaults;

/// <summary>
/// Create an uninitialized <see cref="HttpsConfigurationService"/>.
/// To initialize it later, call <see cref="Initialize"/>.
/// </summary>
public HttpsConfigurationService()
{
}

/// <summary>
/// Create an initialized <see cref="HttpsConfigurationService"/>.
/// </summary>
/// <remarks>
/// In practice, <see cref="Initialize"/> won't be called until it's needed.
/// </remarks>
public HttpsConfigurationService(IInitializer initializer)
{
_initializer = initializer;
}

/// <inheritdoc />
// 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;

/// <inheritdoc/>
public void Initialize(
IHostEnvironment hostEnvironment,
ILogger<KestrelServer> serverLogger,
ILogger<HttpsConnectionMiddleware> httpsLogger)
{
if (_isInitialized)
{
return;
}

_isInitialized = true;

_tlsConfigurationLoader = new TlsConfigurationLoader(hostEnvironment, serverLogger, httpsLogger);
_populateMultiplexedTransportFeatures = PopulateMultiplexedTransportFeaturesWorker;
_useHttpsWithDefaults = UseHttpsWithDefaultsWorker;
}

/// <inheritdoc/>
public void ApplyHttpsConfiguration(
HttpsConnectionAdapterOptions httpsOptions,
EndpointConfig endpoint,
KestrelServerOptions serverOptions,
CertificateConfig? defaultCertificateConfig,
ConfigurationReader configurationReader)
{
EnsureInitialized(CoreStrings.NeedHttpsConfigurationToApplyHttpsConfiguration);
_tlsConfigurationLoader.ApplyHttpsConfiguration(httpsOptions, endpoint, serverOptions, defaultCertificateConfig, configurationReader);
}

/// <inheritdoc/>
public ListenOptions UseHttpsWithSni(ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint)
{
// This doesn't get a distinct string since it won't actually throw - it's always called after ApplyHttpsConfiguration
EnsureInitialized(CoreStrings.NeedHttpsConfigurationToApplyHttpsConfiguration);
return _tlsConfigurationLoader.UseHttpsWithSni(listenOptions, httpsOptions, endpoint);
}

/// <inheritdoc/>
public CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader)
{
EnsureInitialized(CoreStrings.NeedHttpsConfigurationToLoadDefaultCertificate);
return _tlsConfigurationLoader.LoadDefaultCertificate(configurationReader);
}

/// <inheritdoc/>
public void PopulateMultiplexedTransportFeatures(FeatureCollection features, ListenOptions listenOptions)
{
EnsureInitialized(CoreStrings.NeedHttpsConfigurationToUseHttp3);
_populateMultiplexedTransportFeatures.Invoke(features, listenOptions);
}

/// <inheritdoc/>
public ListenOptions UseHttpsWithDefaults(ListenOptions listenOptions)
{
EnsureInitialized(CoreStrings.NeedHttpsConfigurationToBindHttpsAddresses);
return _useHttpsWithDefaults.Invoke(listenOptions);
}

/// <summary>
/// If this instance has not been initialized, initialize it if possible and throw otherwise.
/// </summary>
/// <exception cref="InvalidOperationException">If initialization is not possible.</exception>
[MemberNotNull(nameof(_useHttpsWithDefaults), nameof(_tlsConfigurationLoader), nameof(_populateMultiplexedTransportFeatures))]
private void EnsureInitialized(string uninitializedError)
{
if (!_isInitialized)
{
if (_initializer is not null)
{
_initializer.Initialize(this);
}
else
{
throw new InvalidOperationException(uninitializedError);
}
}

Debug.Assert(_useHttpsWithDefaults is not null);
Debug.Assert(_tlsConfigurationLoader is not null);
Debug.Assert(_populateMultiplexedTransportFeatures is not null);
}

/// <summary>
/// The initialized implementation of <see cref="PopulateMultiplexedTransportFeatures"/>.
/// </summary>
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> { SslApplicationProtocol.Http3 },
OnConnection = (context, cancellationToken) => ValueTask.FromResult(sslServerAuthenticationOptions),
OnConnectionState = null,
});
}
else if (listenOptions.HttpsCallbackOptions != null)
{
features.Set(new TlsConnectionCallbackOptions
{
ApplicationProtocols = new List<SslApplicationProtocol> { 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,
});
}
}

/// <summary>
/// The initialized implementation of <see cref="UseHttpsWithDefaults"/>.
/// </summary>
internal static ListenOptions UseHttpsWithDefaultsWorker(ListenOptions listenOptions)
{
return listenOptions.UseHttps();
}

/// <summary>
/// TlsHandshakeCallbackContext.Connection is ConnectionContext but QUIC connection only implements BaseConnectionContext.
/// </summary>
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<object, object?> 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();
}

/// <summary>
/// Register an instance of this type to initialize registered instances of <see cref="HttpsConfigurationService"/>.
/// </summary>
internal interface IInitializer
{
/// <summary>
/// Invokes <see cref="IHttpsConfigurationService.Initialize"/>, passing appropriate arguments.
/// </summary>
void Initialize(IHttpsConfigurationService httpsConfigurationService);
}

/// <inheritdoc/>
internal sealed class Initializer : IInitializer
{
private readonly IHostEnvironment _hostEnvironment;
private readonly ILogger<KestrelServer> _serverLogger;
private readonly ILogger<HttpsConnectionMiddleware> _httpsLogger;

public Initializer(
IHostEnvironment hostEnvironment,
ILogger<KestrelServer> serverLogger,
ILogger<HttpsConnectionMiddleware> httpsLogger)
{
_hostEnvironment = hostEnvironment;
_serverLogger = serverLogger;
_httpsLogger = httpsLogger;
}

/// <inheritdoc/>
public void Initialize(IHttpsConfigurationService httpsConfigurationService)
{
httpsConfigurationService.Initialize(_hostEnvironment, _serverLogger, _httpsLogger);
}
}
}

Expand Up @@ -55,6 +55,11 @@ public HttpsConnectionAdapterOptions()
/// </summary>
public Func<ConnectionContext?, string?, X509Certificate2?>? ServerCertificateSelector { get; set; }

/// <summary>
/// Convenient shorthand for a common check.
/// </summary>
internal bool HasServerCertificateOrSelector => ServerCertificate is not null || ServerCertificateSelector is not null;
amcasey marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Specifies the client certificate requirements for a HTTPS connection. Defaults to <see cref="ClientCertificateMode.NoCertificate"/>.
/// </summary>
Expand Down