Skip to content

Commit 04f23ec

Browse files
authored
Add async ServerOptionsSelectionCallback UseHttps overload (#25390)
1 parent 071a539 commit 04f23ec

File tree

4 files changed

+209
-60
lines changed

4 files changed

+209
-60
lines changed

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

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Net.Security;
77
using System.Security.Cryptography.X509Certificates;
88
using Microsoft.AspNetCore.Server.Kestrel.Core;
9-
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
109
using Microsoft.AspNetCore.Server.Kestrel.Https;
1110
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
1211
using Microsoft.Extensions.DependencyInjection;
@@ -209,7 +208,8 @@ internal static bool TryUseHttps(this ListenOptions listenOptions)
209208
}
210209

211210
/// <summary>
212-
/// Configure Kestrel to use HTTPS.
211+
/// Configure Kestrel to use HTTPS. This does not use default certificates or other defaults specified via config or
212+
/// <see cref="KestrelServerOptions.ConfigureHttpsDefaults(Action{HttpsConnectionAdapterOptions})"/>.
213213
/// </summary>
214214
/// <param name="listenOptions">The <see cref="ListenOptions"/> to configure.</param>
215215
/// <param name="httpsOptions">Options to configure HTTPS.</param>
@@ -230,12 +230,44 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConn
230230
return listenOptions;
231231
}
232232

233+
/// <summary>
234+
/// Configure Kestrel to use HTTPS. This does not use default certificates or other defaults specified via config or
235+
/// <see cref="KestrelServerOptions.ConfigureHttpsDefaults(Action{HttpsConnectionAdapterOptions})"/>.
236+
/// </summary>
237+
/// <param name="listenOptions">The <see cref="ListenOptions"/> to configure.</param>
238+
/// <param name="serverOptionsSelectionCallback">Callback to configure HTTPS options.</param>
239+
/// <param name="state">State for the <paramref name="serverOptionsSelectionCallback"/>.</param>
240+
/// <returns>The <see cref="ListenOptions"/>.</returns>
241+
public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, object state)
242+
{
243+
return listenOptions.UseHttps(serverOptionsSelectionCallback, state, HttpsConnectionAdapterOptions.DefaultHandshakeTimeout);
244+
}
245+
246+
/// <summary>
247+
/// Configure Kestrel to use HTTPS. This does not use default certificates or other defaults specified via config or
248+
/// <see cref="KestrelServerOptions.ConfigureHttpsDefaults(Action{HttpsConnectionAdapterOptions})"/>.
249+
/// </summary>
250+
/// <param name="listenOptions">The <see cref="ListenOptions"/> to configure.</param>
251+
/// <param name="serverOptionsSelectionCallback">Callback to configure HTTPS options.</param>
252+
/// <param name="state">State for the <paramref name="serverOptionsSelectionCallback"/>.</param>
253+
/// <param name="handshakeTimeout">Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite.</param>
254+
/// <returns>The <see cref="ListenOptions"/>.</returns>
255+
public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, object state, TimeSpan handshakeTimeout)
256+
{
257+
// HttpsOptionsCallback is an internal delegate that is just the ServerOptionsSelectionCallback + a ConnectionContext parameter.
258+
// Given that ConnectionContext will eventually be replaced by System.Net.Connections, it doesn't make much sense to make the HttpsOptionsCallback delegate public.
259+
HttpsOptionsCallback adaptedCallback = (connection, stream, clientHelloInfo, state, cancellationToken) =>
260+
serverOptionsSelectionCallback(stream, clientHelloInfo, state, cancellationToken);
261+
262+
return listenOptions.UseHttps(adaptedCallback, state, handshakeTimeout);
263+
}
264+
233265
/// <summary>
234266
/// Configure Kestrel to use HTTPS.
235267
/// </summary>
236268
/// <param name="listenOptions">The <see cref="ListenOptions"/> to configure.</param>
237269
/// <param name="httpsOptionsCallback">Callback to configure HTTPS options.</param>
238-
/// <param name="state">State for the <see cref="ServerOptionsSelectionCallback" />.</param>
270+
/// <param name="state">State for the <paramref name="httpsOptionsCallback"/>.</param>
239271
/// <param name="handshakeTimeout">Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite.</param>
240272
/// <returns>The <see cref="ListenOptions"/>.</returns>
241273
internal static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsOptionsCallback httpsOptionsCallback, object state, TimeSpan handshakeTimeout)

src/Servers/Kestrel/samples/SampleApp/Startup.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics;
66
using System.IO;
77
using System.Net;
8+
using System.Net.Security;
89
using System.Security.Authentication;
910
using System.Security.Cryptography.X509Certificates;
1011
using System.Threading.Tasks;
@@ -109,15 +110,21 @@ public static Task Main(string[] args)
109110

110111
options.ListenAnyIP(basePort + 5, listenOptions =>
111112
{
112-
listenOptions.UseHttps(httpsOptions =>
113+
var localhostCert = CertificateLoader.LoadFromStoreCert("localhost", "My", StoreLocation.CurrentUser, allowInvalid: true);
114+
115+
listenOptions.UseHttps((stream, clientHelloInfo, state, cancellationToken) =>
113116
{
114-
var localhostCert = CertificateLoader.LoadFromStoreCert("localhost", "My", StoreLocation.CurrentUser, allowInvalid: true);
115-
httpsOptions.ServerCertificateSelector = (features, name) =>
117+
// Here you would check the name, select an appropriate cert, and provide a fallback or fail for null names.
118+
if (clientHelloInfo.ServerName != null && clientHelloInfo.ServerName != "localhost")
116119
{
117-
// Here you would check the name, select an appropriate cert, and provide a fallback or fail for null names.
118-
return localhostCert;
119-
};
120-
});
120+
throw new AuthenticationException($"The endpoint is not configured for sever name '{clientHelloInfo.ServerName}'.");
121+
}
122+
123+
return new ValueTask<SslServerAuthenticationOptions>(new SslServerAuthenticationOptions
124+
{
125+
ServerCertificate = localhostCert
126+
});
127+
}, state: null);
121128
});
122129

123130
options

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

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,42 @@ void ConfigureListenOptions(ListenOptions listenOptions)
123123
}
124124
}
125125

126+
[Fact]
127+
public async Task HandshakeDetailsAreAvailableAfterAsyncCallback()
128+
{
129+
void ConfigureListenOptions(ListenOptions listenOptions)
130+
{
131+
listenOptions.UseHttps(async (stream, clientHelloInfo, state, cancellationToken) =>
132+
{
133+
await Task.Yield();
134+
135+
return new SslServerAuthenticationOptions
136+
{
137+
ServerCertificate = _x509Certificate2,
138+
};
139+
}, state: null);
140+
}
141+
142+
await using (var server = new TestServer(context =>
143+
{
144+
var tlsFeature = context.Features.Get<ITlsHandshakeFeature>();
145+
Assert.NotNull(tlsFeature);
146+
Assert.True(tlsFeature.Protocol > SslProtocols.None, "Protocol");
147+
Assert.True(tlsFeature.CipherAlgorithm > CipherAlgorithmType.Null, "Cipher");
148+
Assert.True(tlsFeature.CipherStrength > 0, "CipherStrength");
149+
Assert.True(tlsFeature.HashAlgorithm >= HashAlgorithmType.None, "HashAlgorithm"); // May be None on Linux.
150+
Assert.True(tlsFeature.HashStrength >= 0, "HashStrength"); // May be 0 for some algorithms
151+
Assert.True(tlsFeature.KeyExchangeAlgorithm >= ExchangeAlgorithmType.None, "KeyExchangeAlgorithm"); // Maybe None on Windows 7
152+
Assert.True(tlsFeature.KeyExchangeStrength >= 0, "KeyExchangeStrength"); // May be 0 on mac
153+
154+
return context.Response.WriteAsync("hello world");
155+
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
156+
{
157+
var result = await server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false);
158+
Assert.Equal("hello world", result);
159+
}
160+
}
161+
126162
[Fact]
127163
public async Task RequireCertificateFailsWhenNoCertificate()
128164
{
@@ -166,22 +202,18 @@ void ConfigureListenOptions(ListenOptions listenOptions)
166202
}
167203

168204
[Fact]
169-
[QuarantinedTest("https://github.com/dotnet/runtime/issues/40402")]
170-
public async Task ClientCertificateRequiredConfiguredInCallbackContinuesWhenNoCertificate()
205+
public async Task AsyncCallbackSettingClientCertificateRequiredContinuesWhenNoCertificate()
171206
{
172207
void ConfigureListenOptions(ListenOptions listenOptions)
173208
{
174-
listenOptions.UseHttps((connection, stream, clientHelloInfo, state, cancellationToken) =>
209+
listenOptions.UseHttps((stream, clientHelloInfo, state, cancellationToken) =>
175210
new ValueTask<SslServerAuthenticationOptions>(new SslServerAuthenticationOptions
176211
{
177212
ServerCertificate = _x509Certificate2,
178-
// From the API Docs: "Note that this is only a request --
179-
// if no certificate is provided, the server still accepts the connection request."
180-
// Not to mention this is equivalent to the test above.
181213
ClientCertificateRequired = true,
182214
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true,
183215
CertificateRevocationCheckMode = X509RevocationMode.NoCheck
184-
}), state: null, HttpsConnectionAdapterOptions.DefaultHandshakeTimeout);
216+
}), state: null);
185217
}
186218

187219
await using (var server = new TestServer(context =>
@@ -255,6 +287,39 @@ void ConfigureListenOptions(ListenOptions listenOptions)
255287
}
256288
}
257289

290+
[Fact]
291+
public async Task UsesProvidedAsyncCallback()
292+
{
293+
var selectorCalled = 0;
294+
void ConfigureListenOptions(ListenOptions listenOptions)
295+
{
296+
listenOptions.UseHttps(async (stream, clientHelloInfo, state, cancellationToken) =>
297+
{
298+
await Task.Yield();
299+
300+
Assert.NotNull(stream);
301+
Assert.Equal("localhost", clientHelloInfo.ServerName);
302+
selectorCalled++;
303+
304+
return new SslServerAuthenticationOptions
305+
{
306+
ServerCertificate = _x509Certificate2
307+
};
308+
}, state: null);
309+
}
310+
311+
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
312+
{
313+
using (var connection = server.CreateConnection())
314+
{
315+
var stream = OpenSslStream(connection.Stream);
316+
await stream.AuthenticateAsClientAsync("localhost");
317+
Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2));
318+
Assert.Equal(1, selectorCalled);
319+
}
320+
}
321+
}
322+
258323
[Fact]
259324
public async Task UsesProvidedServerCertificateSelectorEachTime()
260325
{

0 commit comments

Comments
 (0)