Add async ServerOptionsSelectionCallback UseHttps overload (#25390)

This commit is contained in:
Stephen Halter 2020-08-31 17:13:19 -07:00 committed by GitHub
parent 071a539ae5
commit 04f23ecfc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 209 additions and 60 deletions

View File

@ -6,7 +6,6 @@ using System.IO;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
using Microsoft.Extensions.DependencyInjection;
@ -209,7 +208,8 @@ namespace Microsoft.AspNetCore.Hosting
}
/// <summary>
/// Configure Kestrel to use HTTPS.
/// Configure Kestrel to use HTTPS. 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>
@ -230,12 +230,44 @@ namespace Microsoft.AspNetCore.Hosting
return listenOptions;
}
/// <summary>
/// Configure Kestrel to use HTTPS. 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="serverOptionsSelectionCallback">Callback to configure HTTPS options.</param>
/// <param name="state">State for the <paramref name="serverOptionsSelectionCallback"/>.</param>
/// <returns>The <see cref="ListenOptions"/>.</returns>
public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, object state)
{
return listenOptions.UseHttps(serverOptionsSelectionCallback, state, HttpsConnectionAdapterOptions.DefaultHandshakeTimeout);
}
/// <summary>
/// Configure Kestrel to use HTTPS. 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="serverOptionsSelectionCallback">Callback to configure HTTPS options.</param>
/// <param name="state">State for the <paramref name="serverOptionsSelectionCallback"/>.</param>
/// <param name="handshakeTimeout">Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite.</param>
/// <returns>The <see cref="ListenOptions"/>.</returns>
public static ListenOptions UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, object state, TimeSpan handshakeTimeout)
{
// HttpsOptionsCallback is an internal delegate that is just the ServerOptionsSelectionCallback + a ConnectionContext parameter.
// Given that ConnectionContext will eventually be replaced by System.Net.Connections, it doesn't make much sense to make the HttpsOptionsCallback delegate public.
HttpsOptionsCallback adaptedCallback = (connection, stream, clientHelloInfo, state, cancellationToken) =>
serverOptionsSelectionCallback(stream, clientHelloInfo, state, cancellationToken);
return listenOptions.UseHttps(adaptedCallback, state, handshakeTimeout);
}
/// <summary>
/// Configure Kestrel to use HTTPS.
/// </summary>
/// <param name="listenOptions">The <see cref="ListenOptions"/> to configure.</param>
/// <param name="httpsOptionsCallback">Callback to configure HTTPS options.</param>
/// <param name="state">State for the <see cref="ServerOptionsSelectionCallback" />.</param>
/// <param name="state">State for the <paramref name="httpsOptionsCallback"/>.</param>
/// <param name="handshakeTimeout">Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite.</param>
/// <returns>The <see cref="ListenOptions"/>.</returns>
internal static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsOptionsCallback httpsOptionsCallback, object state, TimeSpan handshakeTimeout)

View File

@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
@ -109,15 +110,21 @@ namespace SampleApp
options.ListenAnyIP(basePort + 5, listenOptions =>
{
listenOptions.UseHttps(httpsOptions =>
var localhostCert = CertificateLoader.LoadFromStoreCert("localhost", "My", StoreLocation.CurrentUser, allowInvalid: true);
listenOptions.UseHttps((stream, clientHelloInfo, state, cancellationToken) =>
{
var localhostCert = CertificateLoader.LoadFromStoreCert("localhost", "My", StoreLocation.CurrentUser, allowInvalid: true);
httpsOptions.ServerCertificateSelector = (features, name) =>
// Here you would check the name, select an appropriate cert, and provide a fallback or fail for null names.
if (clientHelloInfo.ServerName != null && clientHelloInfo.ServerName != "localhost")
{
// Here you would check the name, select an appropriate cert, and provide a fallback or fail for null names.
return localhostCert;
};
});
throw new AuthenticationException($"The endpoint is not configured for sever name '{clientHelloInfo.ServerName}'.");
}
return new ValueTask<SslServerAuthenticationOptions>(new SslServerAuthenticationOptions
{
ServerCertificate = localhostCert
});
}, state: null);
});
options

View File

@ -123,6 +123,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
[Fact]
public async Task HandshakeDetailsAreAvailableAfterAsyncCallback()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(async (stream, clientHelloInfo, state, cancellationToken) =>
{
await Task.Yield();
return new SslServerAuthenticationOptions
{
ServerCertificate = _x509Certificate2,
};
}, state: null);
}
await using (var server = new TestServer(context =>
{
var tlsFeature = context.Features.Get<ITlsHandshakeFeature>();
Assert.NotNull(tlsFeature);
Assert.True(tlsFeature.Protocol > SslProtocols.None, "Protocol");
Assert.True(tlsFeature.CipherAlgorithm > CipherAlgorithmType.Null, "Cipher");
Assert.True(tlsFeature.CipherStrength > 0, "CipherStrength");
Assert.True(tlsFeature.HashAlgorithm >= HashAlgorithmType.None, "HashAlgorithm"); // May be None on Linux.
Assert.True(tlsFeature.HashStrength >= 0, "HashStrength"); // May be 0 for some algorithms
Assert.True(tlsFeature.KeyExchangeAlgorithm >= ExchangeAlgorithmType.None, "KeyExchangeAlgorithm"); // Maybe None on Windows 7
Assert.True(tlsFeature.KeyExchangeStrength >= 0, "KeyExchangeStrength"); // May be 0 on mac
return context.Response.WriteAsync("hello world");
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
var result = await server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false);
Assert.Equal("hello world", result);
}
}
[Fact]
public async Task RequireCertificateFailsWhenNoCertificate()
{
@ -166,22 +202,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
[Fact]
[QuarantinedTest("https://github.com/dotnet/runtime/issues/40402")]
public async Task ClientCertificateRequiredConfiguredInCallbackContinuesWhenNoCertificate()
public async Task AsyncCallbackSettingClientCertificateRequiredContinuesWhenNoCertificate()
{
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps((connection, stream, clientHelloInfo, state, cancellationToken) =>
listenOptions.UseHttps((stream, clientHelloInfo, state, cancellationToken) =>
new ValueTask<SslServerAuthenticationOptions>(new SslServerAuthenticationOptions
{
ServerCertificate = _x509Certificate2,
// From the API Docs: "Note that this is only a request --
// if no certificate is provided, the server still accepts the connection request."
// Not to mention this is equivalent to the test above.
ClientCertificateRequired = true,
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true,
CertificateRevocationCheckMode = X509RevocationMode.NoCheck
}), state: null, HttpsConnectionAdapterOptions.DefaultHandshakeTimeout);
}), state: null);
}
await using (var server = new TestServer(context =>
@ -255,6 +287,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
[Fact]
public async Task UsesProvidedAsyncCallback()
{
var selectorCalled = 0;
void ConfigureListenOptions(ListenOptions listenOptions)
{
listenOptions.UseHttps(async (stream, clientHelloInfo, state, cancellationToken) =>
{
await Task.Yield();
Assert.NotNull(stream);
Assert.Equal("localhost", clientHelloInfo.ServerName);
selectorCalled++;
return new SslServerAuthenticationOptions
{
ServerCertificate = _x509Certificate2
};
}, state: null);
}
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), ConfigureListenOptions))
{
using (var connection = server.CreateConnection())
{
var stream = OpenSslStream(connection.Stream);
await stream.AuthenticateAsClientAsync("localhost");
Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2));
Assert.Equal(1, selectorCalled);
}
}
}
[Fact]
public async Task UsesProvidedServerCertificateSelectorEachTime()
{

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
@ -13,7 +12,6 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
@ -21,13 +19,14 @@ using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
public class HttpsTests : LoggedTest
{
private static X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate();
private KestrelServerOptions CreateServerOptions()
{
var serverOptions = new KestrelServerOptions();
@ -41,8 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
public void UseHttpsDefaultsToDefaultCert()
{
var serverOptions = CreateServerOptions();
var defaultCert = TestResources.GetTestCertificate();
serverOptions.DefaultCertificate = defaultCert;
serverOptions.DefaultCertificate = _x509Certificate2;
serverOptions.ListenLocalhost(5000, options =>
{
@ -62,22 +60,51 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
Assert.False(serverOptions.IsDevCertLoaded);
}
[Fact]
public async Task UseHttpsWithAsyncCallbackDoeNotFallBackToDefaultCert()
{
var loggerProvider = new HandshakeErrorLoggerProvider();
LoggerFactory.AddProvider(loggerProvider);
var testContext = new TestServiceContext(LoggerFactory);
await using (var server = new TestServer(context => Task.CompletedTask,
testContext,
listenOptions =>
{
listenOptions.UseHttps((stream, clientHelloInfo, state, cancellationToken) =>
new ValueTask<SslServerAuthenticationOptions>(new SslServerAuthenticationOptions()), state: null);
}))
{
using (var connection = server.CreateConnection())
using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
{
var ex = await Assert.ThrowsAnyAsync<Exception>(() =>
sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
enabledSslProtocols: SslProtocols.Tls,
checkCertificateRevocation: false));
}
}
var errorException = Assert.Single(loggerProvider.ErrorLogger.ErrorExceptions);
Assert.IsType<NotSupportedException>(errorException);
}
[Fact]
public void ConfigureHttpsDefaultsNeverLoadsDefaultCert()
{
var serverOptions = CreateServerOptions();
var testCert = TestResources.GetTestCertificate();
serverOptions.ConfigureHttpsDefaults(options =>
{
Assert.Null(options.ServerCertificate);
options.ServerCertificate = testCert;
options.ServerCertificate = _x509Certificate2;
options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
serverOptions.ListenLocalhost(5000, options =>
{
options.UseHttps(opt =>
{
Assert.Equal(testCert, opt.ServerCertificate);
Assert.Equal(_x509Certificate2, opt.ServerCertificate);
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode);
});
});
@ -90,14 +117,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
public void ConfigureCertSelectorNeverLoadsDefaultCert()
{
var serverOptions = CreateServerOptions();
var testCert = TestResources.GetTestCertificate();
serverOptions.ConfigureHttpsDefaults(options =>
{
Assert.Null(options.ServerCertificate);
Assert.Null(options.ServerCertificateSelector);
options.ServerCertificateSelector = (features, name) =>
{
return testCert;
return _x509Certificate2;
};
options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
@ -126,7 +152,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.GetTestCertificate());
listenOptions.UseHttps(_x509Certificate2);
}))
{
using (var connection = server.CreateConnection())
@ -139,7 +165,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
Assert.Equal(1, loggerProvider.FilterLogger.LastEventId.Id);
Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
Assert.True(loggerProvider.ErrorLogger.TotalErrorsLogged == 0,
Assert.True(loggerProvider.ErrorLogger.ErrorMessages.Count == 0,
userMessage: string.Join(Environment.NewLine, loggerProvider.ErrorLogger.ErrorMessages));
}
@ -154,7 +180,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.GetTestCertificate());
listenOptions.UseHttps(_x509Certificate2);
}))
{
using (var connection = server.CreateConnection())
@ -168,7 +194,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
Assert.Equal(1, loggerProvider.FilterLogger.LastEventId.Id);
Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
Assert.True(loggerProvider.ErrorLogger.TotalErrorsLogged == 0,
Assert.True(loggerProvider.ErrorLogger.ErrorMessages.Count == 0,
userMessage: string.Join(Environment.NewLine, loggerProvider.ErrorLogger.ErrorMessages));
}
@ -198,7 +224,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.GetTestCertificate());
listenOptions.UseHttps(_x509Certificate2);
}))
{
using (var connection = server.CreateConnection())
@ -242,7 +268,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.GetTestCertificate());
listenOptions.UseHttps(_x509Certificate2);
}))
{
using (var connection = server.CreateConnection())
@ -273,7 +299,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.GetTestCertificate());
listenOptions.UseHttps(_x509Certificate2);
}))
{
using (var connection = server.CreateConnection())
@ -299,7 +325,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
new TestServiceContext(LoggerFactory),
listenOptions =>
{
listenOptions.UseHttps(TestResources.GetTestCertificate());
listenOptions.UseHttps(_x509Certificate2);
}))
{
using (var connection = server.CreateConnection())
@ -316,10 +342,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
LoggerFactory.AddProvider(loggerProvider);
var testContext = new TestServiceContext(LoggerFactory);
var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager);
var handshakeStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
TimeSpan handshakeTimeout = default;
await using (var server = new TestServer(context => Task.CompletedTask,
testContext,
@ -327,26 +349,47 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
listenOptions.UseHttps(o =>
{
o.ServerCertificate = new X509Certificate2(TestResources.GetTestCertificate());
o.OnAuthenticate = (_, __) =>
{
handshakeStartedTcs.SetResult();
};
handshakeTimeout = o.HandshakeTimeout;
o.ServerCertificate = new X509Certificate2(_x509Certificate2);
o.HandshakeTimeout = TimeSpan.FromMilliseconds(100);
});
}))
{
using (var connection = server.CreateConnection())
{
// HttpsConnectionAdapter dispatches via Task.Run() before starting the handshake.
// Wait for the handshake to start before advancing the system clock.
await handshakeStartedTcs.Task.DefaultTimeout();
Assert.Equal(0, await connection.Stream.ReadAsync(new byte[1], 0, 1).DefaultTimeout());
}
}
// Min amount of time between requests that triggers a handshake timeout.
testContext.MockSystemClock.UtcNow += handshakeTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1);
heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow);
await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout();
Assert.Equal(2, loggerProvider.FilterLogger.LastEventId);
Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
}
[Fact]
public async Task HandshakeTimesOutAndIsLoggedAsDebugWithAsyncCallback()
{
var loggerProvider = new HandshakeErrorLoggerProvider();
LoggerFactory.AddProvider(loggerProvider);
var testContext = new TestServiceContext(LoggerFactory);
await using (var server = new TestServer(context => Task.CompletedTask,
testContext,
listenOptions =>
{
listenOptions.UseHttps(async (stream, clientHelloInfo, state, cancellationToken) =>
{
await Task.Yield();
return new SslServerAuthenticationOptions
{
ServerCertificate = _x509Certificate2,
};
}, state: null, handshakeTimeout: TimeSpan.FromMilliseconds(100));
}))
{
using (var connection = server.CreateConnection())
{
Assert.Equal(0, await connection.Stream.ReadAsync(new byte[1], 0, 1).DefaultTimeout());
}
}
@ -394,7 +437,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
var loggerProvider = new HandshakeErrorLoggerProvider();
LoggerFactory.AddProvider(loggerProvider);
var testCert = TestResources.GetTestCertificate();
var testCert = _x509Certificate2;
var onAuthenticateCalled = false;
await using (var server = new TestServer(context => Task.CompletedTask,
@ -430,7 +473,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
var loggerProvider = new HandshakeErrorLoggerProvider();
LoggerFactory.AddProvider(loggerProvider);
var testCert = TestResources.GetTestCertificate();
var testCert = _x509Certificate2;
var onAuthenticateCalled = false;
await using (var server = new TestServer(context => Task.CompletedTask,
@ -511,11 +554,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
private class ApplicationErrorLogger : ILogger
{
private List<string> _errorMessages = new List<string>();
public IEnumerable<string> ErrorMessages => _errorMessages;
public int TotalErrorsLogged => _errorMessages.Count;
public List<string> ErrorMessages => new List<string>();
public List<Exception> ErrorExceptions { get; } = new List<Exception>();
public bool ObjectDisposedExceptionLogged { get; set; }
@ -524,7 +564,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
if (logLevel == LogLevel.Error)
{
var log = $"Log {logLevel}[{eventId}]: {formatter(state, exception)} {exception}";
_errorMessages.Add(log);
ErrorMessages.Add(log);
if (exception != null)
{
ErrorExceptions.Add(exception);
}
}
if (exception is ObjectDisposedException)