File: HttpsTests.cs
Web Access
Project: src\src\Servers\HttpSys\test\FunctionalTests\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj (Microsoft.AspNetCore.Server.HttpSys.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.Diagnostics;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.HttpSys.Internal;
using Microsoft.AspNetCore.InternalTesting;
using Windows.Win32.Networking.HttpServer;
using Xunit;
 
namespace Microsoft.AspNetCore.Server.HttpSys;
 
public class HttpsTests : LoggedTest
{
    private static readonly X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate("eku.client.pfx");
 
    [ConditionalFact]
    public async Task Https_200OK_Success()
    {
        using (Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
        {
            return Task.FromResult(0);
        }, LoggerFactory))
        {
            string response = await SendRequestAsync(address);
            Assert.Equal(string.Empty, response);
        }
    }
 
    [ConditionalFact]
    public async Task Https_SendHelloWorld_Success()
    {
        using (Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
        {
            byte[] body = Encoding.UTF8.GetBytes("Hello World");
            httpContext.Response.ContentLength = body.Length;
            return httpContext.Response.Body.WriteAsync(body, 0, body.Length);
        }, LoggerFactory))
        {
            string response = await SendRequestAsync(address);
            Assert.Equal("Hello World", response);
        }
    }
 
    [ConditionalFact]
    public async Task Https_EchoHelloWorld_Success()
    {
        using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
        {
            var input = await new StreamReader(httpContext.Request.Body).ReadToEndAsync();
            Assert.Equal("Hello World", input);
            var body = Encoding.UTF8.GetBytes("Hello World");
            httpContext.Response.ContentLength = body.Length;
            await httpContext.Response.Body.WriteAsync(body, 0, body.Length);
        }, LoggerFactory))
        {
            string response = await SendRequestAsync(address, "Hello World");
            Assert.Equal("Hello World", response);
        }
    }
 
    [ConditionalTheory]
    [InlineData(ClientCertificateMethod.NoCertificate)]
    [InlineData(ClientCertificateMethod.AllowCertificate)]
    [InlineData(ClientCertificateMethod.AllowRenegotation)]
    public async Task Https_ClientCertNotSent_ClientCertNotPresent(ClientCertificateMethod clientCertificateMethod)
    {
        using (Utilities.CreateDynamicHttpsServer("", out var root, out var address, options =>
        {
            options.ClientCertificateMethod = clientCertificateMethod;
        },
        async httpContext =>
        {
            var tls = httpContext.Features.Get<ITlsConnectionFeature>();
            Assert.NotNull(tls);
            Assert.Null(tls.ClientCertificate);
            var cert = await tls.GetClientCertificateAsync(CancellationToken.None);
            Assert.Null(cert);
            Assert.Null(tls.ClientCertificate);
        }))
        {
            var response = await SendRequestAsync(address);
            Assert.Equal(string.Empty, response);
        }
    }
 
    [ConditionalTheory]
    [InlineData(ClientCertificateMethod.NoCertificate)]
    [InlineData(ClientCertificateMethod.AllowCertificate)]
    [InlineData(ClientCertificateMethod.AllowRenegotation)]
    public async Task Https_ClientCertRequested_ClientCertPresent(ClientCertificateMethod clientCertificateMethod)
    {
        using (Utilities.CreateDynamicHttpsServer("", out var root, out var address, options =>
        {
            options.ClientCertificateMethod = clientCertificateMethod;
        },
        async httpContext =>
        {
            var tls = httpContext.Features.Get<ITlsConnectionFeature>();
            Assert.NotNull(tls);
            Assert.Null(tls.ClientCertificate);
            var cert = await tls.GetClientCertificateAsync(CancellationToken.None);
            if (clientCertificateMethod == ClientCertificateMethod.AllowRenegotation)
            {
                Assert.NotNull(cert);
                Assert.NotNull(tls.ClientCertificate);
            }
            else
            {
                Assert.Null(cert);
                Assert.Null(tls.ClientCertificate);
            }
        }))
        {
            Assert.NotNull(_x509Certificate2);
            var response = await SendRequestAsync(address, _x509Certificate2);
            Assert.Equal(string.Empty, response);
        }
    }
 
    [ConditionalFact]
    [MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win7)]
    public async Task Https_SkipsITlsHandshakeFeatureOnWin7()
    {
        using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
        {
            try
            {
                var tlsFeature = httpContext.Features.Get<ITlsHandshakeFeature>();
                Assert.Null(tlsFeature);
            }
            catch (Exception ex)
            {
                await httpContext.Response.WriteAsync(ex.ToString());
            }
        }, LoggerFactory))
        {
            string response = await SendRequestAsync(address);
            Assert.Equal(string.Empty, response);
        }
    }
 
    [ConditionalFact]
    [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win8)]
    public async Task Https_SetsITlsHandshakeFeature()
    {
        using (Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
        {
            var tlsFeature = httpContext.Features.Get<ITlsHandshakeFeature>();
            Assert.NotNull(tlsFeature);
            return httpContext.Response.WriteAsJsonAsync(tlsFeature);
        }, LoggerFactory))
        {
            string response = await SendRequestAsync(address);
            var result = System.Text.Json.JsonDocument.Parse(response).RootElement;
 
            var protocol = (SslProtocols)result.GetProperty("protocol").GetInt32();
            Assert.True(protocol > SslProtocols.None, "Protocol: " + protocol);
            Assert.True(Enum.IsDefined(typeof(SslProtocols), protocol), "Defined: " + protocol); // Mapping is required, make sure it's current
 
            var cipherAlgorithm = (CipherAlgorithmType)result.GetProperty("cipherAlgorithm").GetInt32();
            Assert.True(cipherAlgorithm > CipherAlgorithmType.Null, "Cipher: " + cipherAlgorithm);
 
            var cipherStrength = result.GetProperty("cipherStrength").GetInt32();
            Assert.True(cipherStrength > 0, "CipherStrength: " + cipherStrength);
 
            var hashAlgorithm = (HashAlgorithmType)result.GetProperty("hashAlgorithm").GetInt32();
            Assert.True(hashAlgorithm >= HashAlgorithmType.None, "HashAlgorithm: " + hashAlgorithm);
 
            var hashStrength = result.GetProperty("hashStrength").GetInt32();
            Assert.True(hashStrength >= 0, "HashStrength: " + hashStrength); // May be 0 for some algorithms
 
            var keyExchangeAlgorithm = (ExchangeAlgorithmType)result.GetProperty("keyExchangeAlgorithm").GetInt32();
            Assert.True(keyExchangeAlgorithm >= ExchangeAlgorithmType.None, "KeyExchangeAlgorithm: " + keyExchangeAlgorithm);
 
            var keyExchangeStrength = result.GetProperty("keyExchangeStrength").GetInt32();
            Assert.True(keyExchangeStrength >= 0, "KeyExchangeStrength: " + keyExchangeStrength);
 
            if (Environment.OSVersion.Version > new Version(10, 0, 19043, 0))
            {
                var hostName = result.GetProperty("hostName").ToString();
                Assert.Equal("localhost", hostName);
            }
        }
    }
 
    [ConditionalFact]
    [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win8)]
    public async Task Https_ITlsHandshakeFeature_MatchesIHttpSysExtensionInfoFeature()
    {
        using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
        {
            try
            {
                var tlsFeature = httpContext.Features.Get<ITlsHandshakeFeature>();
                var requestInfoFeature = httpContext.Features.Get<IHttpSysRequestInfoFeature>();
                Assert.NotNull(tlsFeature);
                Assert.NotNull(requestInfoFeature);
                Assert.True(requestInfoFeature.RequestInfo.Count > 0);
                var tlsInfo = requestInfoFeature.RequestInfo[(int)HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeSslProtocol];
                HTTP_SSL_PROTOCOL_INFO tlsCopy;
                unsafe
                {
                    using var handle = tlsInfo.Pin();
                    tlsCopy = Marshal.PtrToStructure<HTTP_SSL_PROTOCOL_INFO>((IntPtr)handle.Pointer);
                }
 
                // Assert.Equal(tlsFeature.Protocol, tlsCopy.Protocol); // These don't directly match because the native and managed enums use different values.
                Assert.Equal((uint)tlsFeature.CipherAlgorithm, tlsCopy.CipherType);
                Assert.Equal(tlsFeature.CipherStrength, (int)tlsCopy.CipherStrength);
                Assert.Equal((uint)tlsFeature.HashAlgorithm, tlsCopy.HashType);
                Assert.Equal(tlsFeature.HashStrength, (int)tlsCopy.HashStrength);
                Assert.Equal((uint)tlsFeature.KeyExchangeAlgorithm, tlsCopy.KeyExchangeType);
                Assert.Equal(tlsFeature.KeyExchangeStrength, (int)tlsCopy.KeyExchangeStrength);
            }
            catch (Exception ex)
            {
                await httpContext.Response.WriteAsync(ex.ToString());
            }
        }, LoggerFactory))
        {
            string response = await SendRequestAsync(address);
            Assert.Equal(string.Empty, response);
        }
    }
 
    [ConditionalFact]
    [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)]
    public async Task Https_SetsIHttpSysRequestTimingFeature()
    {
        using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
        {
            try
            {
                var requestTimingFeature = httpContext.Features.Get<IHttpSysRequestTimingFeature>();
                Assert.NotNull(requestTimingFeature);
                Assert.True(requestTimingFeature.Timestamps.Length > (int)HttpSysRequestTimingType.Http3HeaderDecodeEnd);
                Assert.True(requestTimingFeature.TryGetTimestamp(HttpSysRequestTimingType.RequestHeaderParseStart, out var headerStart));
                Assert.True(requestTimingFeature.TryGetTimestamp(HttpSysRequestTimingType.RequestHeaderParseEnd, out var headerEnd));
                Assert.True(requestTimingFeature.TryGetElapsedTime(HttpSysRequestTimingType.RequestHeaderParseStart, HttpSysRequestTimingType.RequestHeaderParseEnd, out var elapsed));
                Assert.Equal(Stopwatch.GetElapsedTime(headerStart, headerEnd), elapsed);
                Assert.False(requestTimingFeature.TryGetTimestamp(HttpSysRequestTimingType.Http3StreamStart, out var streamStart));
                Assert.False(requestTimingFeature.TryGetTimestamp((HttpSysRequestTimingType)int.MaxValue, out var invalid));
                Assert.False(requestTimingFeature.TryGetElapsedTime(HttpSysRequestTimingType.Http3StreamStart, HttpSysRequestTimingType.RequestHeaderParseStart, out elapsed));
            }
            catch (Exception ex)
            {
                await httpContext.Response.WriteAsync(ex.ToString());
            }
        }, LoggerFactory))
        {
            string response = await SendRequestAsync(address);
            Assert.Equal(string.Empty, response);
        }
    }
 
    private async Task<string> SendRequestAsync(string uri,
        X509Certificate cert = null)
    {
        var handler = new HttpClientHandler();
        handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
        if (cert != null)
        {
            handler.ClientCertificates.Add(cert);
        }
        using HttpClient client = new HttpClient(handler);
        return await client.GetStringAsync(uri);
    }
 
    private async Task<string> SendRequestAsync(string uri, string upload)
    {
        var handler = new WinHttpHandler();
        handler.ServerCertificateValidationCallback = (a, b, c, d) => true;
        using (HttpClient client = new HttpClient(handler))
        {
            HttpResponseMessage response = await client.PostAsync(uri, new StringContent(upload));
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }
 
    private X509Certificate2 FindClientCert()
    {
        var store = new X509Store();
        store.Open(OpenFlags.ReadOnly);
 
        foreach (var cert in store.Certificates)
        {
            bool isClientAuth = false;
            bool isSmartCard = false;
            foreach (var extension in cert.Extensions)
            {
                var eku = extension as X509EnhancedKeyUsageExtension;
                if (eku != null)
                {
                    foreach (var oid in eku.EnhancedKeyUsages)
                    {
                        if (oid.FriendlyName == "Client Authentication")
                        {
                            isClientAuth = true;
                        }
                        else if (oid.FriendlyName == "Smart Card Logon")
                        {
                            isSmartCard = true;
                            break;
                        }
                    }
                }
            }
 
            if (isClientAuth && !isSmartCard)
            {
                return cert;
            }
        }
        return null;
    }
}