From a671f9652808921d6bbe74994c16065372bec6f6 Mon Sep 17 00:00:00 2001
From: Chris Ross <chrross@microsoft.com>
Date: Tue, 10 Aug 2021 16:56:21 -0700
Subject: [PATCH] Enable ServerCertificateSelector for HTTP/3 #34858 (#35243)

---
 .../Core/src/HttpsConnectionAdapterOptions.cs |  4 +-
 .../Infrastructure/TransportManager.cs        | 17 ++++
 .../Kestrel/Core/src/PublicAPI.Unshipped.txt  |  2 +-
 .../src/QuicTransportFactory.cs               |  7 +-
 .../Kestrel/samples/Http3SampleApp/Program.cs | 35 +++++++-
 .../Http3/Http3Helpers.cs                     | 77 ++++++++++++++++
 .../Http3/Http3RequestTests.cs                | 88 ++++---------------
 .../Http3/Http3TlsTests.cs                    | 69 +++++++++++++++
 8 files changed, 221 insertions(+), 78 deletions(-)
 create mode 100644 src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3Helpers.cs
 create mode 100644 src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs

diff --git a/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs b/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs
index 5d82b34e42c..e18238e8ce7 100644
--- a/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs
+++ b/src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs
@@ -42,13 +42,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
         /// <summary>
         /// <para>
         /// A callback that will be invoked to dynamically select a server certificate. This is higher priority than ServerCertificate.
-        /// If SNI is not available then the name parameter will be null.
+        /// If SNI is not available then the name parameter will be null. The <see cref="ConnectionContext"/> will be null for HTTP/3 connections.
         /// </para>
         /// <para>
         /// If the server certificate has an Extended Key Usage extension, the usages must include Server Authentication (OID 1.3.6.1.5.5.7.3.1).
         /// </para>
         /// </summary>
-        public Func<ConnectionContext, string?, X509Certificate2?>? ServerCertificateSelector { get; set; }
+        public Func<ConnectionContext?, string?, X509Certificate2?>? ServerCertificateSelector { get; set; }
 
         /// <summary>
         /// Specifies the client certificate requirements for a HTTPS connection. Defaults to <see cref="ClientCertificateMode.NoCertificate"/>.
diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs
index 392dd7b533f..6a09c909110 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs
@@ -12,6 +12,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Connections;
 using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
 
 namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
 {
@@ -66,6 +67,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
                     ApplicationProtocols = new List<SslApplicationProtocol>() { new SslApplicationProtocol("h3"), new SslApplicationProtocol("h3-29") }
                 };
 
+                if (listenOptions.HttpsOptions.ServerCertificateSelector != null)
+                {
+                    // We can't set both
+                    sslServerAuthenticationOptions.ServerCertificate = null;
+                    sslServerAuthenticationOptions.ServerCertificateSelectionCallback = (sender, host) =>
+                    {
+                        // There is no ConnectionContext available durring the QUIC handshake.
+                        var cert = listenOptions.HttpsOptions.ServerCertificateSelector(null, host);
+                        if (cert != null)
+                        {
+                            HttpsConnectionMiddleware.EnsureCertificateIsAllowedForServerAuth(cert);
+                        }
+                        return cert!;
+                    };
+                }
+
                 features.Set(sslServerAuthenticationOptions);
             }
 
diff --git a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt
index ab9bdb1830c..9bbfeb01dde 100644
--- a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt
+++ b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt
@@ -183,7 +183,7 @@ Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.OnAuthen
 Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.OnAuthenticate.set -> void
 Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.ServerCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2?
 Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.ServerCertificate.set -> void
-Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.ServerCertificateSelector.get -> System.Func<Microsoft.AspNetCore.Connections.ConnectionContext!, string?, System.Security.Cryptography.X509Certificates.X509Certificate2?>?
+Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.ServerCertificateSelector.get -> System.Func<Microsoft.AspNetCore.Connections.ConnectionContext?, string?, System.Security.Cryptography.X509Certificates.X509Certificate2?>?
 Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions.ServerCertificateSelector.set -> void
 Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader.AnyIPEndpoint(int port) -> Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader!
 Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader.AnyIPEndpoint(int port, System.Action<Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions!>! configure) -> Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader!
diff --git a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs
index db03c9a308b..8a82ae301f9 100644
--- a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs
+++ b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs
@@ -54,9 +54,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic
             {
                 throw new InvalidOperationException("Couldn't find HTTPS configuration for QUIC transport.");
             }
-            if (sslServerAuthenticationOptions.ServerCertificate == null)
+            if (sslServerAuthenticationOptions.ServerCertificate == null
+                && sslServerAuthenticationOptions.ServerCertificateContext == null
+                && sslServerAuthenticationOptions.ServerCertificateSelectionCallback == null)
             {
-                var message = $"{nameof(SslServerAuthenticationOptions)}.{nameof(SslServerAuthenticationOptions.ServerCertificate)} must be configured with a value.";
+                var message = $"{nameof(SslServerAuthenticationOptions)} must provide a server certificate using {nameof(SslServerAuthenticationOptions.ServerCertificate)},"
+                    + $" {nameof(SslServerAuthenticationOptions.ServerCertificateContext)}, or {nameof(SslServerAuthenticationOptions.ServerCertificateSelectionCallback)}.";
                 throw new InvalidOperationException(message);
             }
 
diff --git a/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs b/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs
index f66f6a01033..f590dcb9a2c 100644
--- a/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs
+++ b/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs
@@ -25,6 +25,7 @@ namespace Http3SampleApp
                     .ConfigureKestrel((context, options) =>
                     {
                         var cert = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, false);
+
                         options.ConfigureHttpsDefaults(httpsOptions =>
                         {
                             httpsOptions.ServerCertificate = cert;
@@ -54,13 +55,41 @@ namespace Http3SampleApp
                         {
                             listenOptions.UseHttps(httpsOptions =>
                             {
-                                httpsOptions.ServerCertificateSelector = (_, _) => cert;
+                                // ConnectionContext is null
+                                httpsOptions.ServerCertificateSelector = (context, host) => cert;
                             });
                             listenOptions.UseConnectionLogging();
-                            listenOptions.Protocols = HttpProtocols.Http1AndHttp2; // TODO: http3
+                            listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
                         });
 
+                        // No SslServerAuthenticationOptions callback is currently supported by QuicListener
                         options.ListenAnyIP(5004, listenOptions =>
+                        {
+                            listenOptions.UseHttps(httpsOptions =>
+                            {
+                                httpsOptions.OnAuthenticate = (_, sslOptions) => sslOptions.ServerCertificate = cert;
+                            });
+                            listenOptions.UseConnectionLogging();
+                            listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
+                        });
+
+                        // ServerOptionsSelectionCallback isn't currently supported by QuicListener
+                        options.ListenAnyIP(5005, listenOptions =>
+                        {
+                            ServerOptionsSelectionCallback callback = (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) =>
+                            {
+                                var options = new SslServerAuthenticationOptions()
+                                {
+                                    ServerCertificate = cert,
+                                };
+                                return new ValueTask<SslServerAuthenticationOptions>(options);
+                            };
+                            listenOptions.UseHttps(callback, state: null);
+                            listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
+                        });
+
+                        // TlsHandshakeCallbackOptions (ServerOptionsSelectionCallback) isn't currently supported by QuicListener
+                        options.ListenAnyIP(5006, listenOptions =>
                         {
                             listenOptions.UseHttps(new TlsHandshakeCallbackOptions()
                             {
@@ -74,7 +103,7 @@ namespace Http3SampleApp
                                 },
                             });
                             listenOptions.UseConnectionLogging();
-                            listenOptions.Protocols = HttpProtocols.Http1AndHttp2; // TODO: http3
+                            listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
                         });
                     })
                     .UseStartup<Startup>();
diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3Helpers.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3Helpers.cs
new file mode 100644
index 00000000000..32bc6f619f5
--- /dev/null
+++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3Helpers.cs
@@ -0,0 +1,77 @@
+// 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;
+using System.Net.Http;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Interop.FunctionalTests.Http3
+{
+    public static class Http3Helpers
+    {
+        public static HttpMessageInvoker CreateClient(TimeSpan? idleTimeout = null)
+        {
+            var handler = new SocketsHttpHandler();
+            handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions
+            {
+                RemoteCertificateValidationCallback = (_, __, ___, ____) => true,
+                TargetHost = "targethost"
+            };
+            if (idleTimeout != null)
+            {
+                handler.PooledConnectionIdleTimeout = idleTimeout.Value;
+            }
+
+            return new HttpMessageInvoker(handler);
+        }
+
+        public static IHostBuilder CreateHostBuilder(Action<IServiceCollection> configureServices, RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action<KestrelServerOptions> configureKestrel = null)
+        {
+            return new HostBuilder()
+                .ConfigureWebHost(webHostBuilder =>
+                {
+                    webHostBuilder
+                        .UseKestrel(o =>
+                        {
+                            if (configureKestrel == null)
+                            {
+                                o.Listen(IPAddress.Parse("127.0.0.1"), 0, listenOptions =>
+                                {
+                                    listenOptions.Protocols = protocol ?? HttpProtocols.Http3;
+                                    listenOptions.UseHttps();
+                                });
+                            }
+                            else
+                            {
+                                configureKestrel(o);
+                            }
+                        })
+                        .Configure(app =>
+                        {
+                            app.Run(requestDelegate);
+                        });
+                })
+                .ConfigureServices(configureServices)
+                .ConfigureHostOptions(o =>
+                {
+                    if (Debugger.IsAttached)
+                    {
+                        // Avoid timeout while debugging.
+                        o.ShutdownTimeout = TimeSpan.FromHours(1);
+                    }
+                    else
+                    {
+                        o.ShutdownTimeout = TimeSpan.FromSeconds(1);
+                    }
+                });
+        }
+    }
+}
diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs
index c5adad76592..81d705f5f59 100644
--- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs
+++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs
@@ -6,7 +6,6 @@ using System.Net;
 using System.Net.Http;
 using System.Net.Quic;
 using System.Text;
-using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Connections;
 using Microsoft.AspNetCore.Connections.Features;
 using Microsoft.AspNetCore.Hosting;
@@ -91,7 +90,7 @@ namespace Interop.FunctionalTests.Http3
             }, protocol: protocol);
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync().DefaultTimeout();
 
@@ -193,7 +192,7 @@ namespace Interop.FunctionalTests.Http3
             }, protocol: protocol);
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync();
 
@@ -232,7 +231,7 @@ namespace Interop.FunctionalTests.Http3
             }, protocol: protocol);
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync();
 
@@ -276,7 +275,7 @@ namespace Interop.FunctionalTests.Http3
             });
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync();
 
@@ -344,7 +343,7 @@ namespace Interop.FunctionalTests.Http3
             }, protocol: protocol);
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync().DefaultTimeout();
 
@@ -415,7 +414,7 @@ namespace Interop.FunctionalTests.Http3
             }, protocol: protocol);
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync().DefaultTimeout();
 
@@ -481,7 +480,7 @@ namespace Interop.FunctionalTests.Http3
             });
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync();
 
@@ -533,7 +532,7 @@ namespace Interop.FunctionalTests.Http3
             });
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync();
 
@@ -599,7 +598,7 @@ namespace Interop.FunctionalTests.Http3
             });
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync();
 
@@ -652,7 +651,7 @@ namespace Interop.FunctionalTests.Http3
             });
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync();
 
@@ -713,7 +712,7 @@ namespace Interop.FunctionalTests.Http3
                 });
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync();
 
@@ -778,7 +777,7 @@ namespace Interop.FunctionalTests.Http3
             {
                 await host.StartAsync();
 
-                var client = CreateClient();
+                var client = Http3Helpers.CreateClient();
                 try
                 {
                     var port = host.GetPort();
@@ -838,7 +837,7 @@ namespace Interop.FunctionalTests.Http3
                 });
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync();
 
@@ -943,7 +942,7 @@ namespace Interop.FunctionalTests.Http3
                 });
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync();
 
@@ -1020,7 +1019,7 @@ namespace Interop.FunctionalTests.Http3
             });
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync();
 
@@ -1077,7 +1076,7 @@ namespace Interop.FunctionalTests.Http3
             }, protocol: protocol);
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync().DefaultTimeout();
 
@@ -1172,7 +1171,7 @@ namespace Interop.FunctionalTests.Http3
             }, protocol: protocol);
 
             using (var host = builder.Build())
-            using (var client = CreateClient())
+            using (var client = Http3Helpers.CreateClient())
             {
                 await host.StartAsync().DefaultTimeout();
 
@@ -1215,60 +1214,9 @@ namespace Interop.FunctionalTests.Http3
             }
         }
 
-        private static HttpMessageInvoker CreateClient(TimeSpan? idleTimeout = null)
-        {
-            var handler = new SocketsHttpHandler();
-            handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions
-            {
-                RemoteCertificateValidationCallback = (_, __, ___, ____) => true
-            };
-            if (idleTimeout != null)
-            {
-                handler.PooledConnectionIdleTimeout = idleTimeout.Value;
-            }
-
-            return new HttpMessageInvoker(handler);
-        }
-
         private IHostBuilder CreateHostBuilder(RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action<KestrelServerOptions> configureKestrel = null)
         {
-            return new HostBuilder()
-                .ConfigureWebHost(webHostBuilder =>
-                {
-                    webHostBuilder
-                        .UseKestrel(o =>
-                        {
-                            if (configureKestrel == null)
-                            {
-                                o.Listen(IPAddress.Parse("127.0.0.1"), 0, listenOptions =>
-                                {
-                                    listenOptions.Protocols = protocol ?? HttpProtocols.Http3;
-                                    listenOptions.UseHttps();
-                                });
-                            }
-                            else
-                            {
-                                configureKestrel(o);
-                            }
-                        })
-                        .Configure(app =>
-                        {
-                            app.Run(requestDelegate);
-                        });
-                })
-                .ConfigureServices(AddTestLogging)
-                .ConfigureHostOptions(o =>
-                {
-                    if (Debugger.IsAttached)
-                    {
-                        // Avoid timeout while debugging.
-                        o.ShutdownTimeout = TimeSpan.FromHours(1);
-                    }
-                    else
-                    {
-                        o.ShutdownTimeout = TimeSpan.FromSeconds(1);
-                    }
-                });
+            return Http3Helpers.CreateHostBuilder(AddTestLogging, requestDelegate, protocol, configureKestrel);
         }
     }
 }
diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs
new file mode 100644
index 00000000000..234e32cf81b
--- /dev/null
+++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs
@@ -0,0 +1,69 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Net;
+using System.Net.Http;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace Interop.FunctionalTests.Http3
+{
+    [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/35070")]
+    public class Http3TlsTests : LoggedTest
+    {
+        [ConditionalFact]
+        [MsQuicSupported]
+        public async Task ServerCertificateSelector_Invoked()
+        {
+            var builder = CreateHostBuilder(async context =>
+            {
+                await context.Response.WriteAsync("Hello World");
+            }, configureKestrel: kestrelOptions =>
+            {
+                kestrelOptions.ListenAnyIP(0, listenOptions =>
+                {
+                    listenOptions.Protocols = HttpProtocols.Http3;
+                    listenOptions.UseHttps(httpsOptions =>
+                    {
+                        httpsOptions.ServerCertificateSelector = (context, host) =>
+                        {
+                            Assert.Null(context); // The context isn't available durring the quic handshake.
+                            Assert.Equal("localhost", host);
+                            return TestResources.GetTestCertificate();
+                        };
+                    });
+                });
+            });
+
+            using var host = builder.Build();
+            using var client = Http3Helpers.CreateClient();
+
+            await host.StartAsync().DefaultTimeout();
+
+            // Using localhost instead of 127.0.0.1 because IPs don't set SNI and the Host header isn't currently used as an override.
+            var request = new HttpRequestMessage(HttpMethod.Get, $"https://localhost:{host.GetPort()}/");
+            request.Version = HttpVersion.Version30;
+            request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
+            // https://github.com/dotnet/runtime/issues/57169 Host isn't used for SNI
+            request.Headers.Host = "testhost";
+
+            var response = await client.SendAsync(request, CancellationToken.None);
+            response.EnsureSuccessStatusCode();
+            var result = await response.Content.ReadAsStringAsync();
+            Assert.Equal(HttpVersion.Version30, response.Version);
+            Assert.Equal("Hello World", result);
+
+            await host.StopAsync().DefaultTimeout();
+        }
+
+        private IHostBuilder CreateHostBuilder(RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action<KestrelServerOptions> configureKestrel = null)
+        {
+            return Http3Helpers.CreateHostBuilder(AddTestLogging, requestDelegate, protocol, configureKestrel);
+        }
+    }
+}
-- 
GitLab