File: SniOptionsSelectorTests.cs
Web Access
Project: src\src\Servers\Kestrel\Core\test\Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj (Microsoft.AspNetCore.Server.Kestrel.Core.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.IO.Pipelines;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging;
using Moq;
 
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests;
 
public class SniOptionsSelectorTests
{
    private static readonly X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate();
 
    [Fact]
    public void PrefersExactMatchOverWildcardPrefixOverWildcardOnly()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "www.example.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "Exact"
                        }
                    }
                },
                {
                    "*.example.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "WildcardPrefix"
                        }
                    }
                },
                {
                    "*",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "WildcardOnly"
                        }
                    }
                }
            };
 
        var mockCertificateConfigLoader = new MockCertificateConfigLoader();
        var pathDictionary = mockCertificateConfigLoader.CertToPathDictionary;
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            mockCertificateConfigLoader,
            fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (wwwSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
        Assert.Equal("Exact", pathDictionary[wwwSubdomainOptions.ServerCertificate]);
 
        var (baSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "b.a.example.org");
        Assert.Equal("WildcardPrefix", pathDictionary[baSubdomainOptions.ServerCertificate]);
 
        var (aSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "a.example.org");
        Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.ServerCertificate]);
 
        // "*.example.org" is preferred over "*", but "*.example.org" doesn't match "example.org".
        // REVIEW: Are we OK with "example.org" matching "*" instead of "*.example.org"? It feels annoying to me to have to configure example.org twice.
        // Unfortunately, the alternative would have "a.example.org" match "*.a.example.org" before "*.example.org", and that just seems wrong.
        var (noSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "example.org");
        Assert.Equal("WildcardOnly", pathDictionary[noSubdomainOptions.ServerCertificate]);
 
        var (anotherTldOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "dot.net");
        Assert.Equal("WildcardOnly", pathDictionary[anotherTldOptions.ServerCertificate]);
    }
 
    [Fact]
    public void PerfersLongerWildcardPrefixOverShorterWildcardPrefix()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "*.a.example.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "Long"
                        }
                    }
                },
                {
                    "*.example.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "Short"
                        }
                    }
                }
            };
 
        var mockCertificateConfigLoader = new MockCertificateConfigLoader();
        var pathDictionary = mockCertificateConfigLoader.CertToPathDictionary;
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            mockCertificateConfigLoader,
            fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (baSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "b.a.example.org");
        Assert.Equal("Long", pathDictionary[baSubdomainOptions.ServerCertificate]);
 
        // "*.a.example.org" is preferred over "*.example.org", but "a.example.org" doesn't match "*.a.example.org".
        var (aSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "a.example.org");
        Assert.Equal("Short", pathDictionary[aSubdomainOptions.ServerCertificate]);
    }
 
    [Fact]
    public void ServerNameMatchingIsCaseInsensitive()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "Www.Example.Org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "Exact"
                        }
                    }
                },
                {
                    "*.Example.Org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "WildcardPrefix"
                        }
                    }
                }
            };
 
        var mockCertificateConfigLoader = new MockCertificateConfigLoader();
        var pathDictionary = mockCertificateConfigLoader.CertToPathDictionary;
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            mockCertificateConfigLoader,
            fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (wwwSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "wWw.eXample.oRg");
        Assert.Equal("Exact", pathDictionary[wwwSubdomainOptions.ServerCertificate]);
 
        var (baSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "B.a.eXample.oRg");
        Assert.Equal("WildcardPrefix", pathDictionary[baSubdomainOptions.ServerCertificate]);
 
        var (aSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "A.eXample.oRg");
        Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.ServerCertificate]);
    }
 
    [Fact]
    public void FullChainCertsCanBeLoaded()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "Www.Example.Org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "Exact"
                        }
                    }
                },
                {
                    "*.Example.Org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "WildcardPrefix"
                        }
                    }
                }
            };
 
        var mockCertificateConfigLoader = new MockCertificateConfigLoader();
        var pathDictionary = mockCertificateConfigLoader.CertToPathDictionary;
        var fullChainDictionary = mockCertificateConfigLoader.CertToFullChain;
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            mockCertificateConfigLoader,
            fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (wwwSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "wWw.eXample.oRg");
        Assert.Equal("Exact", pathDictionary[wwwSubdomainOptions.ServerCertificate]);
 
        var (baSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "B.a.eXample.oRg");
        Assert.Equal("WildcardPrefix", pathDictionary[baSubdomainOptions.ServerCertificate]);
 
        var (aSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "A.eXample.oRg");
        Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.ServerCertificate]);
 
        /*
         * Chain test certs were created using smallstep cli: https://github.com/smallstep/cli
         * root_ca(pwd: testroot) -> 
         * intermediate_ca 1(pwd: inter) -> 
         * intermediate_ca 2(pwd: inter) -> 
         * leaf.com(pwd: leaf) (bundled)
         */
        var fullChain = fullChainDictionary[aSubdomainOptions.ServerCertificate];
        // Expect intermediate 2 cert and leaf.com
        Assert.Equal(2, fullChain.Count);
        Assert.Equal("CN=leaf.com", fullChain[0].Subject);
        Assert.Equal("CN=Test Intermediate CA 2", fullChain[0].IssuerName.Name);
        Assert.Equal("CN=Test Intermediate CA 2", fullChain[1].Subject);
        Assert.Equal("CN=Test Intermediate CA 1", fullChain[1].IssuerName.Name);
    }
 
    [Fact]
    public void MultipleWildcardPrefixServerNamesOfSameLengthAreAllowed()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "*.a.example.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "a"
                        }
                    }
                },
                {
                    "*.b.example.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "b"
                        }
                    }
                }
            };
 
        var mockCertificateConfigLoader = new MockCertificateConfigLoader();
        var pathDictionary = mockCertificateConfigLoader.CertToPathDictionary;
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            mockCertificateConfigLoader,
            fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (aSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "c.a.example.org");
        Assert.Equal("a", pathDictionary[aSubdomainOptions.ServerCertificate]);
 
        var (bSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "c.b.example.org");
        Assert.Equal("b", pathDictionary[bSubdomainOptions.ServerCertificate]);
    }
 
    [Fact]
    public void DuplicateWildcardPrefixServerNamesThrowsArgumentException()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "*.example.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "a"
                        }
                    }
                },
                {
                    "*.EXAMPLE.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "b"
                        }
                    }
                }
            };
 
        var mockCertificateConfigLoader = new MockCertificateConfigLoader();
        var pathDictionary = mockCertificateConfigLoader.CertToPathDictionary;
 
        var exception = Assert.Throws<ArgumentException>(() => new SniOptionsSelector(
             "TestEndpointName",
             sniDictionary,
             mockCertificateConfigLoader,
             fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
             fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
             logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>()));
        Assert.Equal("An item with the same key has already been added. Key: .EXAMPLE.org (Parameter 'key')", exception.Message);
    }
 
    [Fact]
    public void GetOptionsThrowsAnAuthenticationExceptionIfThereIsNoMatchingSniSection()
    {
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            new Dictionary<string, SniConfig>(),
            new MockCertificateConfigLoader(),
            fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var authExWithServerName = Assert.Throws<AuthenticationException>(() => sniOptionsSelector.GetOptions(new MockConnectionContext(), "example.org"));
        Assert.Equal(CoreStrings.FormatSniNotConfiguredForServerName("example.org", "TestEndpointName"), authExWithServerName.Message);
 
        var authExWithoutServerName = Assert.Throws<AuthenticationException>(() => sniOptionsSelector.GetOptions(new MockConnectionContext(), null));
        Assert.Equal(CoreStrings.FormatSniNotConfiguredToAllowNoServerName("TestEndpointName"), authExWithoutServerName.Message);
    }
 
    [Fact]
    public void WildcardOnlyMatchesNullServerNameDueToNoAlpn()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "*",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig
                        {
                            Path = "WildcardOnly"
                        }
                    }
                }
            };
 
        var mockCertificateConfigLoader = new MockCertificateConfigLoader();
        var pathDictionary = mockCertificateConfigLoader.CertToPathDictionary;
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            mockCertificateConfigLoader,
            fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), null);
        Assert.Equal("WildcardOnly", pathDictionary[options.ServerCertificate]);
    }
 
    [Fact]
    public void CachesSslServerAuthenticationOptions()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "www.example.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig()
                    }
                }
            };
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            new MockCertificateConfigLoader(),
            fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (options1, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
        var (options2, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
        Assert.Same(options1, options2);
    }
 
    [Fact]
    public void ClonesSslServerAuthenticationOptionsIfAnOnAuthenticateCallbackIsDefined()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "www.example.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig()
                    }
                }
            };
 
        SslServerAuthenticationOptions lastSeenSslOptions = null;
 
        var fallbackOptions = new HttpsConnectionAdapterOptions
        {
            OnAuthenticate = (context, sslOptions) =>
            {
                lastSeenSslOptions = sslOptions;
            }
        };
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            new MockCertificateConfigLoader(),
            fallbackOptions,
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (options1, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
        Assert.Same(lastSeenSslOptions, options1);
 
        var (options2, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
        Assert.Same(lastSeenSslOptions, options2);
 
        Assert.NotSame(options1, options2);
    }
 
    [Fact]
    public void ClonesSslServerAuthenticationOptionsIfTheFallbackServerCertificateSelectorIsUsed()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "selector.example.org",
                    new SniConfig()
                },
                {
                    "config.example.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig()
                    }
                }
            };
 
        var selectorCertificate = _x509Certificate2;
 
        var fallbackOptions = new HttpsConnectionAdapterOptions
        {
            ServerCertificate = new X509Certificate2(Array.Empty<byte>()),
            ServerCertificateSelector = (context, serverName) => selectorCertificate
        };
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            new MockCertificateConfigLoader(),
            fallbackOptions,
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (selectorOptions1, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "selector.example.org");
        Assert.Same(selectorCertificate, selectorOptions1.ServerCertificate);
 
        var (selectorOptions2, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "selector.example.org");
        Assert.Same(selectorCertificate, selectorOptions2.ServerCertificate);
 
        // The SslServerAuthenticationOptions were cloned because the cert came from the ServerCertificateSelector fallback.
        Assert.NotSame(selectorOptions1, selectorOptions2);
 
        var (configOptions1, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "config.example.org");
        Assert.NotSame(selectorCertificate, configOptions1.ServerCertificate);
 
        var (configOptions2, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "config.example.org");
        Assert.NotSame(selectorCertificate, configOptions2.ServerCertificate);
 
        // The SslServerAuthenticationOptions don't need to be cloned if a static cert is defined in config for the given server name.
        Assert.Same(configOptions1, configOptions2);
    }
 
    [Fact]
    public void ConstructorThrowsInvalidOperationExceptionIfNoCertificateDefiniedInConfigOrFallback()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                { "www.example.org", new SniConfig() }
            };
 
        var ex = Assert.Throws<InvalidOperationException>(
            () => new SniOptionsSelector(
                "TestEndpointName",
                sniDictionary,
                new MockCertificateConfigLoader(),
                fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
                fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
                logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>()));
 
        Assert.Equal(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound, ex.Message);
    }
 
    [Fact]
    public void FallsBackToHttpsConnectionAdapterCertificate()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                { "www.example.org", new SniConfig() }
            };
        var fallbackOptions = new HttpsConnectionAdapterOptions
        {
            ServerCertificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword")
        };
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            new MockCertificateConfigLoader(),
            fallbackOptions,
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
        Assert.Same(fallbackOptions.ServerCertificate, options.ServerCertificate);
    }
 
    [Fact]
    public void FallsBackToHttpsConnectionAdapterServerCertificateSelectorOverServerCertificate()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                { "www.example.org", new SniConfig() }
            };
 
        var selectorCertificate = _x509Certificate2;
 
        var fallbackOptions = new HttpsConnectionAdapterOptions
        {
            ServerCertificate = new X509Certificate2(Array.Empty<byte>()),
            ServerCertificateSelector = (context, serverName) => selectorCertificate
        };
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            new MockCertificateConfigLoader(),
            fallbackOptions,
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
        Assert.Same(selectorCertificate, options.ServerCertificate);
    }
 
    [Fact]
    public void PrefersHttpProtocolsDefinedInSniConfig()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "www.example.org",
                    new SniConfig
                    {
                        Protocols = HttpProtocols.None,
                        Certificate = new CertificateConfig()
                    }
                }
            };
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            new MockCertificateConfigLoader(),
            new HttpsConnectionAdapterOptions(),
            fallbackHttpProtocols: HttpProtocols.Http1,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var mockConnectionContext = new MockConnectionContext();
        sniOptionsSelector.GetOptions(mockConnectionContext, "www.example.org");
 
        var httpProtocolsFeature = mockConnectionContext.Features.Get<HttpProtocolsFeature>();
        Assert.NotNull(httpProtocolsFeature);
        Assert.Equal(HttpProtocols.None, httpProtocolsFeature.HttpProtocols);
    }
 
    [Fact]
    public void ConfiguresAlpnBasedOnConfiguredHttpProtocols()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "www.example.org",
                    new SniConfig
                    {
                        // I'm not using Http1AndHttp2 or Http2 because I don't want to account for
                        // validation and normalization. Other tests cover that.
                        Protocols = HttpProtocols.Http1,
                        Certificate = new CertificateConfig()
                    }
                }
            };
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            new MockCertificateConfigLoader(),
            new HttpsConnectionAdapterOptions(),
            fallbackHttpProtocols: HttpProtocols.None,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
        var alpnList = options.ApplicationProtocols;
 
        Assert.NotNull(alpnList);
        var protocol = Assert.Single(alpnList);
        Assert.Equal(SslApplicationProtocol.Http11, protocol);
    }
 
    [Fact]
    public void FallsBackToFallbackHttpProtocols()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "www.example.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig()
                    }
                }
            };
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            new MockCertificateConfigLoader(),
            new HttpsConnectionAdapterOptions(),
            fallbackHttpProtocols: HttpProtocols.Http1,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var mockConnectionContext = new MockConnectionContext();
        sniOptionsSelector.GetOptions(mockConnectionContext, "www.example.org");
 
        var httpProtocolsFeature = mockConnectionContext.Features.Get<HttpProtocolsFeature>();
        Assert.NotNull(httpProtocolsFeature);
        Assert.Equal(HttpProtocols.Http1, httpProtocolsFeature.HttpProtocols);
    }
 
    [Fact]
    public void PrefersSslProtocolsDefinedInSniConfig()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "www.example.org",
                    new SniConfig
                    {
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
                        SslProtocols = SslProtocols.Tls13 | SslProtocols.Tls11,
#pragma warning restore SYSLIB0039
                        Certificate = new CertificateConfig()
                    }
                }
            };
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            new MockCertificateConfigLoader(),
            new HttpsConnectionAdapterOptions
            {
                SslProtocols = SslProtocols.Tls13
            },
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
        Assert.Equal(SslProtocols.Tls13 | SslProtocols.Tls11, options.EnabledSslProtocols);
#pragma warning restore SYSLIB0039
    }
 
    [Fact]
    public void FallsBackToFallbackSslProtocols()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "www.example.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig()
                    }
                }
            };
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            new MockCertificateConfigLoader(),
            new HttpsConnectionAdapterOptions
            {
                SslProtocols = SslProtocols.Tls13
            },
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (options, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
        Assert.Equal(SslProtocols.Tls13, options.EnabledSslProtocols);
    }
 
    [Fact]
    public void PrefersClientCertificateModeDefinedInSniConfig()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "www.example.org",
                    new SniConfig
                    {
                        ClientCertificateMode = ClientCertificateMode.DelayCertificate,
                        Certificate = new CertificateConfig()
                    }
                }
            };
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            new MockCertificateConfigLoader(),
            new HttpsConnectionAdapterOptions
            {
                ClientCertificateMode = ClientCertificateMode.AllowCertificate
            },
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (options, certMode) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
 
        Assert.Equal(ClientCertificateMode.DelayCertificate, certMode);
        Assert.False(options.ClientCertificateRequired);
 
        Assert.NotNull(options.RemoteCertificateValidationCallback);
        // The RemoteCertificateValidationCallback should first check if the certificate is null and return true since it's optional.
        Assert.True(options.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None));
    }
 
    [Fact]
    public void FallsBackToFallbackClientCertificateMode()
    {
        var sniDictionary = new Dictionary<string, SniConfig>
            {
                {
                    "www.example.org",
                    new SniConfig
                    {
                        Certificate = new CertificateConfig()
                    }
                }
            };
 
        var sniOptionsSelector = new SniOptionsSelector(
            "TestEndpointName",
            sniDictionary,
            new MockCertificateConfigLoader(),
            new HttpsConnectionAdapterOptions
            {
                ClientCertificateMode = ClientCertificateMode.AllowCertificate
            },
            fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
            logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
 
        var (options, certMode) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "www.example.org");
 
        Assert.Equal(ClientCertificateMode.AllowCertificate, certMode);
        // Despite the confusing name, ClientCertificateRequired being true simply requests a certificate from the client, but doesn't require it.
        Assert.True(options.ClientCertificateRequired);
 
        Assert.NotNull(options.RemoteCertificateValidationCallback);
        // The RemoteCertificateValidationCallback should see we're in the AllowCertificate mode and return true.
        Assert.True(options.RemoteCertificateValidationCallback(sender: null, certificate: null, chain: null, SslPolicyErrors.None));
    }
 
    [Fact]
    public void CloneSslOptionsClonesAllProperties()
    {
        var propertyNames = typeof(SslServerAuthenticationOptions).GetProperties().Select(property => property.Name).ToList();
 
        CipherSuitesPolicy cipherSuitesPolicy = null;
 
        if (!OperatingSystem.IsWindows())
        {
            try
            {
                // The CipherSuitesPolicy ctor throws a PlatformNotSupportedException on Windows.
                cipherSuitesPolicy = new CipherSuitesPolicy(new[] { TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 });
            }
            catch (PlatformNotSupportedException)
            {
                // The CipherSuitesPolicy ctor throws a PlatformNotSupportedException on Ubuntu 16.04.
                // I don't know exactly which other distros/versions throw PNEs, but it isn't super relevant to this test,
                // so let's just swallow this exception.
            }
        }
 
        // Set options properties to non-default values to verify they're copied.
        var options = new SslServerAuthenticationOptions
        {
            // Defaults to true
            AllowRenegotiation = false,
            // Defaults to null
            ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http2 },
            // Defaults to X509RevocationMode.NoCheck
            CertificateRevocationCheckMode = X509RevocationMode.Offline,
            // Defaults to null
            CipherSuitesPolicy = cipherSuitesPolicy,
            // Defaults to false
            ClientCertificateRequired = true,
            // Defaults to SslProtocols.None
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
            EnabledSslProtocols = SslProtocols.Tls13 | SslProtocols.Tls11,
#pragma warning restore SYSLIB0039
#pragma warning disable SYSLIB0040 // EncryptionPolicy.NoEncryption is obsolete
            // Defaults to EncryptionPolicy.RequireEncryption
            EncryptionPolicy = EncryptionPolicy.NoEncryption,
#pragma warning restore SYSLIB0040
            // Defaults to null
            RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true,
            // Defaults to null
            ServerCertificate = new X509Certificate2(Array.Empty<byte>()),
            // Defaults to null
            ServerCertificateContext = SslStreamCertificateContext.Create(_x509Certificate2, additionalCertificates: null, offline: true),
            // Defaults to null
            ServerCertificateSelectionCallback = (sender, serverName) => null,
            // Defaults to null
            CertificateChainPolicy = new X509ChainPolicy(),
            // Defaults to true
            AllowTlsResume = false,
        };
 
        var clonedOptions = SniOptionsSelector.CloneSslOptions(options);
 
        Assert.NotSame(options, clonedOptions);
 
        Assert.Equal(options.AllowRenegotiation, clonedOptions.AllowRenegotiation);
        Assert.True(propertyNames.Remove(nameof(options.AllowRenegotiation)));
 
        // Ensure the List<SslApplicationProtocol> is also cloned since it could be modified by a user callback.
        Assert.NotSame(options.ApplicationProtocols, clonedOptions.ApplicationProtocols);
        Assert.Equal(Assert.Single(options.ApplicationProtocols), Assert.Single(clonedOptions.ApplicationProtocols));
        Assert.True(propertyNames.Remove(nameof(options.ApplicationProtocols)));
 
        Assert.Equal(options.CertificateRevocationCheckMode, clonedOptions.CertificateRevocationCheckMode);
        Assert.True(propertyNames.Remove(nameof(options.CertificateRevocationCheckMode)));
 
        Assert.Same(options.CipherSuitesPolicy, clonedOptions.CipherSuitesPolicy);
        Assert.True(propertyNames.Remove(nameof(options.CipherSuitesPolicy)));
 
        Assert.Equal(options.ClientCertificateRequired, clonedOptions.ClientCertificateRequired);
        Assert.True(propertyNames.Remove(nameof(options.ClientCertificateRequired)));
 
        Assert.Equal(options.EnabledSslProtocols, clonedOptions.EnabledSslProtocols);
        Assert.True(propertyNames.Remove(nameof(options.EnabledSslProtocols)));
 
        Assert.Equal(options.EncryptionPolicy, clonedOptions.EncryptionPolicy);
        Assert.True(propertyNames.Remove(nameof(options.EncryptionPolicy)));
 
        Assert.Same(options.RemoteCertificateValidationCallback, clonedOptions.RemoteCertificateValidationCallback);
        Assert.True(propertyNames.Remove(nameof(options.RemoteCertificateValidationCallback)));
 
        // Technically the ServerCertificate could be reset/reimported, but I'm hoping this is uncommon. Trying to clone the certificate and/or context seems risky.
        Assert.Same(options.ServerCertificate, clonedOptions.ServerCertificate);
        Assert.True(propertyNames.Remove(nameof(options.ServerCertificate)));
 
        Assert.Same(options.ServerCertificateContext, clonedOptions.ServerCertificateContext);
        Assert.True(propertyNames.Remove(nameof(options.ServerCertificateContext)));
 
        Assert.Same(options.ServerCertificateSelectionCallback, clonedOptions.ServerCertificateSelectionCallback);
        Assert.True(propertyNames.Remove(nameof(options.ServerCertificateSelectionCallback)));
 
        Assert.Same(options.CertificateChainPolicy, clonedOptions.CertificateChainPolicy);
        Assert.True(propertyNames.Remove(nameof(options.CertificateChainPolicy)));
 
        Assert.Equal(options.AllowTlsResume, clonedOptions.AllowTlsResume);
        Assert.True(propertyNames.Remove(nameof(options.AllowTlsResume)));
 
        // Ensure we've checked every property. When new properties get added, we'll have to update this test along with the CloneSslOptions implementation.
        Assert.Empty(propertyNames);
    }
 
    private class MockCertificateConfigLoader : ICertificateConfigLoader
    {
        public Dictionary<object, string> CertToPathDictionary { get; } = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
        public Dictionary<object, X509Certificate2Collection> CertToFullChain { get; } = new Dictionary<object, X509Certificate2Collection>(ReferenceEqualityComparer.Instance);
 
        public bool IsTestMock => true;
 
        public (X509Certificate2, X509Certificate2Collection) LoadCertificate(CertificateConfig certInfo, string endpointName)
        {
            if (certInfo is null)
            {
                return (null, null);
            }
 
            var cert = TestResources.GetTestCertificate();
            CertToPathDictionary.Add(cert, certInfo.Path);
 
            var fullChain = TestResources.GetTestChain();
            CertToFullChain[cert] = fullChain;
 
            return (cert, fullChain);
        }
    }
 
    private class MockConnectionContext : ConnectionContext
    {
        public override IDuplexPipe Transport { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
        public override string ConnectionId { get; set; } = "MockConnectionId";
        public override IFeatureCollection Features { get; } = new FeatureCollection();
        public override IDictionary<object, object> Items { get; set; } = new Dictionary<object, object>();
    }
}