Skip to content

Commit f8d3cd0

Browse files
committed
Defer search for dev cert until build bind time
...so that it can occur after IConfiguration is read. (In particular, so that it occurs during AddressBinder.BindAsync which happens strictly after KestrelConfigurationLoader.Load.) This is important in Docker scenarios since the Docker tools use IConfiguration to tell us where the dev cert directory is mounted.# with '#' will be ignored, and an empty message aborts the commit. Was dotnet#46296 Fixes dotnet#45801
1 parent 90ddbbc commit f8d3cd0

File tree

6 files changed

+243
-15
lines changed

6 files changed

+243
-15
lines changed

src/Servers/Kestrel/Core/src/HttpsConfigurationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ internal static void PopulateMultiplexedTransportFeaturesWorker(FeatureCollectio
147147
// The QUIC transport will check if TlsConnectionCallbackOptions is missing.
148148
if (listenOptions.HttpsOptions != null)
149149
{
150-
var sslServerAuthenticationOptions = HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions, logger);
150+
var sslServerAuthenticationOptions = HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions.Value, logger);
151151
features.Set(new TlsConnectionCallbackOptions
152152
{
153153
ApplicationProtocols = sslServerAuthenticationOptions.ApplicationProtocols ?? new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },

src/Servers/Kestrel/Core/src/ListenOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ internal string Scheme
140140
}
141141

142142
internal bool IsTls { get; set; }
143-
internal HttpsConnectionAdapterOptions? HttpsOptions { get; set; }
143+
internal Lazy<HttpsConnectionAdapterOptions>? HttpsOptions { get; set; }
144144
internal TlsHandshakeCallbackOptions? HttpsCallbackOptions { get; set; }
145145

146146
/// <summary>

src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -166,17 +166,20 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, Action<Ht
166166
// We consider calls to `UseHttps` to be a clear expression of user intent to pull in HTTPS configuration support
167167
listenOptions.KestrelServerOptions.EnableHttpsConfiguration();
168168

169-
var options = new HttpsConnectionAdapterOptions();
170-
listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options);
171-
configureOptions(options);
172-
listenOptions.KestrelServerOptions.ApplyDefaultCertificate(options);
173-
174-
if (!options.HasServerCertificateOrSelector)
169+
return listenOptions.UseHttps(new Lazy<HttpsConnectionAdapterOptions>(() =>
175170
{
176-
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
177-
}
171+
var options = new HttpsConnectionAdapterOptions();
172+
listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options);
173+
configureOptions(options);
174+
listenOptions.KestrelServerOptions.ApplyDefaultCertificate(options);
175+
176+
if (!options.HasServerCertificateOrSelector)
177+
{
178+
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
179+
}
178180

179-
return listenOptions.UseHttps(options);
181+
return options;
182+
}));
180183
}
181184

182185
/// <summary>
@@ -188,14 +191,28 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, Action<Ht
188191
/// <returns>The <see cref="ListenOptions"/>.</returns>
189192
public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions)
190193
{
191-
var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<ILoggerFactory>();
192-
var metrics = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<KestrelMetrics>();
194+
return listenOptions.UseHttps(new Lazy<HttpsConnectionAdapterOptions>(httpsOptions));
195+
}
193196

197+
/// <summary>
198+
/// Configure Kestrel to use HTTPS. This does not use default certificates or other defaults specified via config or
199+
/// <see cref="KestrelServerOptions.ConfigureHttpsDefaults(Action{HttpsConnectionAdapterOptions})"/>.
200+
/// </summary>
201+
/// <param name="listenOptions">The <see cref="ListenOptions"/> to configure.</param>
202+
/// <param name="lazyHttpsOptions">Options to configure HTTPS.</param>
203+
/// <returns>The <see cref="ListenOptions"/>.</returns>
204+
private static ListenOptions UseHttps(this ListenOptions listenOptions, Lazy<HttpsConnectionAdapterOptions> lazyHttpsOptions)
205+
{
194206
listenOptions.IsTls = true;
195-
listenOptions.HttpsOptions = httpsOptions;
207+
listenOptions.HttpsOptions = lazyHttpsOptions;
196208

209+
// NB: This lambda will only be invoked if either HTTP/1.* or HTTP/2 is being used
197210
listenOptions.Use(next =>
198211
{
212+
// Evaluate the HttpsConnectionAdapterOptions, now that the configuration, if any, has been loaded
213+
var httpsOptions = lazyHttpsOptions.Value;
214+
var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<ILoggerFactory>();
215+
var metrics = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<KestrelMetrics>();
199216
var middleware = new HttpsConnectionMiddleware(next, httpsOptions, listenOptions.Protocols, loggerFactory, metrics);
200217
return middleware.OnConnectionAsync;
201218
});
@@ -257,6 +274,7 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, TlsHandsh
257274
listenOptions.IsTls = true;
258275
listenOptions.HttpsCallbackOptions = callbackOptions;
259276

277+
// NB: This lambda will only be invoked if either HTTP/1.* or HTTP/2 is being used
260278
listenOptions.Use(next =>
261279
{
262280
// Set the list of protocols from listen options.

src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,105 @@ public void ConfigureEndpoint_RecoverFromBadPassword()
380380
void CheckListenOptions(X509Certificate2 expectedCert)
381381
{
382382
var listenOptions = Assert.Single(serverOptions.ConfigurationBackedListenOptions);
383-
Assert.Equal(expectedCert.SerialNumber, listenOptions.HttpsOptions.ServerCertificate.SerialNumber);
383+
Assert.Equal(expectedCert.SerialNumber, listenOptions.HttpsOptions!.Value.ServerCertificate.SerialNumber);
384+
}
385+
}
386+
387+
[Fact]
388+
public void LoadDevelopmentCertificate_ConfigureFirst()
389+
{
390+
try
391+
{
392+
var serverOptions = CreateServerOptions();
393+
var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
394+
var bytes = certificate.Export(X509ContentType.Pkcs12, "1234");
395+
var path = GetCertificatePath();
396+
Directory.CreateDirectory(Path.GetDirectoryName(path));
397+
File.WriteAllBytes(path, bytes);
398+
399+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
400+
{
401+
new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
402+
}).Build();
403+
404+
serverOptions.Configure(config);
405+
406+
Assert.Null(serverOptions.ConfigurationLoader.DefaultCertificate);
407+
408+
serverOptions.ConfigurationLoader.Load();
409+
410+
Assert.NotNull(serverOptions.ConfigurationLoader.DefaultCertificate);
411+
Assert.Equal(serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber, certificate.SerialNumber);
412+
413+
var ran1 = false;
414+
serverOptions.ListenAnyIP(4545, listenOptions =>
415+
{
416+
ran1 = true;
417+
listenOptions.UseHttps();
418+
});
419+
Assert.True(ran1);
420+
421+
var listenOptions = serverOptions.CodeBackedListenOptions.Single();
422+
Assert.False(listenOptions.HttpsOptions.IsValueCreated);
423+
listenOptions.Build();
424+
Assert.True(listenOptions.HttpsOptions.IsValueCreated);
425+
Assert.Equal(listenOptions.HttpsOptions.Value.ServerCertificate?.SerialNumber, certificate.SerialNumber);
426+
}
427+
finally
428+
{
429+
if (File.Exists(GetCertificatePath()))
430+
{
431+
File.Delete(GetCertificatePath());
432+
}
433+
}
434+
}
435+
436+
[Fact]
437+
public void LoadDevelopmentCertificate_UseHttpsFirst()
438+
{
439+
try
440+
{
441+
var serverOptions = CreateServerOptions();
442+
var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
443+
var bytes = certificate.Export(X509ContentType.Pkcs12, "1234");
444+
var path = GetCertificatePath();
445+
Directory.CreateDirectory(Path.GetDirectoryName(path));
446+
File.WriteAllBytes(path, bytes);
447+
448+
var ran1 = false;
449+
serverOptions.ListenAnyIP(4545, listenOptions =>
450+
{
451+
ran1 = true;
452+
listenOptions.UseHttps();
453+
});
454+
Assert.True(ran1);
455+
456+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
457+
{
458+
new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
459+
}).Build();
460+
461+
serverOptions.Configure(config);
462+
463+
Assert.Null(serverOptions.ConfigurationLoader.DefaultCertificate);
464+
465+
serverOptions.ConfigurationLoader.Load();
466+
467+
Assert.NotNull(serverOptions.ConfigurationLoader.DefaultCertificate);
468+
Assert.Equal(serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber, certificate.SerialNumber);
469+
470+
var listenOptions = serverOptions.CodeBackedListenOptions.Single();
471+
Assert.False(listenOptions.HttpsOptions.IsValueCreated);
472+
listenOptions.Build();
473+
Assert.True(listenOptions.HttpsOptions.IsValueCreated);
474+
Assert.Equal(listenOptions.HttpsOptions.Value.ServerCertificate?.SerialNumber, certificate.SerialNumber);
475+
}
476+
finally
477+
{
478+
if (File.Exists(GetCertificatePath()))
479+
{
480+
File.Delete(GetCertificatePath());
481+
}
384482
}
385483
}
386484

@@ -862,6 +960,8 @@ public void EndpointConfigureSection_CanSetSslProtocol()
862960
});
863961
});
864962

963+
_ = serverOptions.CodeBackedListenOptions.Single().HttpsOptions.Value; // Force evaluation
964+
865965
Assert.True(ranDefault);
866966
Assert.True(ran1);
867967
Assert.True(ran2);
@@ -997,6 +1097,8 @@ public void EndpointConfigureSection_CanSetClientCertificateMode()
9971097
});
9981098
});
9991099

1100+
_ = serverOptions.CodeBackedListenOptions.Single().HttpsOptions.Value; // Force evaluation
1101+
10001102
Assert.True(ranDefault);
10011103
Assert.True(ran1);
10021104
Assert.True(ran2);

src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,18 @@ public void UseHttpsDefaultsToDefaultCert()
6464

6565
Assert.False(serverOptions.IsDevelopmentCertificateLoaded);
6666

67+
var ranUseHttpsAction = false;
6768
serverOptions.ListenLocalhost(5001, options =>
6869
{
6970
options.UseHttps(opt =>
7071
{
7172
// The default cert is applied after UseHttps.
7273
Assert.Null(opt.ServerCertificate);
74+
ranUseHttpsAction = true;
7375
});
7476
});
77+
_ = serverOptions.CodeBackedListenOptions[1].HttpsOptions.Value; // Force evaluation
78+
Assert.True(ranUseHttpsAction);
7579
Assert.False(serverOptions.IsDevelopmentCertificateLoaded);
7680
}
7781

@@ -117,14 +121,20 @@ public void ConfigureHttpsDefaultsNeverLoadsDefaultCert()
117121
options.ServerCertificate = _x509Certificate2;
118122
options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
119123
});
124+
var ranUseHttpsAction = false;
120125
serverOptions.ListenLocalhost(5000, options =>
121126
{
122127
options.UseHttps(opt =>
123128
{
124129
Assert.Equal(_x509Certificate2, opt.ServerCertificate);
125130
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode);
131+
ranUseHttpsAction = true;
126132
});
127133
});
134+
135+
_ = serverOptions.CodeBackedListenOptions.Single().HttpsOptions.Value; // Force evaluation
136+
Assert.True(ranUseHttpsAction);
137+
128138
// Never lazy loaded
129139
Assert.False(serverOptions.IsDevelopmentCertificateLoaded);
130140
Assert.Null(serverOptions.DevelopmentCertificate);
@@ -144,15 +154,21 @@ public void ConfigureCertSelectorNeverLoadsDefaultCert()
144154
};
145155
options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
146156
});
157+
var ranUseHttpsAction = false;
147158
serverOptions.ListenLocalhost(5000, options =>
148159
{
149160
options.UseHttps(opt =>
150161
{
151162
Assert.Null(opt.ServerCertificate);
152163
Assert.NotNull(opt.ServerCertificateSelector);
153164
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode);
165+
ranUseHttpsAction = true;
154166
});
155167
});
168+
169+
_ = serverOptions.CodeBackedListenOptions.Single().HttpsOptions.Value; // Force evaluation
170+
Assert.True(ranUseHttpsAction);
171+
156172
// Never lazy loaded
157173
Assert.False(serverOptions.IsDevelopmentCertificateLoaded);
158174
Assert.Null(serverOptions.DevelopmentCertificate);

0 commit comments

Comments
 (0)