Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,4 +272,43 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, TlsHandsh

return listenOptions;
}

/// <summary>
/// Configure Kestrel to use HTTPS with both standard options and a per-connection callback.
/// This does not use default certificates or other defaults specified via config or
/// <see cref="KestrelServerOptions.ConfigureHttpsDefaults(Action{HttpsConnectionAdapterOptions})"/>.
/// </summary>
/// <param name="listenOptions">The <see cref="ListenOptions"/> to configure.</param>
/// <param name="httpsOptions">Options to configure HTTPS.</param>
/// <param name="callbackOptions">Options for a per connection callback.</param>
/// <returns>The <see cref="ListenOptions"/>.</returns>
public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, TlsHandshakeCallbackOptions callbackOptions)
{
ArgumentNullException.ThrowIfNull(httpsOptions);
ArgumentNullException.ThrowIfNull(callbackOptions);

if (callbackOptions.OnConnection is null)
{
throw new ArgumentException($"{nameof(TlsHandshakeCallbackOptions.OnConnection)} must not be null.");
}

var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<ILoggerFactory>();
var metrics = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<KestrelMetrics>();

listenOptions.IsTls = true;
listenOptions.HttpsOptions = httpsOptions;
listenOptions.HttpsCallbackOptions = callbackOptions;

listenOptions.Use(next =>
{
// Set the list of protocols from listen options.
// Set it inside Use delegate so Protocols and UseHttps can be called out of order.
callbackOptions.HttpProtocols = listenOptions.Protocols;

var middleware = new HttpsConnectionMiddleware(next, httpsOptions, callbackOptions, loggerFactory, metrics);
return middleware.OnConnectionAsync;
});

return listenOptions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,37 @@ internal HttpsConnectionMiddleware(
_sslStreamFactory = s => new SslStream(s);
}

internal HttpsConnectionMiddleware(
ConnectionDelegate next,
HttpsConnectionAdapterOptions options,
TlsHandshakeCallbackOptions tlsCallbackOptions,
ILoggerFactory loggerFactory,
KestrelMetrics metrics)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(tlsCallbackOptions);

_next = next;
_handshakeTimeout = tlsCallbackOptions.HandshakeTimeout;
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
_metrics = metrics;

_options = options;
_tlsCallbackOptions = tlsCallbackOptions.OnConnection;
_tlsCallbackOptionsState = tlsCallbackOptions.OnConnectionState;
_httpProtocols = ValidateAndNormalizeHttpProtocols(tlsCallbackOptions.HttpProtocols, _logger);

var remoteCertificateValidationCallback = _options.ClientCertificateMode == ClientCertificateMode.NoCertificate ?
(RemoteCertificateValidationCallback?)null : RemoteCertificateValidationCallback;

_sslStreamFactory = s => new SslStream(s, leaveInnerStreamOpen: false, userCertificateValidationCallback: remoteCertificateValidationCallback);

if (options.TlsClientHelloBytesCallback is not null)
{
_tlsListener = new TlsListener(options.TlsClientHelloBytesCallback);
}
}

public async Task OnConnectionAsync(ConnectionContext context)
{
if (context.Features.Get<ITlsConnectionFeature>() != null)
Expand Down
1 change: 1 addition & 0 deletions src/Servers/Kestrel/Core/src/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ static Microsoft.AspNetCore.Hosting.KestrelServerOptionsSystemdExtensions.UseSys
static Microsoft.AspNetCore.Hosting.ListenOptionsConnectionLoggingExtensions.UseConnectionLogging(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, string? loggerName) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!
static Microsoft.AspNetCore.Hosting.ListenOptionsConnectionLoggingExtensions.UseConnectionLogging(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!
static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions! httpsOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!
static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions! httpsOptions, Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions! callbackOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!
static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, Microsoft.AspNetCore.Server.Kestrel.Https.TlsHandshakeCallbackOptions! callbackOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!
static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, string! fileName, string? password, System.Action<Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions!>! configureOptions) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!
static Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(this Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions! listenOptions, string! fileName, string? password) -> Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Authentication;
Expand Down Expand Up @@ -1517,6 +1518,164 @@ private static SslStream OpenSslStreamWithCert(Stream rawStream, X509Certificate
(sender, host, certificates, certificate, issuers) => clientCertificate ?? _x509Certificate2);
}

[ConditionalFact]
[TlsAlpnSupported]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing platform support.")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/33566#issuecomment-892031659", Queues = HelixConstants.AlmaLinuxAmd64)] // Outdated OpenSSL client
public async Task UseHttpsWithBothOptionsAndCallback_AppliesBothSettings()
{
var clientHelloCalled = false;
var onConnectionCalled = false;
var remoteValidationCalled = false;

void ConfigureListenOptions(ListenOptions listenOptions)
{
var httpsOptions = new HttpsConnectionAdapterOptions()
{
ClientCertificateMode = ClientCertificateMode.DelayCertificate,
ClientCertificateValidation = (cert, chain, errors) =>
{
remoteValidationCalled = true;
return true;
},
TlsClientHelloBytesCallback = (context, bytes) =>
{
clientHelloCalled = true;
}
};

var callbackOptions = new TlsHandshakeCallbackOptions()
{
OnConnection = context =>
{
onConnectionCalled = true;
context.AllowDelayedClientCertificateNegotation = true;
return ValueTask.FromResult(new SslServerAuthenticationOptions()
{
ServerCertificate = _x509Certificate2,
ClientCertificateRequired = false,
RemoteCertificateValidationCallback = (_, _, _, _) => true,
});
}
};

listenOptions.UseHttps(httpsOptions, callbackOptions);
}

await using var server = new TestServer(async context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.Null(tlsFeature.ClientCertificate);
Assert.Null(context.Connection.ClientCertificate);

var clientCert = await context.Connection.GetClientCertificateAsync();
Assert.NotNull(clientCert);
Assert.NotNull(tlsFeature.ClientCertificate);
Assert.NotNull(context.Connection.ClientCertificate);

await context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions);

using var connection = server.CreateConnection();
var stream = OpenSslStreamWithCert(connection.Stream);
await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString());
await AssertConnectionResult(stream, true);

Assert.True(clientHelloCalled, "TlsClientHelloBytesCallback from HttpsConnectionAdapterOptions should be called");
Assert.True(onConnectionCalled, "OnConnection from TlsHandshakeCallbackOptions should be called");
Assert.True(remoteValidationCalled, "ClientCertificateValidation from HttpsConnectionAdapterOptions should be called");
}

[Fact]
public async Task UseHttpsWithBothOptionsAndCallback_WorksWithBasicScenario()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
var httpsOptions = new HttpsConnectionAdapterOptions()
{
CheckCertificateRevocation = false,
ClientCertificateMode = ClientCertificateMode.NoCertificate
};

var callbackOptions = new TlsHandshakeCallbackOptions()
{
OnConnection = context =>
{
return ValueTask.FromResult(new SslServerAuthenticationOptions()
{
ServerCertificate = _x509Certificate2,
});
}
};

listenOptions.UseHttps(httpsOptions, callbackOptions);
}

await using (var server = new TestServer(App, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
var result = await server.HttpClientSlim.PostAsync($"https://localhost:{server.Port}/",
new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("content", "Hello World?")
}),
validateCertificate: false);

Assert.Equal("content=Hello+World%3F", result);
}
}

[Fact]
public void UseHttpsWithBothOptionsAndCallback_ThrowsIfCallbackOptionsIsNull()
{
var serverOptions = CreateServerOptions();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
KestrelServerOptions = serverOptions
};

var httpsOptions = new HttpsConnectionAdapterOptions();

var ex = Assert.Throws<ArgumentNullException>(() =>
listenOptions.UseHttps(httpsOptions, callbackOptions: null));
Assert.Equal("callbackOptions", ex.ParamName);
}

[Fact]
public void UseHttpsWithBothOptionsAndCallback_ThrowsIfHttpsOptionsIsNull()
{
var serverOptions = CreateServerOptions();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
KestrelServerOptions = serverOptions
};

var callbackOptions = new TlsHandshakeCallbackOptions()
{
OnConnection = context => ValueTask.FromResult(new SslServerAuthenticationOptions())
};

var ex = Assert.Throws<ArgumentNullException>(() =>
listenOptions.UseHttps(httpsOptions: null, callbackOptions));
Assert.Equal("httpsOptions", ex.ParamName);
}

[Fact]
public void UseHttpsWithBothOptionsAndCallback_ThrowsIfOnConnectionIsNull()
{
var serverOptions = CreateServerOptions();
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
KestrelServerOptions = serverOptions
};

var httpsOptions = new HttpsConnectionAdapterOptions();
var callbackOptions = new TlsHandshakeCallbackOptions();

var ex = Assert.Throws<ArgumentException>(() =>
listenOptions.UseHttps(httpsOptions, callbackOptions));
Assert.Contains("OnConnection", ex.Message);
}

private static async Task AssertConnectionResult(SslStream stream, bool success, string body = null)
{
var request = body == null ? Encoding.UTF8.GetBytes("GET / HTTP/1.0\r\n\r\n")
Expand Down
Loading