File: HttpsTests.cs
Web Access
Project: src\src\Servers\Kestrel\test\InMemory.FunctionalTests\InMemory.FunctionalTests.csproj (InMemory.FunctionalTests)
// 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.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
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;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Moq;
 
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests;
 
public class HttpsTests : LoggedTest
{
    private static readonly X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate();
 
    private static KestrelServerOptions CreateServerOptions()
    {
        // It's not actually going to be used - we just need to satisfy the check in ApplyDefaultCertificate
        var mockHttpsConfig = new Mock<IHttpsConfigurationService>();
        mockHttpsConfig.Setup(m => m.IsInitialized).Returns(true);
 
        var serverOptions = new KestrelServerOptions();
        serverOptions.ApplicationServices = new ServiceCollection()
            .AddLogging()
            .AddSingleton(mockHttpsConfig.Object)
            .AddSingleton(Mock.Of<IHostEnvironment>())
            .AddSingleton(new KestrelMetrics(new TestMeterFactory()))
            .BuildServiceProvider();
        return serverOptions;
    }
 
    [Fact]
    public void UseHttpsDefaultsToDefaultCert()
    {
        var serverOptions = CreateServerOptions();
        serverOptions.TestOverrideDefaultCertificate = _x509Certificate2;
 
        serverOptions.ListenLocalhost(5000, options =>
        {
            options.UseHttps();
        });
 
        Assert.False(serverOptions.IsDevelopmentCertificateLoaded);
 
        var ranUseHttpsAction = false;
        serverOptions.ListenLocalhost(5001, options =>
        {
            options.UseHttps(opt =>
            {
                // The default cert is applied after UseHttps.
                Assert.Null(opt.ServerCertificate);
                ranUseHttpsAction = true;
            });
        });
 
        Assert.True(ranUseHttpsAction);
        Assert.False(serverOptions.IsDevelopmentCertificateLoaded);
    }
 
    [Fact]
    public async Task UseHttpsWithAsyncCallbackDoesNotFallBackToDefaultCert()
    {
        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.None,
                        checkCertificateRevocation: false));
 
                Logger.LogTrace(ex, "AuthenticateAsClientAsync Exception");
            }
        }
 
        var errorException = Assert.Single(loggerProvider.ErrorLogger.ErrorExceptions);
        Assert.IsType<NotSupportedException>(errorException);
    }
 
    [Fact]
    public void ConfigureHttpsDefaultsNeverLoadsDefaultCert()
    {
        var serverOptions = CreateServerOptions();
        serverOptions.ConfigureHttpsDefaults(options =>
        {
            Assert.Null(options.ServerCertificate);
            options.ServerCertificate = _x509Certificate2;
            options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
        });
        var ranUseHttpsAction = false;
        serverOptions.ListenLocalhost(5000, options =>
        {
            options.UseHttps(opt =>
            {
                Assert.Equal(_x509Certificate2, opt.ServerCertificate);
                Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode);
                ranUseHttpsAction = true;
            });
        });
 
        Assert.True(ranUseHttpsAction);
 
        // Never lazy loaded
        Assert.False(serverOptions.IsDevelopmentCertificateLoaded);
        Assert.Null(serverOptions.DevelopmentCertificate);
    }
 
    [Fact]
    public void ConfigureCertSelectorNeverLoadsDefaultCert()
    {
        var serverOptions = CreateServerOptions();
        serverOptions.ConfigureHttpsDefaults(options =>
        {
            Assert.Null(options.ServerCertificate);
            Assert.Null(options.ServerCertificateSelector);
            options.ServerCertificateSelector = (features, name) =>
            {
                return _x509Certificate2;
            };
            options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
        });
        var ranUseHttpsAction = false;
        serverOptions.ListenLocalhost(5000, options =>
        {
            options.UseHttps(opt =>
            {
                Assert.Null(opt.ServerCertificate);
                Assert.NotNull(opt.ServerCertificateSelector);
                Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode);
                ranUseHttpsAction = true;
            });
        });
 
        Assert.True(ranUseHttpsAction);
 
        // Never lazy loaded
        Assert.False(serverOptions.IsDevelopmentCertificateLoaded);
        Assert.Null(serverOptions.DevelopmentCertificate);
    }
 
    [ConditionalFact]
    [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)] // Investigation: https://github.com/dotnet/aspnetcore/issues/22917
    public async Task EmptyRequestLoggedAsDebug()
    {
        var loggerProvider = new HandshakeErrorLoggerProvider();
        LoggerFactory.AddProvider(loggerProvider);
 
        await using (var server = new TestServer(context => Task.CompletedTask,
            new TestServiceContext(LoggerFactory),
            listenOptions =>
            {
                listenOptions.UseHttps(_x509Certificate2);
            }))
        {
            using (var connection = server.CreateConnection())
            {
                // Close socket immediately
            }
 
            await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout();
        }
 
        Assert.Equal(1, loggerProvider.FilterLogger.LastEventId.Id);
        Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
        Assert.True(loggerProvider.ErrorLogger.ErrorMessages.Count == 0,
            userMessage: string.Join(Environment.NewLine, loggerProvider.ErrorLogger.ErrorMessages));
    }
 
    [ConditionalFact]
    [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)] // Investigation: https://github.com/dotnet/aspnetcore/issues/22917
    public async Task ClientHandshakeFailureLoggedAsDebug()
    {
        var loggerProvider = new HandshakeErrorLoggerProvider();
        LoggerFactory.AddProvider(loggerProvider);
 
        await using (var server = new TestServer(context => Task.CompletedTask,
            new TestServiceContext(LoggerFactory),
            listenOptions =>
            {
                listenOptions.UseHttps(_x509Certificate2);
            }))
        {
            using (var connection = server.CreateConnection())
            {
                // Send null bytes and close socket
                await connection.Stream.WriteAsync(new byte[10], 0, 10);
            }
 
            await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout();
        }
 
        Assert.Equal(1, loggerProvider.FilterLogger.LastEventId.Id);
        Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
        Assert.True(loggerProvider.ErrorLogger.ErrorMessages.Count == 0,
            userMessage: string.Join(Environment.NewLine, loggerProvider.ErrorLogger.ErrorMessages));
    }
 
    // Regression test for https://github.com/aspnet/KestrelHttpServer/issues/1103#issuecomment-246971172
    [Fact]
    public async Task DoesNotThrowObjectDisposedExceptionOnConnectionAbort()
    {
        var loggerProvider = new HandshakeErrorLoggerProvider();
        LoggerFactory.AddProvider(loggerProvider);
 
        await using (var server = new TestServer(async httpContext =>
            {
                var ct = httpContext.RequestAborted;
                while (!ct.IsCancellationRequested)
                {
                    try
                    {
                        await httpContext.Response.WriteAsync("hello, world", ct);
                        await Task.Delay(1000, ct);
                    }
                    catch (TaskCanceledException)
                    {
                        // Don't regard connection abort as an error
                    }
                }
            },
            new TestServiceContext(LoggerFactory),
            listenOptions =>
            {
                listenOptions.UseHttps(_x509Certificate2);
            }))
        {
            using (var connection = server.CreateConnection())
            using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
            {
                await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
                    enabledSslProtocols: SslProtocols.None,
                    checkCertificateRevocation: false);
 
                var request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n");
                await sslStream.WriteAsync(request, 0, request.Length);
 
                await sslStream.ReadAsync(new byte[32], 0, 32);
            }
        }
 
        Assert.False(loggerProvider.ErrorLogger.ObjectDisposedExceptionLogged);
    }
 
    [Fact]
    public async Task DoesNotThrowObjectDisposedExceptionFromWriteAsyncAfterConnectionIsAborted()
    {
        var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var loggerProvider = new HandshakeErrorLoggerProvider();
        loggerProvider.FilterLogger = new HttpsConnectionFilterLogger(expectedEventId: 3); // HttpConnectionEstablished
        LoggerFactory.AddProvider(loggerProvider);
 
        await using (var server = new TestServer(async httpContext =>
            {
                httpContext.Abort();
                try
                {
                    await httpContext.Response.WriteAsync("hello, world");
                    tcs.SetResult();
                }
                catch (Exception ex)
                {
                    tcs.SetException(ex);
                }
            },
            new TestServiceContext(LoggerFactory),
            listenOptions =>
            {
                listenOptions.UseHttps(_x509Certificate2);
            }))
        {
            using (var connection = server.CreateConnection())
            using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
            {
                await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
                    enabledSslProtocols: SslProtocols.None,
                    checkCertificateRevocation: false);
 
                var request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n");
                await sslStream.WriteAsync(request, 0, request.Length);
 
                await sslStream.ReadAsync(new byte[32], 0, 32);
            }
 
            await tcs.Task.DefaultTimeout();
        }
    }
 
    // Regression test for https://github.com/aspnet/KestrelHttpServer/issues/1693
    [Fact]
    public async Task DoesNotThrowObjectDisposedExceptionOnEmptyConnection()
    {
        var loggerProvider = new HandshakeErrorLoggerProvider();
        LoggerFactory.AddProvider(loggerProvider);
 
        await using (var server = new TestServer(context => Task.CompletedTask,
            new TestServiceContext(LoggerFactory),
            listenOptions =>
            {
                listenOptions.UseHttps(_x509Certificate2);
            }))
        {
            using (var connection = server.CreateConnection())
            using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
            {
                await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
                    enabledSslProtocols: SslProtocols.None,
                    checkCertificateRevocation: false);
            }
        }
 
        Assert.False(loggerProvider.ErrorLogger.ObjectDisposedExceptionLogged);
    }
 
    // Regression test for https://github.com/aspnet/KestrelHttpServer/pull/1197
    [Fact]
    public async Task ConnectionFilterDoesNotLeakBlock()
    {
        var loggerProvider = new HandshakeErrorLoggerProvider();
        LoggerFactory.AddProvider(loggerProvider);
 
        await using (var server = new TestServer(context => Task.CompletedTask,
            new TestServiceContext(LoggerFactory),
            listenOptions =>
            {
                listenOptions.UseHttps(_x509Certificate2);
            }))
        {
            using (var connection = server.CreateConnection())
            {
                connection.Reset();
            }
        }
    }
 
    [Fact]
    public async Task HandshakeTimesOutAndIsLoggedAsDebug()
    {
        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(o =>
                {
                    o.ServerCertificate = new X509Certificate2(_x509Certificate2);
                    o.HandshakeTimeout = TimeSpan.FromMilliseconds(100);
                });
            }))
        {
            using (var connection = server.CreateConnection())
            {
                Assert.Equal(0, await connection.Stream.ReadAsync(new byte[1], 0, 1).DefaultTimeout());
            }
        }
 
        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());
            }
        }
 
        await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout();
        Assert.Equal(2, loggerProvider.FilterLogger.LastEventId);
        Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
    }
 
    [Fact]
    public async Task Http3_UseHttpsNoArgsWithDefaultCertificate_UseDefaultCertificate()
    {
        var serverOptions = CreateServerOptions();
        serverOptions.TestOverrideDefaultCertificate = _x509Certificate2;
 
        IFeatureCollection bindFeatures = null;
        var multiplexedConnectionListenerFactory = new MockMultiplexedConnectionListenerFactory();
        multiplexedConnectionListenerFactory.OnBindAsync = (ep, features) =>
        {
            bindFeatures = features;
        };
 
        var testContext = new TestServiceContext(LoggerFactory);
        testContext.ServerOptions = serverOptions;
        await using (var server = new TestServer(context => Task.CompletedTask,
            testContext,
            serverOptions =>
            {
                serverOptions.ListenLocalhost(5001, listenOptions =>
                {
                    listenOptions.Protocols = HttpProtocols.Http3;
                    listenOptions.UseHttps();
                });
            },
            services =>
            {
                services.AddSingleton<IMultiplexedConnectionListenerFactory>(multiplexedConnectionListenerFactory);
            }))
        {
        }
 
        Assert.NotNull(bindFeatures);
 
        var sslOptions = bindFeatures.Get<TlsConnectionCallbackOptions>();
        Assert.NotNull(sslOptions);
 
        var sslServerAuthenticationOptions = await sslOptions.OnConnection(new TlsConnectionCallbackContext(), CancellationToken.None);
        Assert.Equal(_x509Certificate2, sslServerAuthenticationOptions.ServerCertificate);
    }
 
    [Fact]
    public async Task Http3_ConfigureHttpsDefaults_Works()
    {
        var serverOptions = CreateServerOptions();
 
        IFeatureCollection bindFeatures = null;
        var multiplexedConnectionListenerFactory = new MockMultiplexedConnectionListenerFactory();
        multiplexedConnectionListenerFactory.OnBindAsync = (ep, features) =>
        {
            bindFeatures = features;
        };
 
        var testContext = new TestServiceContext(LoggerFactory);
        testContext.ServerOptions = serverOptions;
        await using (var server = new TestServer(context => Task.CompletedTask,
            testContext,
            serverOptions =>
            {
                serverOptions.ConfigureHttpsDefaults(https =>
                {
                    https.ServerCertificate = _x509Certificate2;
                });
                serverOptions.ListenLocalhost(5001, listenOptions =>
                {
                    listenOptions.Protocols = HttpProtocols.Http3;
                    listenOptions.UseHttps();
                });
            },
            services =>
            {
                services.AddSingleton<IMultiplexedConnectionListenerFactory>(multiplexedConnectionListenerFactory);
            }))
        {
        }
 
        Assert.NotNull(bindFeatures);
 
        var tlsOptions = bindFeatures.Get<TlsConnectionCallbackOptions>();
        Assert.NotNull(tlsOptions);
 
        var sslServerAuthenticationOptions = await tlsOptions.OnConnection(new TlsConnectionCallbackContext(), CancellationToken.None);
        Assert.Equal(_x509Certificate2, sslServerAuthenticationOptions.ServerCertificate);
    }
 
    [Fact]
    public async Task Http1And2And3_NoUseHttps_MultiplexBindNotCalled()
    {
        var serverOptions = CreateServerOptions();
        serverOptions.TestOverrideDefaultCertificate = _x509Certificate2;
 
        var bindCalled = false;
        var multiplexedConnectionListenerFactory = new MockMultiplexedConnectionListenerFactory();
        multiplexedConnectionListenerFactory.OnBindAsync = (ep, features) =>
        {
            bindCalled = true;
        };
 
        var testContext = new TestServiceContext(LoggerFactory);
        testContext.ServerOptions = serverOptions;
        await using (var server = new TestServer(context => Task.CompletedTask,
            testContext,
            serverOptions =>
            {
                serverOptions.ListenLocalhost(5001, listenOptions =>
                {
                    listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
                });
            },
            services =>
            {
                services.AddSingleton<IMultiplexedConnectionListenerFactory>(multiplexedConnectionListenerFactory);
            }))
        {
        }
 
        Assert.False(bindCalled);
    }
 
    [Fact]
    public async Task Http3_NoUseHttps_Throws()
    {
        var serverOptions = CreateServerOptions();
        serverOptions.TestOverrideDefaultCertificate = _x509Certificate2;
 
        var bindCalled = false;
        var multiplexedConnectionListenerFactory = new MockMultiplexedConnectionListenerFactory();
        multiplexedConnectionListenerFactory.OnBindAsync = (ep, features) =>
        {
            bindCalled = true;
        };
 
        var testContext = new TestServiceContext(LoggerFactory);
        testContext.ServerOptions = serverOptions;
        var ex = await Assert.ThrowsAsync<IOException>(async () =>
        {
            await using var server = new TestServer(context => Task.CompletedTask,
                testContext,
                serverOptions =>
                {
                    serverOptions.ListenLocalhost(5001, listenOptions =>
                    {
                        listenOptions.Protocols = HttpProtocols.Http3;
                    });
                },
                services =>
                {
                    services.AddSingleton<IMultiplexedConnectionListenerFactory>(multiplexedConnectionListenerFactory);
                });
        });
 
        Assert.False(bindCalled);
        Assert.Equal("HTTP/3 requires HTTPS.", ex.InnerException.InnerException.Message);
    }
 
    [Fact]
    public async Task Http3_ServerOptionsSelectionCallback_Works()
    {
        var serverOptions = CreateServerOptions();
        serverOptions.TestOverrideDefaultCertificate = _x509Certificate2;
 
        IFeatureCollection bindFeatures = null;
        var multiplexedConnectionListenerFactory = new MockMultiplexedConnectionListenerFactory();
        multiplexedConnectionListenerFactory.OnBindAsync = (ep, features) =>
        {
            bindFeatures = features;
        };
 
        var testState = new object();
        var testContext = new TestServiceContext(LoggerFactory);
        testContext.ServerOptions = serverOptions;
        await using (var server = new TestServer(context => Task.CompletedTask,
            testContext,
            serverOptions =>
            {
                serverOptions.ListenLocalhost(5001, listenOptions =>
                {
                    listenOptions.Protocols = HttpProtocols.Http3;
                    listenOptions.UseHttps((SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) =>
                    {
                        return ValueTask.FromResult(new SslServerAuthenticationOptions());
                    }, state: testState);
                });
            },
            services =>
            {
                services.AddSingleton<IMultiplexedConnectionListenerFactory>(multiplexedConnectionListenerFactory);
            }))
        {
        }
 
        Assert.NotNull(bindFeatures);
 
        var tlsOptions = bindFeatures.Get<TlsConnectionCallbackOptions>();
        Assert.NotNull(tlsOptions);
 
        Assert.Collection(tlsOptions.ApplicationProtocols, p => Assert.Equal(SslApplicationProtocol.Http3, p));
 
        Assert.Equal(testState, tlsOptions.OnConnectionState);
    }
 
    [Fact]
    public async Task Http3_TlsHandshakeCallbackOptions_Works()
    {
        var serverOptions = CreateServerOptions();
        serverOptions.TestOverrideDefaultCertificate = _x509Certificate2;
 
        IFeatureCollection bindFeatures = null;
        var multiplexedConnectionListenerFactory = new MockMultiplexedConnectionListenerFactory();
        multiplexedConnectionListenerFactory.OnBindAsync = (ep, features) =>
        {
            bindFeatures = features;
        };
 
        var testState = new object();
        var testContext = new TestServiceContext(LoggerFactory);
        testContext.ServerOptions = serverOptions;
        await using (var server = new TestServer(context => Task.CompletedTask,
            testContext,
            serverOptions =>
            {
                serverOptions.ListenLocalhost(5001, listenOptions =>
                {
                    listenOptions.Protocols = HttpProtocols.Http3;
                    listenOptions.UseHttps(new TlsHandshakeCallbackOptions()
                    {
                        OnConnection = context =>
                        {
                            return ValueTask.FromResult(new SslServerAuthenticationOptions());
                        },
                        OnConnectionState = testState
                    });
                });
            },
            services =>
            {
                services.AddSingleton<IMultiplexedConnectionListenerFactory>(multiplexedConnectionListenerFactory);
            }))
        {
        }
 
        Assert.NotNull(bindFeatures);
 
        var tlsOptions = bindFeatures.Get<TlsConnectionCallbackOptions>();
        Assert.Collection(tlsOptions.ApplicationProtocols, p => Assert.Equal(SslApplicationProtocol.Http3, p));
 
        Assert.NotNull(tlsOptions.OnConnection);
        Assert.Equal(testState, tlsOptions.OnConnectionState);
    }
 
    [Fact]
    public async Task ClientAttemptingToUseUnsupportedProtocolIsLoggedAsDebug()
    {
        var loggerProvider = new HandshakeErrorLoggerProvider();
        LoggerFactory.AddProvider(loggerProvider);
 
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
        await using (var server = new TestServer(context => Task.CompletedTask,
            new TestServiceContext(LoggerFactory),
            listenOptions =>
            {
                listenOptions.UseHttps(TestResources.GetTestCertificate("no_extensions.pfx"), httpsOptions =>
                {
                    httpsOptions.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11;
                });
            }))
        {
            using (var connection = server.CreateConnection())
            using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
            {
                // SslProtocols.Tls is TLS 1.0 which isn't supported by Kestrel by default.
                await Assert.ThrowsAnyAsync<Exception>(() =>
                    sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
                        enabledSslProtocols: SslProtocols.Tls,
                        checkCertificateRevocation: false));
            }
        }
#pragma warning restore SYSLIB0039
 
        await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout();
        Assert.Equal(1, loggerProvider.FilterLogger.LastEventId);
        Assert.Equal(LogLevel.Debug, loggerProvider.FilterLogger.LastLogLevel);
    }
 
    [Fact]
    public async Task OnAuthenticate_SeesOtherSettings()
    {
        var loggerProvider = new HandshakeErrorLoggerProvider();
        LoggerFactory.AddProvider(loggerProvider);
 
        var testCert = _x509Certificate2;
        var onAuthenticateCalled = false;
 
        await using (var server = new TestServer(context => Task.CompletedTask,
            new TestServiceContext(LoggerFactory),
            listenOptions =>
            {
                listenOptions.UseHttps(httpsOptions =>
                {
                    httpsOptions.ServerCertificate = testCert;
                    httpsOptions.OnAuthenticate = (connectionContext, authOptions) =>
                    {
                        Assert.Same(testCert, authOptions.ServerCertificate);
                        onAuthenticateCalled = true;
                    };
                });
            }))
        {
            using (var connection = server.CreateConnection())
            using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
            {
                await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
                        enabledSslProtocols: SslProtocols.None,
                        checkCertificateRevocation: false);
            }
        }
 
        Assert.True(onAuthenticateCalled, "onAuthenticateCalled");
    }
 
    [Fact]
    public async Task OnAuthenticate_CanSetSettings()
    {
        var loggerProvider = new HandshakeErrorLoggerProvider();
        LoggerFactory.AddProvider(loggerProvider);
 
        var testCert = _x509Certificate2;
        var onAuthenticateCalled = false;
 
        await using (var server = new TestServer(context => Task.CompletedTask,
            new TestServiceContext(LoggerFactory),
            listenOptions =>
            {
                listenOptions.UseHttps(httpsOptions =>
                {
                    httpsOptions.ServerCertificateSelector = (_, __) => throw new NotImplementedException();
                    httpsOptions.OnAuthenticate = (connectionContext, authOptions) =>
                    {
                        Assert.Null(authOptions.ServerCertificate);
                        Assert.NotNull(authOptions.ServerCertificateSelectionCallback);
                        authOptions.ServerCertificate = testCert;
                        authOptions.ServerCertificateSelectionCallback = null;
                        onAuthenticateCalled = true;
                    };
                });
            }))
        {
            using (var connection = server.CreateConnection())
            using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true))
            {
                await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
                        enabledSslProtocols: SslProtocols.None,
                        checkCertificateRevocation: false);
            }
        }
 
        Assert.True(onAuthenticateCalled, "onAuthenticateCalled");
    }
 
    private class HandshakeErrorLoggerProvider : ILoggerProvider
    {
        public HttpsConnectionFilterLogger FilterLogger { get; set; } = new HttpsConnectionFilterLogger();
        public ApplicationErrorLogger ErrorLogger { get; } = new ApplicationErrorLogger();
 
        public ILogger CreateLogger(string categoryName)
        {
            if (categoryName == TypeNameHelper.GetTypeDisplayName(typeof(HttpsConnectionMiddleware)))
            {
                return FilterLogger;
            }
            else
            {
                return ErrorLogger;
            }
        }
 
        public void Dispose()
        {
        }
    }
 
    private class HttpsConnectionFilterLogger : ILogger
    {
        private readonly int? _expectedEventId;
 
        public HttpsConnectionFilterLogger()
        {
        }
 
        public HttpsConnectionFilterLogger(int expectedEventId)
        {
            _expectedEventId = expectedEventId;
        }
 
        public LogLevel LastLogLevel { get; set; }
        public EventId LastEventId { get; set; }
        public TaskCompletionSource LogTcs { get; } = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
 
        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
        {
            if (!_expectedEventId.HasValue || _expectedEventId.Value == eventId)
            {
                LastLogLevel = logLevel;
                LastEventId = eventId;
                LogTcs.SetResult();
            }
        }
 
        public bool IsEnabled(LogLevel logLevel)
        {
            throw new NotImplementedException();
        }
 
        public IDisposable BeginScope<TState>(TState state)
        {
            throw new NotImplementedException();
        }
    }
 
    private class ApplicationErrorLogger : ILogger
    {
        public List<string> ErrorMessages => new List<string>();
        public List<Exception> ErrorExceptions { get; } = new List<Exception>();
 
        public bool ObjectDisposedExceptionLogged { get; set; }
 
        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
        {
            if (logLevel == LogLevel.Error)
            {
                var log = $"Log {logLevel}[{eventId}]: {formatter(state, exception)} {exception}";
                ErrorMessages.Add(log);
 
                if (exception != null)
                {
                    ErrorExceptions.Add(exception);
                }
            }
 
            if (exception is ObjectDisposedException)
            {
                ObjectDisposedExceptionLogged = true;
            }
        }
 
        public bool IsEnabled(LogLevel logLevel)
        {
            return true;
        }
 
        public IDisposable BeginScope<TState>(TState state)
        {
            return NullScope.Instance;
        }
    }
}