Skip to content

Commit 19f1321

Browse files
authored
Configure ALPN for callback scenarios #31303 (#34242)
1 parent 8e9af3c commit 19f1321

File tree

4 files changed

+104
-0
lines changed

4 files changed

+104
-0
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,8 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, TlsHandsh
288288
listenOptions.IsTls = true;
289289
listenOptions.Use(next =>
290290
{
291+
// Set the list of protocols from listen options
292+
callbackOptions.HttpProtocols = listenOptions.Protocols;
291293
var middleware = new HttpsConnectionMiddleware(next, callbackOptions, loggerFactory);
292294
return middleware.OnConnectionAsync;
293295
});

src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ internal class HttpsConnectionMiddleware
4646
// The following fields are only set by TlsHandshakeCallbackOptions ctor.
4747
private readonly Func<TlsHandshakeCallbackContext, ValueTask<SslServerAuthenticationOptions>>? _tlsCallbackOptions;
4848
private readonly object? _tlsCallbackOptionsState;
49+
private readonly HttpProtocols _httpProtocols;
4950

5051
// Pool for cancellation tokens that cancel the handshake
5152
private readonly CancellationTokenSourcePool _ctsPool = new();
@@ -127,6 +128,7 @@ internal HttpsConnectionMiddleware(
127128

128129
_tlsCallbackOptions = tlsCallbackOptions.OnConnection;
129130
_tlsCallbackOptionsState = tlsCallbackOptions.OnConnectionState;
131+
_httpProtocols = ValidateAndNormalizeHttpProtocols(tlsCallbackOptions.HttpProtocols, _logger);
130132
_sslStreamFactory = s => new SslStream(s);
131133
}
132134

@@ -434,6 +436,11 @@ private static async ValueTask<SslServerAuthenticationOptions> ServerOptionsCall
434436
var sslOptions = await middleware._tlsCallbackOptions!(callbackContext);
435437
feature.AllowDelayedClientCertificateNegotation = callbackContext.AllowDelayedClientCertificateNegotation;
436438

439+
// The callback didn't set ALPN so we will.
440+
if (sslOptions.ApplicationProtocols == null)
441+
{
442+
ConfigureAlpn(sslOptions, middleware._httpProtocols);
443+
}
437444
KestrelEventSource.Log.TlsHandshakeStart(context, sslOptions);
438445

439446
return sslOptions;

src/Servers/Kestrel/Core/src/TlsHandshakeCallbackOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,8 @@ public TimeSpan HandshakeTimeout
4242
_handshakeTimeout = value != Timeout.InfiniteTimeSpan ? value : TimeSpan.MaxValue;
4343
}
4444
}
45+
46+
// Copied from the ListenOptions to enable ALPN
47+
internal HttpProtocols HttpProtocols { get; set; }
4548
}
4649
}

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,98 @@ void ConfigureListenOptions(ListenOptions listenOptions)
793793
await AssertConnectionResult(stream, true, expectedBody);
794794
}
795795

796+
[ConditionalFact]
797+
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "ALPN not supported")]
798+
public async Task ServerOptionsSelectionCallback_SetsALPN()
799+
{
800+
static void ConfigureListenOptions(ListenOptions listenOptions)
801+
{
802+
listenOptions.UseHttps((_, _, _, _) =>
803+
ValueTask.FromResult(new SslServerAuthenticationOptions()
804+
{
805+
ServerCertificate = _x509Certificate2,
806+
}), state: null);
807+
}
808+
809+
await using var server = new TestServer(context => Task.CompletedTask,
810+
new TestServiceContext(LoggerFactory), ConfigureListenOptions);
811+
812+
using var connection = server.CreateConnection();
813+
var stream = OpenSslStream(connection.Stream);
814+
await stream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions()
815+
{
816+
// Use a random host name to avoid the TLS session resumption cache.
817+
TargetHost = Guid.NewGuid().ToString(),
818+
ApplicationProtocols = new() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11, },
819+
});
820+
Assert.Equal(SslApplicationProtocol.Http2, stream.NegotiatedApplicationProtocol);
821+
}
822+
823+
[ConditionalFact]
824+
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "ALPN not supported")]
825+
public async Task TlsHandshakeCallbackOptionsOverload_SetsALPN()
826+
{
827+
static void ConfigureListenOptions(ListenOptions listenOptions)
828+
{
829+
listenOptions.UseHttps(new TlsHandshakeCallbackOptions()
830+
{
831+
OnConnection = context =>
832+
{
833+
return ValueTask.FromResult(new SslServerAuthenticationOptions()
834+
{
835+
ServerCertificate = _x509Certificate2,
836+
});
837+
}
838+
});
839+
}
840+
841+
await using var server = new TestServer(context => Task.CompletedTask,
842+
new TestServiceContext(LoggerFactory), ConfigureListenOptions);
843+
844+
using var connection = server.CreateConnection();
845+
var stream = OpenSslStream(connection.Stream);
846+
await stream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions()
847+
{
848+
// Use a random host name to avoid the TLS session resumption cache.
849+
TargetHost = Guid.NewGuid().ToString(),
850+
ApplicationProtocols = new() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11, },
851+
});
852+
Assert.Equal(SslApplicationProtocol.Http2, stream.NegotiatedApplicationProtocol);
853+
}
854+
855+
[ConditionalFact]
856+
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "ALPN not supported")]
857+
public async Task TlsHandshakeCallbackOptionsOverload_EmptyAlpnList_DisablesAlpn()
858+
{
859+
static void ConfigureListenOptions(ListenOptions listenOptions)
860+
{
861+
listenOptions.UseHttps(new TlsHandshakeCallbackOptions()
862+
{
863+
OnConnection = context =>
864+
{
865+
return ValueTask.FromResult(new SslServerAuthenticationOptions()
866+
{
867+
ServerCertificate = _x509Certificate2,
868+
ApplicationProtocols = new(),
869+
});
870+
}
871+
});
872+
}
873+
874+
await using var server = new TestServer(context => Task.CompletedTask,
875+
new TestServiceContext(LoggerFactory), ConfigureListenOptions);
876+
877+
using var connection = server.CreateConnection();
878+
var stream = OpenSslStream(connection.Stream);
879+
await stream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions()
880+
{
881+
// Use a random host name to avoid the TLS session resumption cache.
882+
TargetHost = Guid.NewGuid().ToString(),
883+
ApplicationProtocols = new() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11, },
884+
});
885+
Assert.Equal(default, stream.NegotiatedApplicationProtocol);
886+
}
887+
796888
[ConditionalFact]
797889
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Not supported yet.")]
798890
public async Task CanRenegotiateForClientCertificateOnPostIfDrained()

0 commit comments

Comments
 (0)