Skip to content

Commit f1bbdd4

Browse files
amcaseyJamesNK
andauthored
Make TLS & QUIC Pay-for-Play (Redux) (#47454)
* 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 <[email protected]> Add assert message Clarify comment on assert Fix typo in doc comment Co-authored-by: Aditya Mandaleeka <[email protected]> Fix typo in doc comment Co-authored-by: Aditya Mandaleeka <[email protected]> Fix typo in doc comment Co-authored-by: Aditya Mandaleeka <[email protected]> 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 * Improve assert message Co-authored-by: James Newton-King <[email protected]> * Adopt MemberNotNullAttribute * Assert that members are non-null to suppress CS8774 * UseHttpsConfiguration -> UseKestrelHttpsConfiguration * UseKestrelSlim -> UseKestrelCore * Drop convenience overloads of UseKestrelCore * Use more explicit error strings in HttpsConfigurationService.EnsureInitialized --------- Co-authored-by: James Newton-King <[email protected]>
1 parent b120134 commit f1bbdd4

29 files changed

+1160
-331
lines changed

src/DefaultBuilder/src/WebHost.cs

+19-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
1010
using Microsoft.AspNetCore.Http;
1111
using Microsoft.AspNetCore.Routing;
12+
using Microsoft.AspNetCore.Server.Kestrel.Core;
1213
using Microsoft.Extensions.Configuration;
1314
using Microsoft.Extensions.DependencyInjection;
1415
using Microsoft.Extensions.Hosting;
@@ -223,23 +224,31 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder)
223224
}
224225
});
225226

226-
ConfigureWebDefaultsCore(builder, services =>
227-
{
228-
services.AddRouting();
229-
});
227+
ConfigureWebDefaultsWorker(
228+
builder.UseKestrel(ConfigureKestrel),
229+
services =>
230+
{
231+
services.AddRouting();
232+
});
230233

231234
builder
232235
.UseIIS()
233236
.UseIISIntegration();
234237
}
235238

236-
internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder, Action<IServiceCollection>? configureRouting = null)
239+
internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder)
237240
{
238-
builder.UseKestrel((builderContext, options) =>
239-
{
240-
options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true);
241-
})
242-
.ConfigureServices((hostingContext, services) =>
241+
ConfigureWebDefaultsWorker(builder.UseKestrelCore().ConfigureKestrel(ConfigureKestrel), configureRouting: null);
242+
}
243+
244+
private static void ConfigureKestrel(WebHostBuilderContext builderContext, KestrelServerOptions options)
245+
{
246+
options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true);
247+
}
248+
249+
private static void ConfigureWebDefaultsWorker(IWebHostBuilder builder, Action<IServiceCollection>? configureRouting)
250+
{
251+
builder.ConfigureServices((hostingContext, services) =>
243252
{
244253
// Fallback
245254
services.PostConfigure<HostFilteringOptions>(options =>

src/ProjectTemplates/Shared/Project.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ namespace Templates.Test.Helpers;
2424
[DebuggerDisplay("{ToString(),nq}")]
2525
public class Project : IDisposable
2626
{
27+
private const string _urlsNoHttps = "http://127.0.0.1:0";
2728
private const string _urls = "http://127.0.0.1:0;https://127.0.0.1:0";
2829

2930
public static string ArtifactsLogDir
@@ -181,11 +182,11 @@ internal async Task RunDotNetBuildAsync(IDictionary<string, string> packageOptio
181182
Assert.True(0 == result.ExitCode, ErrorMessages.GetFailedProcessMessage("build", this, result));
182183
}
183184

184-
internal AspNetProcess StartBuiltProjectAsync(bool hasListeningUri = true, ILogger logger = null)
185+
internal AspNetProcess StartBuiltProjectAsync(bool hasListeningUri = true, ILogger logger = null, bool noHttps = false)
185186
{
186187
var environment = new Dictionary<string, string>
187188
{
188-
["ASPNETCORE_URLS"] = _urls,
189+
["ASPNETCORE_URLS"] = noHttps ? _urlsNoHttps : _urls,
189190
["ASPNETCORE_ENVIRONMENT"] = "Development",
190191
["ASPNETCORE_Logging__Console__LogLevel__Default"] = "Debug",
191192
["ASPNETCORE_Logging__Console__LogLevel__System"] = "Debug",
@@ -197,11 +198,11 @@ internal AspNetProcess StartBuiltProjectAsync(bool hasListeningUri = true, ILogg
197198
return new AspNetProcess(DevCert, Output, TemplateOutputDir, projectDll, environment, published: false, hasListeningUri: hasListeningUri, logger: logger);
198199
}
199200

200-
internal AspNetProcess StartPublishedProjectAsync(bool hasListeningUri = true, bool usePublishedAppHost = false)
201+
internal AspNetProcess StartPublishedProjectAsync(bool hasListeningUri = true, bool usePublishedAppHost = false, bool noHttps = false)
201202
{
202203
var environment = new Dictionary<string, string>
203204
{
204-
["ASPNETCORE_URLS"] = _urls,
205+
["ASPNETCORE_URLS"] = noHttps ? _urlsNoHttps : _urls,
205206
["ASPNETCORE_Logging__Console__LogLevel__Default"] = "Debug",
206207
["ASPNETCORE_Logging__Console__LogLevel__System"] = "Debug",
207208
["ASPNETCORE_Logging__Console__LogLevel__Microsoft"] = "Debug",

src/ProjectTemplates/test/Templates.Tests/ApiTemplateTest.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ private async Task ApiTemplateCore(string languageOverride, string[] args = null
8282

8383
await project.RunDotNetBuildAsync();
8484

85-
using (var aspNetProcess = project.StartBuiltProjectAsync())
85+
// The minimal/slim/core scenario doesn't include TLS support, so tell `project` not to register an https address
86+
using (var aspNetProcess = project.StartBuiltProjectAsync(noHttps: true))
8687
{
8788
Assert.False(
8889
aspNetProcess.Process.HasExited,
@@ -91,7 +92,7 @@ private async Task ApiTemplateCore(string languageOverride, string[] args = null
9192
await AssertEndpoints(aspNetProcess);
9293
}
9394

94-
using (var aspNetProcess = project.StartPublishedProjectAsync())
95+
using (var aspNetProcess = project.StartPublishedProjectAsync(noHttps: true))
9596
{
9697
Assert.False(
9798
aspNetProcess.Process.HasExited,

src/Servers/Kestrel/Core/src/CoreStrings.resx

+13-1
Original file line numberDiff line numberDiff line change
@@ -722,4 +722,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
722722
<data name="FailedToBindToIPv6Any" xml:space="preserve">
723723
<value>Failed to bind to http://[::]:{port} (IPv6Any).</value>
724724
</data>
725-
</root>
725+
<data name="NeedHttpsConfigurationToApplyHttpsConfiguration" xml:space="preserve">
726+
<value>Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable loading HTTPS settings from configuration.</value>
727+
</data>
728+
<data name="NeedHttpsConfigurationToLoadDefaultCertificate" xml:space="preserve">
729+
<value>Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable loading the default server certificate from configuration.</value>
730+
</data>
731+
<data name="NeedHttpsConfigurationToUseHttp3" xml:space="preserve">
732+
<value>Call UseKestrelHttpsConfiguration() on IWebHostBuilder to enable transport layer security for HTTP/3.</value>
733+
</data>
734+
<data name="NeedHttpsConfigurationToBindHttpsAddresses" xml:space="preserve">
735+
<value>Call UseKestrelHttpsConfiguration() on IWebHostBuilder to automatically enable HTTPS when an https:// address is used.</value>
736+
</data>
737+
</root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.IO.Pipelines;
7+
using System.Net;
8+
using System.Net.Security;
9+
using Microsoft.AspNetCore.Connections;
10+
using Microsoft.AspNetCore.Hosting;
11+
using Microsoft.AspNetCore.Http.Features;
12+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
13+
using Microsoft.AspNetCore.Server.Kestrel.Https;
14+
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
15+
using Microsoft.Extensions.Hosting;
16+
using Microsoft.Extensions.Logging;
17+
18+
namespace Microsoft.AspNetCore.Server.Kestrel.Core;
19+
20+
/// <inheritdoc />
21+
internal sealed class HttpsConfigurationService : IHttpsConfigurationService
22+
{
23+
private readonly IInitializer? _initializer;
24+
private bool _isInitialized;
25+
26+
private TlsConfigurationLoader? _tlsConfigurationLoader;
27+
private Action<FeatureCollection, ListenOptions>? _populateMultiplexedTransportFeatures;
28+
private Func<ListenOptions, ListenOptions>? _useHttpsWithDefaults;
29+
30+
/// <summary>
31+
/// Create an uninitialized <see cref="HttpsConfigurationService"/>.
32+
/// To initialize it later, call <see cref="Initialize"/>.
33+
/// </summary>
34+
public HttpsConfigurationService()
35+
{
36+
}
37+
38+
/// <summary>
39+
/// Create an initialized <see cref="HttpsConfigurationService"/>.
40+
/// </summary>
41+
/// <remarks>
42+
/// In practice, <see cref="Initialize"/> won't be called until it's needed.
43+
/// </remarks>
44+
public HttpsConfigurationService(IInitializer initializer)
45+
{
46+
_initializer = initializer;
47+
}
48+
49+
/// <inheritdoc />
50+
// If there's an initializer, it *can* be initialized, even though it might not be yet.
51+
// Use explicit interface implentation so we don't accidentally call it within this class.
52+
bool IHttpsConfigurationService.IsInitialized => _isInitialized || _initializer is not null;
53+
54+
/// <inheritdoc/>
55+
public void Initialize(
56+
IHostEnvironment hostEnvironment,
57+
ILogger<KestrelServer> serverLogger,
58+
ILogger<HttpsConnectionMiddleware> httpsLogger)
59+
{
60+
if (_isInitialized)
61+
{
62+
return;
63+
}
64+
65+
_isInitialized = true;
66+
67+
_tlsConfigurationLoader = new TlsConfigurationLoader(hostEnvironment, serverLogger, httpsLogger);
68+
_populateMultiplexedTransportFeatures = PopulateMultiplexedTransportFeaturesWorker;
69+
_useHttpsWithDefaults = UseHttpsWithDefaultsWorker;
70+
}
71+
72+
/// <inheritdoc/>
73+
public void ApplyHttpsConfiguration(
74+
HttpsConnectionAdapterOptions httpsOptions,
75+
EndpointConfig endpoint,
76+
KestrelServerOptions serverOptions,
77+
CertificateConfig? defaultCertificateConfig,
78+
ConfigurationReader configurationReader)
79+
{
80+
EnsureInitialized(CoreStrings.NeedHttpsConfigurationToApplyHttpsConfiguration);
81+
_tlsConfigurationLoader.ApplyHttpsConfiguration(httpsOptions, endpoint, serverOptions, defaultCertificateConfig, configurationReader);
82+
}
83+
84+
/// <inheritdoc/>
85+
public ListenOptions UseHttpsWithSni(ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint)
86+
{
87+
// This doesn't get a distinct string since it won't actually throw - it's always called after ApplyHttpsConfiguration
88+
EnsureInitialized(CoreStrings.NeedHttpsConfigurationToApplyHttpsConfiguration);
89+
return _tlsConfigurationLoader.UseHttpsWithSni(listenOptions, httpsOptions, endpoint);
90+
}
91+
92+
/// <inheritdoc/>
93+
public CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader)
94+
{
95+
EnsureInitialized(CoreStrings.NeedHttpsConfigurationToLoadDefaultCertificate);
96+
return _tlsConfigurationLoader.LoadDefaultCertificate(configurationReader);
97+
}
98+
99+
/// <inheritdoc/>
100+
public void PopulateMultiplexedTransportFeatures(FeatureCollection features, ListenOptions listenOptions)
101+
{
102+
EnsureInitialized(CoreStrings.NeedHttpsConfigurationToUseHttp3);
103+
_populateMultiplexedTransportFeatures.Invoke(features, listenOptions);
104+
}
105+
106+
/// <inheritdoc/>
107+
public ListenOptions UseHttpsWithDefaults(ListenOptions listenOptions)
108+
{
109+
EnsureInitialized(CoreStrings.NeedHttpsConfigurationToBindHttpsAddresses);
110+
return _useHttpsWithDefaults.Invoke(listenOptions);
111+
}
112+
113+
/// <summary>
114+
/// If this instance has not been initialized, initialize it if possible and throw otherwise.
115+
/// </summary>
116+
/// <exception cref="InvalidOperationException">If initialization is not possible.</exception>
117+
[MemberNotNull(nameof(_useHttpsWithDefaults), nameof(_tlsConfigurationLoader), nameof(_populateMultiplexedTransportFeatures))]
118+
private void EnsureInitialized(string uninitializedError)
119+
{
120+
if (!_isInitialized)
121+
{
122+
if (_initializer is not null)
123+
{
124+
_initializer.Initialize(this);
125+
}
126+
else
127+
{
128+
throw new InvalidOperationException(uninitializedError);
129+
}
130+
}
131+
132+
Debug.Assert(_useHttpsWithDefaults is not null);
133+
Debug.Assert(_tlsConfigurationLoader is not null);
134+
Debug.Assert(_populateMultiplexedTransportFeatures is not null);
135+
}
136+
137+
/// <summary>
138+
/// The initialized implementation of <see cref="PopulateMultiplexedTransportFeatures"/>.
139+
/// </summary>
140+
internal static void PopulateMultiplexedTransportFeaturesWorker(FeatureCollection features, ListenOptions listenOptions)
141+
{
142+
// HttpsOptions or HttpsCallbackOptions should always be set in production, but it's not set for InMemory tests.
143+
// The QUIC transport will check if TlsConnectionCallbackOptions is missing.
144+
if (listenOptions.HttpsOptions != null)
145+
{
146+
var sslServerAuthenticationOptions = HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions);
147+
features.Set(new TlsConnectionCallbackOptions
148+
{
149+
ApplicationProtocols = sslServerAuthenticationOptions.ApplicationProtocols ?? new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
150+
OnConnection = (context, cancellationToken) => ValueTask.FromResult(sslServerAuthenticationOptions),
151+
OnConnectionState = null,
152+
});
153+
}
154+
else if (listenOptions.HttpsCallbackOptions != null)
155+
{
156+
features.Set(new TlsConnectionCallbackOptions
157+
{
158+
ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
159+
OnConnection = (context, cancellationToken) =>
160+
{
161+
return listenOptions.HttpsCallbackOptions.OnConnection(new TlsHandshakeCallbackContext
162+
{
163+
ClientHelloInfo = context.ClientHelloInfo,
164+
CancellationToken = cancellationToken,
165+
State = context.State,
166+
Connection = new ConnectionContextAdapter(context.Connection),
167+
});
168+
},
169+
OnConnectionState = listenOptions.HttpsCallbackOptions.OnConnectionState,
170+
});
171+
}
172+
}
173+
174+
/// <summary>
175+
/// The initialized implementation of <see cref="UseHttpsWithDefaults"/>.
176+
/// </summary>
177+
internal static ListenOptions UseHttpsWithDefaultsWorker(ListenOptions listenOptions)
178+
{
179+
return listenOptions.UseHttps();
180+
}
181+
182+
/// <summary>
183+
/// TlsHandshakeCallbackContext.Connection is ConnectionContext but QUIC connection only implements BaseConnectionContext.
184+
/// </summary>
185+
private sealed class ConnectionContextAdapter : ConnectionContext
186+
{
187+
private readonly BaseConnectionContext _inner;
188+
189+
public ConnectionContextAdapter(BaseConnectionContext inner) => _inner = inner;
190+
191+
public override IDuplexPipe Transport
192+
{
193+
get => throw new NotSupportedException("Not supported by HTTP/3 connections.");
194+
set => throw new NotSupportedException("Not supported by HTTP/3 connections.");
195+
}
196+
public override string ConnectionId
197+
{
198+
get => _inner.ConnectionId;
199+
set => _inner.ConnectionId = value;
200+
}
201+
public override IFeatureCollection Features => _inner.Features;
202+
public override IDictionary<object, object?> Items
203+
{
204+
get => _inner.Items;
205+
set => _inner.Items = value;
206+
}
207+
public override EndPoint? LocalEndPoint
208+
{
209+
get => _inner.LocalEndPoint;
210+
set => _inner.LocalEndPoint = value;
211+
}
212+
public override EndPoint? RemoteEndPoint
213+
{
214+
get => _inner.RemoteEndPoint;
215+
set => _inner.RemoteEndPoint = value;
216+
}
217+
public override CancellationToken ConnectionClosed
218+
{
219+
get => _inner.ConnectionClosed;
220+
set => _inner.ConnectionClosed = value;
221+
}
222+
public override ValueTask DisposeAsync() => _inner.DisposeAsync();
223+
}
224+
225+
/// <summary>
226+
/// Register an instance of this type to initialize registered instances of <see cref="HttpsConfigurationService"/>.
227+
/// </summary>
228+
internal interface IInitializer
229+
{
230+
/// <summary>
231+
/// Invokes <see cref="IHttpsConfigurationService.Initialize"/>, passing appropriate arguments.
232+
/// </summary>
233+
void Initialize(IHttpsConfigurationService httpsConfigurationService);
234+
}
235+
236+
/// <inheritdoc/>
237+
internal sealed class Initializer : IInitializer
238+
{
239+
private readonly IHostEnvironment _hostEnvironment;
240+
private readonly ILogger<KestrelServer> _serverLogger;
241+
private readonly ILogger<HttpsConnectionMiddleware> _httpsLogger;
242+
243+
public Initializer(
244+
IHostEnvironment hostEnvironment,
245+
ILogger<KestrelServer> serverLogger,
246+
ILogger<HttpsConnectionMiddleware> httpsLogger)
247+
{
248+
_hostEnvironment = hostEnvironment;
249+
_serverLogger = serverLogger;
250+
_httpsLogger = httpsLogger;
251+
}
252+
253+
/// <inheritdoc/>
254+
public void Initialize(IHttpsConfigurationService httpsConfigurationService)
255+
{
256+
httpsConfigurationService.Initialize(_hostEnvironment, _serverLogger, _httpsLogger);
257+
}
258+
}
259+
}
260+

src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs

+5
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ public HttpsConnectionAdapterOptions()
5555
/// </summary>
5656
public Func<ConnectionContext?, string?, X509Certificate2?>? ServerCertificateSelector { get; set; }
5757

58+
/// <summary>
59+
/// Convenient shorthand for a common check.
60+
/// </summary>
61+
internal bool HasServerCertificateOrSelector => ServerCertificate is not null || ServerCertificateSelector is not null;
62+
5863
/// <summary>
5964
/// Specifies the client certificate requirements for a HTTPS connection. Defaults to <see cref="ClientCertificateMode.NoCertificate"/>.
6065
/// </summary>

0 commit comments

Comments
 (0)