From 757457ae3879754d7889ccd537b5989f1808be03 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Thu, 6 Mar 2025 16:10:23 +0100 Subject: [PATCH 01/24] setup for tls clinet hello exposure --- AspNetCore.sln | 19 +++ .../src/Features/ITlsFingerprintingFeature.cs | 24 ++++ .../src/TLS/TlsClientHello.cs | 25 ++++ src/Servers/HttpSys/HttpSysServer.slnf | 3 +- .../HttpSys/HttpSysConfigurator.cs | 120 ++++++++++++++++++ .../TlsFeaturesObserve/HttpSys/Native.cs | 95 ++++++++++++++ .../samples/TlsFeaturesObserve/Program.cs | 35 +++++ .../Properties/launchSettings.json | 10 ++ .../samples/TlsFeaturesObserve/Startup.cs | 35 +++++ .../TlsFeaturesObserve.csproj | 14 ++ src/Servers/HttpSys/src/LoggerEventIds.cs | 1 + .../HttpSys/src/NativeInterop/HttpApi.cs | 8 +- .../HttpSys/src/RequestProcessing/Request.cs | 7 + .../RequestContext.FeatureCollection.cs | 7 + .../RequestProcessing/RequestContext.Log.cs | 6 + .../src/RequestProcessing/RequestContext.cs | 112 ++++++++++++++-- .../HttpSys/src/StandardFeatureCollection.cs | 1 + 17 files changed, 507 insertions(+), 15 deletions(-) create mode 100644 src/Servers/Connections.Abstractions/src/Features/ITlsFingerprintingFeature.cs create mode 100644 src/Servers/Connections.Abstractions/src/TLS/TlsClientHello.cs create mode 100644 src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs create mode 100644 src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/Native.cs create mode 100644 src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs create mode 100644 src/Servers/HttpSys/samples/TlsFeaturesObserve/Properties/launchSettings.json create mode 100644 src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs create mode 100644 src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj diff --git a/AspNetCore.sln b/AspNetCore.sln index 367d27911f8e..a16a5c63a6e9 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1784,6 +1784,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotReferencedInWasmCodePack EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.WasmRemoteAuthentication", "src\Components\test\testassets\Components.WasmRemoteAuthentication\Components.WasmRemoteAuthentication.csproj", "{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TlsFeaturesObserve", "src\Servers\HttpSys\samples\TlsFeaturesObserve\TlsFeaturesObserve.csproj", "{98C71EC8-1303-F55D-4032-E6728971770E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10753,6 +10755,22 @@ Global {8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x64.Build.0 = Release|Any CPU {8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x86.ActiveCfg = Release|Any CPU {8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x86.Build.0 = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|arm64.ActiveCfg = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|arm64.Build.0 = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|x64.ActiveCfg = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|x64.Build.0 = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|x86.ActiveCfg = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Debug|x86.Build.0 = Debug|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|Any CPU.Build.0 = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|arm64.ActiveCfg = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|arm64.Build.0 = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|x64.ActiveCfg = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|x64.Build.0 = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|x86.ActiveCfg = Release|Any CPU + {98C71EC8-1303-F55D-4032-E6728971770E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11634,6 +11652,7 @@ Global {F232B503-D412-45EE-8B31-EFD46B9FA302} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} {433F91E4-E39D-4EB0-B798-2998B3969A2C} = {6126DCE4-9692-4EE2-B240-C65743572995} {8A021D6D-7935-4AB3-BB47-38D4FF9B0D13} = {6126DCE4-9692-4EE2-B240-C65743572995} + {98C71EC8-1303-F55D-4032-E6728971770E} = {49016328-4D32-46E4-A4D2-94686ED38EA2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/Servers/Connections.Abstractions/src/Features/ITlsFingerprintingFeature.cs b/src/Servers/Connections.Abstractions/src/Features/ITlsFingerprintingFeature.cs new file mode 100644 index 000000000000..e0f1e2bea4f7 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/Features/ITlsFingerprintingFeature.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections.Abstractions.TLS; + +namespace Microsoft.AspNetCore.Connections.Features; + +/// +/// Represents the details about the TLS fingerprinting. +/// +public interface ITlsFingerprintingFeature +{ + /// + /// Returns the TLS client hello details, if any. + /// + TLS_CLIENT_HELLO GetTlsClientHello(); +} diff --git a/src/Servers/Connections.Abstractions/src/TLS/TlsClientHello.cs b/src/Servers/Connections.Abstractions/src/TLS/TlsClientHello.cs new file mode 100644 index 000000000000..56f695697bc4 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/TLS/TlsClientHello.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.Authentication; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Connections.Abstractions.TLS; + +public struct TLS_CLIENT_HELLO +{ + public SslProtocols ProtocolVersion; // Version of the TLS protocol + + public override string ToString() + { + return $""" + TLS CLIENT HELLO MESSAGE: + - ProtocolVersion: {ProtocolVersion} + """; + } +} diff --git a/src/Servers/HttpSys/HttpSysServer.slnf b/src/Servers/HttpSys/HttpSysServer.slnf index 4e0193ae8f4d..2b9ed68e48f0 100644 --- a/src/Servers/HttpSys/HttpSysServer.slnf +++ b/src/Servers/HttpSys/HttpSysServer.slnf @@ -37,6 +37,7 @@ "src\\Servers\\HttpSys\\samples\\QueueSharing\\QueueSharing.csproj", "src\\Servers\\HttpSys\\samples\\SelfHostServer\\SelfHostServer.csproj", "src\\Servers\\HttpSys\\samples\\TestClient\\TestClient.csproj", + "src\\Servers\\HttpSys\\samples\\TlsFeaturesObserve\\TlsFeaturesObserve.csproj", "src\\Servers\\HttpSys\\src\\Microsoft.AspNetCore.Server.HttpSys.csproj", "src\\Servers\\HttpSys\\test\\FunctionalTests\\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj", "src\\Servers\\HttpSys\\test\\NonHelixTests\\Microsoft.AspNetCore.Server.HttpSys.NonHelixTests.csproj", @@ -53,4 +54,4 @@ "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} \ No newline at end of file +} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs new file mode 100644 index 000000000000..5da6fee79e3b --- /dev/null +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.AspNetCore.Http; + +namespace TlsFeaturesObserve.HttpSys; + +internal static class HttpSysConfigurator +{ + const uint HTTP_INITIALIZE_CONFIG = 0x00000002; + const uint ERROR_ALREADY_EXISTS = 183; + + static readonly HTTPAPI_VERSION HttpApiVersion = new HTTPAPI_VERSION(1, 0); + + internal static void ConfigureCacheTlsClientHello() + { + IPEndPoint ipPort = new IPEndPoint(new IPAddress([0, 0, 0, 0]), 6000); + string certThumbprint = "" /* your cert thumbprint here */; + Guid appId = Guid.NewGuid(); + string sslCertStoreName = "My"; + + CallHttpApi(() => SetConfiguration(ipPort, certThumbprint, appId, sslCertStoreName)); + } + + static void SetConfiguration(IPEndPoint ipPort, string certThumbprint, Guid appId, string sslCertStoreName) + { + GCHandle sockAddrHandle = CreateSockaddrStructure(ipPort); + var pIpPort = sockAddrHandle.AddrOfPinnedObject(); + var httpServiceConfigSslKey = new HTTP_SERVICE_CONFIG_SSL_KEY(pIpPort); + + byte[] hash = GetHash(certThumbprint); + var handleHash = GCHandle.Alloc(hash, GCHandleType.Pinned); + var configSslParam = new HTTP_SERVICE_CONFIG_SSL_PARAM + { + AppId = appId, + DefaultFlags = 0x00008000 /* HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO */, + DefaultRevocationFreshnessTime = 0, + DefaultRevocationUrlRetrievalTimeout = 15, + pSslCertStoreName = sslCertStoreName, + pSslHash = handleHash.AddrOfPinnedObject(), + SslHashLength = hash.Length, + pDefaultSslCtlIdentifier = null, + pDefaultSslCtlStoreName = sslCertStoreName + }; + + var configSslSet = new HTTP_SERVICE_CONFIG_SSL_SET + { + ParamDesc = configSslParam, + KeyDesc = httpServiceConfigSslKey + }; + + var pInputConfigInfo = Marshal.AllocCoTaskMem( + Marshal.SizeOf(typeof(HTTP_SERVICE_CONFIG_SSL_SET))); + Marshal.StructureToPtr(configSslSet, pInputConfigInfo, false); + + uint status = HttpSetServiceConfiguration(nint.Zero, + HTTP_SERVICE_CONFIG_ID.HttpServiceConfigSSLCertInfo, + pInputConfigInfo, + Marshal.SizeOf(configSslSet), + nint.Zero); + + if (status == ERROR_ALREADY_EXISTS || status == 0) // already present or success + { + Console.WriteLine("HttpServiceConfiguration is correct"); + } + else + { + Console.WriteLine("Failed to HttpSetServiceConfiguration: " + status); + } + } + + static byte[] GetHash(string thumbprint) + { + int length = thumbprint.Length; + byte[] bytes = new byte[length / 2]; + for (int i = 0; i < length; i += 2) + bytes[i / 2] = Convert.ToByte(thumbprint.Substring(i, 2), 16); + return bytes; + } + + static GCHandle CreateSockaddrStructure(IPEndPoint ipEndPoint) + { + SocketAddress socketAddress = ipEndPoint.Serialize(); + + // use an array of bytes instead of the sockaddr structure + byte[] sockAddrStructureBytes = new byte[socketAddress.Size]; + GCHandle sockAddrHandle = GCHandle.Alloc(sockAddrStructureBytes, GCHandleType.Pinned); + for (int i = 0; i < socketAddress.Size; ++i) + { + sockAddrStructureBytes[i] = socketAddress[i]; + } + return sockAddrHandle; + } + + static void CallHttpApi(Action body) + { + const uint flags = HTTP_INITIALIZE_CONFIG; + uint retVal = HttpInitialize(HttpApiVersion, flags, IntPtr.Zero); + body(); + } + + [DllImport("httpapi.dll", SetLastError = true)] + private static extern uint HttpInitialize( + HTTPAPI_VERSION version, + uint flags, + IntPtr pReserved); + + [DllImport("httpapi.dll", SetLastError = true)] + public static extern uint HttpSetServiceConfiguration( + nint serviceIntPtr, + HTTP_SERVICE_CONFIG_ID configId, + nint pConfigInformation, + int configInformationLength, + nint pOverlapped); +} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/Native.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/Native.cs new file mode 100644 index 000000000000..291ca5c4d8dc --- /dev/null +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/Native.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace TlsFeaturesObserve.HttpSys; + +[StructLayout(LayoutKind.Sequential, Pack = 2)] +public struct HTTPAPI_VERSION +{ + public ushort HttpApiMajorVersion; + public ushort HttpApiMinorVersion; + + public HTTPAPI_VERSION(ushort majorVersion, ushort minorVersion) + { + HttpApiMajorVersion = majorVersion; + HttpApiMinorVersion = minorVersion; + } +} + +public enum HTTP_SERVICE_CONFIG_ID +{ + HttpServiceConfigIPListenList = 0, + HttpServiceConfigSSLCertInfo, + HttpServiceConfigUrlAclInfo, + HttpServiceConfigMax +} + +[StructLayout(LayoutKind.Sequential)] +public struct HTTP_SERVICE_CONFIG_SSL_SET +{ + public HTTP_SERVICE_CONFIG_SSL_KEY KeyDesc; + public HTTP_SERVICE_CONFIG_SSL_PARAM ParamDesc; +} + +[StructLayout(LayoutKind.Sequential)] +public struct HTTP_SERVICE_CONFIG_SSL_KEY +{ + public IntPtr pIpPort; + + public HTTP_SERVICE_CONFIG_SSL_KEY(IntPtr pIpPort) + { + this.pIpPort = pIpPort; + } +} + +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] +public struct HTTP_SERVICE_CONFIG_SSL_PARAM +{ + public int SslHashLength; + public IntPtr pSslHash; + public Guid AppId; + [MarshalAs(UnmanagedType.LPWStr)] + public string pSslCertStoreName; + public CertCheckModes DefaultCertCheckMode; + public int DefaultRevocationFreshnessTime; + public int DefaultRevocationUrlRetrievalTimeout; + [MarshalAs(UnmanagedType.LPWStr)] + public string pDefaultSslCtlIdentifier; + [MarshalAs(UnmanagedType.LPWStr)] + public string pDefaultSslCtlStoreName; + public uint DefaultFlags; // HTTP_SERVICE_CONFIG_SSL_FLAG +} + +[Flags] +public enum CertCheckModes : uint +{ + /// + /// Enables the client certificate revocation check. + /// + None = 0, + + /// + /// Client certificate is not to be verified for revocation. + /// + DoNotVerifyCertificateRevocation = 1, + + /// + /// Only cached certificate is to be used the revocation check. + /// + VerifyRevocationWithCachedCertificateOnly = 2, + + /// + /// The RevocationFreshnessTime setting is enabled. + /// + EnableRevocationFreshnessTime = 4, + + /// + /// No usage check is to be performed. + /// + NoUsageCheck = 0x10000 +} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs new file mode 100644 index 000000000000..742d013d02af --- /dev/null +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.HttpSys; +using Microsoft.Extensions.Hosting; +using TlsFeaturesObserve.HttpSys; + +namespace TlsFeatureObserve; + +public static class Program +{ + public static void Main(string[] args) + { + HttpSysConfigurator.ConfigureCacheTlsClientHello(); + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHost(webBuilder => + { + webBuilder.UseStartup() + .UseHttpSys(options => + { + // If you want to use https locally: https://stackoverflow.com/a/51841893 + options.UrlPrefixes.Add("https://*:6000"); // HTTPS + + options.Authentication.Schemes = AuthenticationSchemes.None; + options.Authentication.AllowAnonymous = true; + }); + }); +} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Properties/launchSettings.json b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Properties/launchSettings.json new file mode 100644 index 000000000000..c9d6b5efcb3c --- /dev/null +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "TlsFeaturesObserve": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "nativeDebugging": true + } + } +} \ No newline at end of file diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs new file mode 100644 index 000000000000..28d002567c7a --- /dev/null +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.HttpSys; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace TlsFeatureObserve; + +public class Startup +{ + public void Configure(IApplicationBuilder app) + { + app.Run(async (HttpContext context) => + { + context.Response.ContentType = "text/plain"; + + var tlsFingerprintingFeature = context.Features.Get(); + if (tlsFingerprintingFeature is null) + { + await context.Response.WriteAsync(nameof(ITlsFingerprintingFeature) + " is not resolved from " + nameof(context)); + return; + } + + var tlsClientHello = tlsFingerprintingFeature.GetTlsClientHello(); + await context.Response.WriteAsync("TLS CLIENT HELLO: " + tlsClientHello); + }); + } +} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj b/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj new file mode 100644 index 000000000000..f65f8a98a72a --- /dev/null +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj @@ -0,0 +1,14 @@ + + + + $(DefaultNetCoreTargetFramework) + Exe + true + + + + + + + + diff --git a/src/Servers/HttpSys/src/LoggerEventIds.cs b/src/Servers/HttpSys/src/LoggerEventIds.cs index 5bc0b6b65ed6..8f531936624a 100644 --- a/src/Servers/HttpSys/src/LoggerEventIds.cs +++ b/src/Servers/HttpSys/src/LoggerEventIds.cs @@ -59,4 +59,5 @@ internal static class LoggerEventIds public const int AcceptCancelExpectationMismatch = 52; public const int AcceptObserveExpectationMismatch = 53; public const int RequestParsingError = 54; + public const int TlsClientHelloParseError = 55; } diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index 575ebc259d25..51173e6a6ee6 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -126,7 +126,7 @@ internal static HTTP_API_VERSION ApiVersion internal static HttpSetRequestPropertyInvoker? HttpSetRequestProperty { get; private set; } [MemberNotNullWhen(true, nameof(HttpSetRequestProperty))] internal static bool SupportsTrailers { get; private set; } - [MemberNotNullWhen(true, nameof(HttpSetRequestProperty))] + [MemberNotNullWhen(true, nameof(HttpSetRequestInvoker))] internal static bool SupportsReset { get; private set; } internal static bool SupportsDelegation { get; private set; } @@ -147,9 +147,9 @@ private static void InitHttpApi(ushort majorVersion, ushort minorVersion) if (supported) { HttpApiModule = SafeLibraryHandle.Open(HTTPAPI); - HttpGetRequestProperty = HttpApiModule.GetProcAddress("HttpQueryRequestProperty", throwIfNotFound: false); - HttpSetRequestProperty = HttpApiModule.GetProcAddress("HttpSetRequestProperty", throwIfNotFound: false); - SupportsReset = HttpSetRequestProperty != null; + HttpGetRequestInvoker = HttpApiModule.GetProcAddress("HttpQueryRequestProperty", throwIfNotFound: false); + HttpSetRequestInvoker = HttpApiModule.GetProcAddress("HttpSetRequestProperty", throwIfNotFound: false); + SupportsReset = HttpSetRequestPropertySupported; SupportsTrailers = IsFeatureSupported(HTTP_FEATURE_ID.HttpFeatureResponseTrailers); SupportsDelegation = IsFeatureSupported(HTTP_FEATURE_ID.HttpFeatureDelegateEx); } diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 6029f8269f53..07e303e8832d 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -8,6 +8,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; +using Microsoft.AspNetCore.Connections.Abstractions.TLS; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; @@ -172,6 +173,7 @@ internal Request(RequestContext requestContext) if (IsHttps) { GetTlsHandshakeResults(); + ParseTlsClientHello(); } // GetTlsTokenBindingInfo(); TODO: https://github.com/aspnet/HttpSysServer/issues/231 @@ -362,6 +364,11 @@ private void GetTlsHandshakeResults() SniHostName = sni.Hostname; } + private void ParseTlsClientHello() + { + TlsClientHelloMessage = RequestContext.TryGetTlsClientHello(); + } + public X509Certificate2? ClientCertificate { get diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs index e1931dc0fc6b..7e485ed417e7 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs @@ -8,6 +8,7 @@ using System.Security.Authentication; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Connections.Abstractions.TLS; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -24,6 +25,7 @@ internal partial class RequestContext : IHttpResponseBodyFeature, ITlsConnectionFeature, ITlsHandshakeFeature, + ITlsFingerprintingFeature, // ITlsTokenBindingFeature, TODO: https://github.com/aspnet/HttpSysServer/issues/231 IHttpRequestLifetimeFeature, IHttpAuthenticationFeature, @@ -382,6 +384,11 @@ string IHttpConnectionFeature.ConnectionId return Request.IsHttps ? this : null; } + internal ITlsFingerprintingFeature? GetTlsFingerprintingFeature() + { + return Request.IsHttps ? this : null; + } + internal IHttpResponseTrailersFeature? GetResponseTrailersFeature() { if (Request.ProtocolVersion >= HttpVersion.Version20 && HttpApi.SupportsTrailers) diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs index 41b1cf480d5a..d02a75dbf585 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs @@ -20,5 +20,11 @@ private static partial class Log [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to parse request.", EventName = "RequestParsingError")] public static partial void RequestParsingError(ILogger logger, Exception exception); + + [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello.", EventName = "TlsClientHelloRetrieveError")] + public static partial void TlsClientHelloRetrieveError(ILogger logger, string message); + + [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to parse TLS client hello message.", EventName = "TlsClientHelloParseError")] + public static partial void TlsClientHelloParseError(ILogger logger, Exception exception); } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index e9f277b6a990..b1c0743af9b7 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -1,10 +1,11 @@ // 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.Buffers; using System.Runtime.InteropServices; -using System.Security.Authentication.ExtendedProtection; +using System.Security.Authentication; using System.Security.Principal; +using Microsoft.AspNetCore.Connections.Abstractions.TLS; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; @@ -234,28 +235,119 @@ internal void ForceCancelRequest() } } - internal unsafe HttpApiTypes.HTTP_REQUEST_PROPERTY_SNI GetClientSni() + internal unsafe TLS_CLIENT_HELLO TryGetTlsClientHello() { - if (HttpApi.HttpGetRequestProperty != null) + if (!HttpApi.HttpGetRequestPropertySupported) + { + return default; + } + + uint bytesReturnedValue = 0; + uint* bytesReturned = &bytesReturnedValue; + + var buffer = new byte[256]; + try { - var buffer = new byte[HttpApiTypes.SniPropertySizeInBytes]; fixed (byte* pBuffer = buffer) { var statusCode = HttpApi.HttpGetRequestProperty( Server.RequestQueue.Handle, RequestId, - HttpApiTypes.HTTP_REQUEST_PROPERTY.HttpRequestPropertySni, + 11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, qualifier: null, qualifierSize: 0, - (void*)pBuffer, + pBuffer, (uint)buffer.Length, - bytesReturned: null, + bytesReturned: (IntPtr)bytesReturned, IntPtr.Zero); - if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + if (statusCode is ErrorCodes.ERROR_MORE_DATA or ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { - return Marshal.PtrToStructure((IntPtr)pBuffer); + // The buffer is too small, we need to allocate a larger one. + // Firstly, return the initial buffer to not leak + // ArrayPool.Shared.Return(buffer); + + // then reallocate the buffer of size as `bytesReturned` + buffer = new byte[(int)bytesReturned]; + + // and try again! + statusCode = HttpApi.HttpGetRequestProperty( + Server.RequestQueue.Handle, + RequestId, + 11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, + qualifier: null, + qualifierSize: 0, + pBuffer, + (uint)buffer.Length, + bytesReturned: (IntPtr)bytesReturned, + IntPtr.Zero); } + + if (statusCode == ErrorCodes.ERROR_SUCCESS) + { + try + { + var tlsMajorVersion = pBuffer[1]; + var tlsMinorVersion = pBuffer[2]; + + return new TLS_CLIENT_HELLO + { + ProtocolVersion = tlsMinorVersion switch + { + 4 => SslProtocols.Tls13, + 3 => SslProtocols.Tls12, +#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete + 2 => SslProtocols.Tls11, + 1 => SslProtocols.Tls, +#pragma warning restore SYSLIB0039 +#pragma warning disable CS0618 // Type or member is obsolete + 0 => SslProtocols.Ssl3, +#pragma warning restore CS0618 // Type or member is obsolete + _ => SslProtocols.None, + } + }; + } + catch (Exception ex) + { + Log.TlsClientHelloParseError(Logger, ex); + } + } + + // if not returned here, we got a non-success status code + Log.TlsClientHelloRetrieveError(Logger, "Status code: " + statusCode); + return default; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + internal unsafe HTTP_REQUEST_PROPERTY_SNI GetClientSni() + { + if (!HttpApi.HttpGetRequestPropertySupported) + { + return default; + } + + var buffer = new byte[HttpApiTypes.SniPropertySizeInBytes]; + fixed (byte* pBuffer = buffer) + { + var statusCode = HttpApi.HttpGetRequestProperty( + Server.RequestQueue.Handle, + RequestId, + HTTP_REQUEST_PROPERTY.HttpRequestPropertySni, + qualifier: null, + qualifierSize: 0, + pBuffer, + (uint)buffer.Length, + bytesReturned: IntPtr.Zero, + IntPtr.Zero); + + if (statusCode == ErrorCodes.ERROR_SUCCESS) + { + return Marshal.PtrToStructure((IntPtr)pBuffer); } } diff --git a/src/Servers/HttpSys/src/StandardFeatureCollection.cs b/src/Servers/HttpSys/src/StandardFeatureCollection.cs index dda57166921e..64724daeb286 100644 --- a/src/Servers/HttpSys/src/StandardFeatureCollection.cs +++ b/src/Servers/HttpSys/src/StandardFeatureCollection.cs @@ -19,6 +19,7 @@ internal sealed class StandardFeatureCollection : IFeatureCollection { typeof(IHttpResponseFeature), _identityFunc }, { typeof(IHttpResponseBodyFeature), _identityFunc }, { typeof(ITlsConnectionFeature), ctx => ctx.GetTlsConnectionFeature() }, + { typeof(ITlsFingerprintingFeature), ctx => ctx.GetTlsFingerprintingFeature() }, { typeof(IHttpRequestLifetimeFeature), _identityFunc }, { typeof(IHttpAuthenticationFeature), _identityFunc }, { typeof(IHttpRequestIdentifierFeature), _identityFunc }, From a376c94bf7e25d649608750879604ab8693859e7 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 7 Mar 2025 13:16:51 +0100 Subject: [PATCH 02/24] correctly retry access --- ...rintingFeature.cs => ITlsAccessFeature.cs} | 8 +- .../samples/TlsFeaturesObserve/Startup.cs | 8 +- .../HttpSys/src/RequestProcessing/Request.cs | 2 +- .../RequestContext.FeatureCollection.cs | 5 +- .../RequestProcessing/RequestContext.Log.cs | 3 - .../src/RequestProcessing/RequestContext.cs | 109 ++++++++---------- .../HttpSys/src/StandardFeatureCollection.cs | 2 +- 7 files changed, 57 insertions(+), 80 deletions(-) rename src/Servers/Connections.Abstractions/src/Features/{ITlsFingerprintingFeature.cs => ITlsAccessFeature.cs} (72%) diff --git a/src/Servers/Connections.Abstractions/src/Features/ITlsFingerprintingFeature.cs b/src/Servers/Connections.Abstractions/src/Features/ITlsAccessFeature.cs similarity index 72% rename from src/Servers/Connections.Abstractions/src/Features/ITlsFingerprintingFeature.cs rename to src/Servers/Connections.Abstractions/src/Features/ITlsAccessFeature.cs index e0f1e2bea4f7..226015e1d5a2 100644 --- a/src/Servers/Connections.Abstractions/src/Features/ITlsFingerprintingFeature.cs +++ b/src/Servers/Connections.Abstractions/src/Features/ITlsAccessFeature.cs @@ -13,12 +13,12 @@ namespace Microsoft.AspNetCore.Connections.Features; /// -/// Represents the details about the TLS fingerprinting. +/// Allows to access underlying TLS data. /// -public interface ITlsFingerprintingFeature +public interface ITlsAccessFeature { /// - /// Returns the TLS client hello details, if any. + /// Returns the raw bytes of TLS client hello message. /// - TLS_CLIENT_HELLO GetTlsClientHello(); + byte[]? GetTlsClientHelloMessageBytes(); } diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs index 28d002567c7a..ea4154d4cb2e 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs @@ -21,15 +21,15 @@ public void Configure(IApplicationBuilder app) { context.Response.ContentType = "text/plain"; - var tlsFingerprintingFeature = context.Features.Get(); + var tlsFingerprintingFeature = context.Features.Get(); if (tlsFingerprintingFeature is null) { - await context.Response.WriteAsync(nameof(ITlsFingerprintingFeature) + " is not resolved from " + nameof(context)); + await context.Response.WriteAsync(nameof(ITlsAccessFeature) + " is not resolved from " + nameof(context)); return; } - var tlsClientHello = tlsFingerprintingFeature.GetTlsClientHello(); - await context.Response.WriteAsync("TLS CLIENT HELLO: " + tlsClientHello); + var tlsClientHello = tlsFingerprintingFeature.GetTlsClientHelloMessageBytes(); + await context.Response.WriteAsync("TLS CLIENT HELLO: bytearray.len=" + tlsClientHello.Length); }); } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 07e303e8832d..b66d16b42346 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -366,7 +366,7 @@ private void GetTlsHandshakeResults() private void ParseTlsClientHello() { - TlsClientHelloMessage = RequestContext.TryGetTlsClientHello(); + TlsClientHelloMessageBytes = RequestContext.GetTlsClientHelloMessageBytes(); } public X509Certificate2? ClientCertificate diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs index 7e485ed417e7..f632d31e9eb8 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs @@ -8,7 +8,6 @@ using System.Security.Authentication; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; -using Microsoft.AspNetCore.Connections.Abstractions.TLS; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -25,7 +24,7 @@ internal partial class RequestContext : IHttpResponseBodyFeature, ITlsConnectionFeature, ITlsHandshakeFeature, - ITlsFingerprintingFeature, + ITlsAccessFeature, // ITlsTokenBindingFeature, TODO: https://github.com/aspnet/HttpSysServer/issues/231 IHttpRequestLifetimeFeature, IHttpAuthenticationFeature, @@ -384,7 +383,7 @@ string IHttpConnectionFeature.ConnectionId return Request.IsHttps ? this : null; } - internal ITlsFingerprintingFeature? GetTlsFingerprintingFeature() + internal ITlsAccessFeature? GetTlsFingerprintingFeature() { return Request.IsHttps ? this : null; } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs index d02a75dbf585..6def3e52483b 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs @@ -23,8 +23,5 @@ private static partial class Log [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello.", EventName = "TlsClientHelloRetrieveError")] public static partial void TlsClientHelloRetrieveError(ILogger logger, string message); - - [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to parse TLS client hello message.", EventName = "TlsClientHelloParseError")] - public static partial void TlsClientHelloParseError(ILogger logger, Exception exception); } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index b1c0743af9b7..65e1aa0917b5 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -3,9 +3,7 @@ using System.Buffers; using System.Runtime.InteropServices; -using System.Security.Authentication; using System.Security.Principal; -using Microsoft.AspNetCore.Connections.Abstractions.TLS; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; @@ -235,7 +233,11 @@ internal void ForceCancelRequest() } } - internal unsafe TLS_CLIENT_HELLO TryGetTlsClientHello() + /// + /// Attempts to get the client hello message bytes from the http.sys. + /// If not successful, will return `null` + /// + internal unsafe byte[]? GetTlsClientHelloMessageBytes() { if (!HttpApi.HttpGetRequestPropertySupported) { @@ -244,84 +246,63 @@ internal unsafe TLS_CLIENT_HELLO TryGetTlsClientHello() uint bytesReturnedValue = 0; uint* bytesReturned = &bytesReturnedValue; + uint statusCode; - var buffer = new byte[256]; + var buffer = ArrayPool.Shared.Rent(256); try { fixed (byte* pBuffer = buffer) { - var statusCode = HttpApi.HttpGetRequestProperty( - Server.RequestQueue.Handle, - RequestId, + statusCode = HttpApi.HttpGetRequestProperty( + Server.RequestQueue.Handle, RequestId, 11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, - qualifier: null, - qualifierSize: 0, - pBuffer, - (uint)buffer.Length, - bytesReturned: (IntPtr)bytesReturned, - IntPtr.Zero); - - if (statusCode is ErrorCodes.ERROR_MORE_DATA or ErrorCodes.ERROR_INSUFFICIENT_BUFFER) + qualifier: null, qualifierSize: 0, + pBuffer, (uint)buffer.Length, + bytesReturned: (IntPtr)bytesReturned, IntPtr.Zero); + + if (statusCode is ErrorCodes.ERROR_SUCCESS) { - // The buffer is too small, we need to allocate a larger one. - // Firstly, return the initial buffer to not leak - // ArrayPool.Shared.Return(buffer); + return buffer.AsSpan().ToArray(); + } + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } - // then reallocate the buffer of size as `bytesReturned` - buffer = new byte[(int)bytesReturned]; + // if buffer supplied is too small, `bytesReturned` will have proper size + // so retry should succeed with the properly allocated buffer + if (statusCode is ErrorCodes.ERROR_MORE_DATA or ErrorCodes.ERROR_INSUFFICIENT_BUFFER) + { + try + { + var correctSize = (int)bytesReturnedValue; + buffer = ArrayPool.Shared.Rent(correctSize); - // and try again! + fixed (byte* pBuffer = buffer) + { statusCode = HttpApi.HttpGetRequestProperty( - Server.RequestQueue.Handle, - RequestId, + Server.RequestQueue.Handle, RequestId, 11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, - qualifier: null, - qualifierSize: 0, - pBuffer, - (uint)buffer.Length, - bytesReturned: (IntPtr)bytesReturned, - IntPtr.Zero); - } + qualifier: null, qualifierSize: 0, + pBuffer, (uint)buffer.Length, + bytesReturned: (IntPtr)bytesReturned, IntPtr.Zero); - if (statusCode == ErrorCodes.ERROR_SUCCESS) - { - try - { - var tlsMajorVersion = pBuffer[1]; - var tlsMinorVersion = pBuffer[2]; - - return new TLS_CLIENT_HELLO - { - ProtocolVersion = tlsMinorVersion switch - { - 4 => SslProtocols.Tls13, - 3 => SslProtocols.Tls12, -#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete - 2 => SslProtocols.Tls11, - 1 => SslProtocols.Tls, -#pragma warning restore SYSLIB0039 -#pragma warning disable CS0618 // Type or member is obsolete - 0 => SslProtocols.Ssl3, -#pragma warning restore CS0618 // Type or member is obsolete - _ => SslProtocols.None, - } - }; - } - catch (Exception ex) + if (statusCode is ErrorCodes.ERROR_SUCCESS) { - Log.TlsClientHelloParseError(Logger, ex); + return buffer.AsSpan(0, correctSize).ToArray(); } } - - // if not returned here, we got a non-success status code - Log.TlsClientHelloRetrieveError(Logger, "Status code: " + statusCode); - return default; + } + finally + { + ArrayPool.Shared.Return(buffer); } } - finally - { - ArrayPool.Shared.Return(buffer); - } + + Log.TlsClientHelloRetrieveError(Logger, "Status code: " + statusCode); + return default; } internal unsafe HTTP_REQUEST_PROPERTY_SNI GetClientSni() diff --git a/src/Servers/HttpSys/src/StandardFeatureCollection.cs b/src/Servers/HttpSys/src/StandardFeatureCollection.cs index 64724daeb286..88e4e3867378 100644 --- a/src/Servers/HttpSys/src/StandardFeatureCollection.cs +++ b/src/Servers/HttpSys/src/StandardFeatureCollection.cs @@ -19,7 +19,7 @@ internal sealed class StandardFeatureCollection : IFeatureCollection { typeof(IHttpResponseFeature), _identityFunc }, { typeof(IHttpResponseBodyFeature), _identityFunc }, { typeof(ITlsConnectionFeature), ctx => ctx.GetTlsConnectionFeature() }, - { typeof(ITlsFingerprintingFeature), ctx => ctx.GetTlsFingerprintingFeature() }, + { typeof(ITlsAccessFeature), ctx => ctx.GetTlsFingerprintingFeature() }, { typeof(IHttpRequestLifetimeFeature), _identityFunc }, { typeof(IHttpAuthenticationFeature), _identityFunc }, { typeof(IHttpRequestIdentifierFeature), _identityFunc }, From 9c5fc1768d0bc07d7b3fb5459fb80d35509bb11e Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 7 Mar 2025 13:30:06 +0100 Subject: [PATCH 03/24] last minute changes --- .../src/TLS/TlsClientHello.cs | 25 ------------------- src/Servers/HttpSys/src/LoggerEventIds.cs | 1 - 2 files changed, 26 deletions(-) delete mode 100644 src/Servers/Connections.Abstractions/src/TLS/TlsClientHello.cs diff --git a/src/Servers/Connections.Abstractions/src/TLS/TlsClientHello.cs b/src/Servers/Connections.Abstractions/src/TLS/TlsClientHello.cs deleted file mode 100644 index 56f695697bc4..000000000000 --- a/src/Servers/Connections.Abstractions/src/TLS/TlsClientHello.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Security.Authentication; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Connections.Abstractions.TLS; - -public struct TLS_CLIENT_HELLO -{ - public SslProtocols ProtocolVersion; // Version of the TLS protocol - - public override string ToString() - { - return $""" - TLS CLIENT HELLO MESSAGE: - - ProtocolVersion: {ProtocolVersion} - """; - } -} diff --git a/src/Servers/HttpSys/src/LoggerEventIds.cs b/src/Servers/HttpSys/src/LoggerEventIds.cs index 8f531936624a..5bc0b6b65ed6 100644 --- a/src/Servers/HttpSys/src/LoggerEventIds.cs +++ b/src/Servers/HttpSys/src/LoggerEventIds.cs @@ -59,5 +59,4 @@ internal static class LoggerEventIds public const int AcceptCancelExpectationMismatch = 52; public const int AcceptObserveExpectationMismatch = 53; public const int RequestParsingError = 54; - public const int TlsClientHelloParseError = 55; } From e8834a28d321a390c106b5a59464e55d63accbb7 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 7 Mar 2025 13:53:46 +0100 Subject: [PATCH 04/24] fix warnings --- .../src/Features/ITlsAccessFeature.cs | 1 - .../TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs | 6 ++++++ src/Servers/HttpSys/src/RequestProcessing/Request.cs | 1 - 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Servers/Connections.Abstractions/src/Features/ITlsAccessFeature.cs b/src/Servers/Connections.Abstractions/src/Features/ITlsAccessFeature.cs index 226015e1d5a2..12664443a01b 100644 --- a/src/Servers/Connections.Abstractions/src/Features/ITlsAccessFeature.cs +++ b/src/Servers/Connections.Abstractions/src/Features/ITlsAccessFeature.cs @@ -8,7 +8,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Connections.Abstractions.TLS; namespace Microsoft.AspNetCore.Connections.Features; diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs index 5da6fee79e3b..e5a81b955842 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs @@ -79,7 +79,10 @@ static byte[] GetHash(string thumbprint) int length = thumbprint.Length; byte[] bytes = new byte[length / 2]; for (int i = 0; i < length; i += 2) + { bytes[i / 2] = Convert.ToByte(thumbprint.Substring(i, 2), 16); + } + return bytes; } @@ -104,6 +107,8 @@ static void CallHttpApi(Action body) body(); } +// disabled warning since it is just a sample +#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time [DllImport("httpapi.dll", SetLastError = true)] private static extern uint HttpInitialize( HTTPAPI_VERSION version, @@ -117,4 +122,5 @@ public static extern uint HttpSetServiceConfiguration( nint pConfigInformation, int configInformationLength, nint pOverlapped); +#pragma warning restore SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time } diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index b66d16b42346..dff8e92222fb 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -8,7 +8,6 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; -using Microsoft.AspNetCore.Connections.Abstractions.TLS; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; From c953a21dd36f44ba75f98cf77ff572c4f74080ed Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 7 Apr 2025 13:04:46 +0200 Subject: [PATCH 05/24] hook up tls client hello callback --- .../src/Features/ITlsAccessFeature.cs | 23 ----------- .../samples/TlsFeaturesObserve/Program.cs | 32 ++++++++++++++++ .../samples/TlsFeaturesObserve/Startup.cs | 11 +----- src/Servers/HttpSys/src/HttpSysListener.cs | 24 +++++------- src/Servers/HttpSys/src/HttpSysOptions.cs | 6 +++ .../src/NativeInterop/DisconnectListener.cs | 23 +++++++++-- .../HttpSys/src/RequestProcessing/Request.cs | 8 ++-- .../RequestContext.FeatureCollection.cs | 6 --- .../src/RequestProcessing/RequestContext.cs | 25 +++++++----- .../RequestProcessing/RequestContextOfT.cs | 3 ++ .../src/RequestProcessing/TlsListener.cs | 38 +++++++++++++++++++ .../HttpSys/src/StandardFeatureCollection.cs | 1 - .../RequestProcessing/NativeRequestContext.cs | 10 ++++- 13 files changed, 137 insertions(+), 73 deletions(-) delete mode 100644 src/Servers/Connections.Abstractions/src/Features/ITlsAccessFeature.cs create mode 100644 src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs diff --git a/src/Servers/Connections.Abstractions/src/Features/ITlsAccessFeature.cs b/src/Servers/Connections.Abstractions/src/Features/ITlsAccessFeature.cs deleted file mode 100644 index 12664443a01b..000000000000 --- a/src/Servers/Connections.Abstractions/src/Features/ITlsAccessFeature.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Connections.Features; - -/// -/// Allows to access underlying TLS data. -/// -public interface ITlsAccessFeature -{ - /// - /// Returns the raw bytes of TLS client hello message. - /// - byte[]? GetTlsClientHelloMessageBytes(); -} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs index 742d013d02af..d6d132697549 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.HttpSys; using Microsoft.Extensions.Hosting; using TlsFeaturesObserve.HttpSys; @@ -30,6 +31,37 @@ public static IHostBuilder CreateHostBuilder(string[] args) => options.Authentication.Schemes = AuthenticationSchemes.None; options.Authentication.AllowAnonymous = true; + + options.TlsClientHelloBytesCallback = ProcessTlsClientHello; }); }); + + private static void ProcessTlsClientHello(IFeatureCollection features, ReadOnlySpan tlsClientHelloBytes) + { + var httpConnectionFeature = features.Get(); + + var myTlsFeature = new MyTlsFeature( + connectionId: httpConnectionFeature.ConnectionId, + tlsClientHelloLength: tlsClientHelloBytes.Length); + + features.Set(myTlsFeature); + } +} + +public interface IMyTlsFeature +{ + string ConnectionId { get; } + int TlsClientHelloLength { get; } +} + +public class MyTlsFeature : IMyTlsFeature +{ + public string ConnectionId { get; } + public int TlsClientHelloLength { get; } + + public MyTlsFeature(string connectionId, int tlsClientHelloLength) + { + ConnectionId = connectionId; + TlsClientHelloLength = tlsClientHelloLength; + } } diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs index ea4154d4cb2e..8ba6d27aef98 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs @@ -21,15 +21,8 @@ public void Configure(IApplicationBuilder app) { context.Response.ContentType = "text/plain"; - var tlsFingerprintingFeature = context.Features.Get(); - if (tlsFingerprintingFeature is null) - { - await context.Response.WriteAsync(nameof(ITlsAccessFeature) + " is not resolved from " + nameof(context)); - return; - } - - var tlsClientHello = tlsFingerprintingFeature.GetTlsClientHelloMessageBytes(); - await context.Response.WriteAsync("TLS CLIENT HELLO: bytearray.len=" + tlsClientHello.Length); + var tlsFeature = context.Features.Get(); + await context.Response.WriteAsync("TlsClientHello data: " + $"connectionId={tlsFeature?.ConnectionId}; length={tlsFeature?.TlsClientHelloLength}"); }); } } diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 2b7924491d32..04d6a07032da 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -5,6 +5,8 @@ using System.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; +using Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.HttpSys; @@ -37,6 +39,7 @@ internal sealed partial class HttpSysListener : IDisposable private readonly UrlGroup _urlGroup; private readonly RequestQueue _requestQueue; private readonly DisconnectListener _disconnectListener; + private readonly TlsListener _tlsListener; private readonly object _internalLock; @@ -74,7 +77,8 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) _urlGroup = new UrlGroup(_serverSession, _requestQueue, Logger); - _disconnectListener = new DisconnectListener(_requestQueue, Logger); + _tlsListener = new TlsListener(options); + _disconnectListener = new DisconnectListener(_requestQueue, Logger, onDisconnect: _tlsListener.ConnectionClosed); } catch (Exception exception) { @@ -96,20 +100,10 @@ internal enum State internal ILogger Logger { get; private set; } - internal UrlGroup UrlGroup - { - get { return _urlGroup; } - } - - internal RequestQueue RequestQueue - { - get { return _requestQueue; } - } - - internal DisconnectListener DisconnectListener - { - get { return _disconnectListener; } - } + internal UrlGroup UrlGroup => _urlGroup; + internal RequestQueue RequestQueue => _requestQueue; + internal TlsListener TlsListener => _tlsListener; + internal DisconnectListener DisconnectListener => _disconnectListener; public HttpSysOptions Options { get; } diff --git a/src/Servers/HttpSys/src/HttpSysOptions.cs b/src/Servers/HttpSys/src/HttpSysOptions.cs index 87fb1ba6d176..7d89ba575e43 100644 --- a/src/Servers/HttpSys/src/HttpSysOptions.cs +++ b/src/Servers/HttpSys/src/HttpSysOptions.cs @@ -246,6 +246,12 @@ public Http503VerbosityLevel Http503Verbosity /// public bool UseLatin1RequestHeaders { get; set; } + /// + /// A callback to be invoked to get the TLS client hello bytes. + /// By default is null, and will not be invoked if is null. + /// + public Action> TlsClientHelloBytesCallback { get; set; } = null!; + // Not called when attaching to an existing queue. internal void Apply(UrlGroup urlGroup, RequestQueue? requestQueue) { diff --git a/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs b/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs index 26f95d13674d..e7bc40717835 100644 --- a/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs +++ b/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs @@ -15,10 +15,14 @@ internal sealed partial class DisconnectListener private readonly RequestQueue _requestQueue; private readonly ILogger _logger; - internal DisconnectListener(RequestQueue requestQueue, ILogger logger) + internal Action OnDisconnect { get; } + + internal DisconnectListener(RequestQueue requestQueue, ILogger logger, Action onDisconnect) { _requestQueue = requestQueue; _logger = logger; + + OnDisconnect = onDisconnect; } internal CancellationToken GetTokenForConnection(ulong connectionId) @@ -70,7 +74,7 @@ private unsafe CancellationToken CreateDisconnectToken(ulong connectionId) boundHandle.FreeNativeOverlapped(pOverlapped); // Pull the token out of the list and Cancel it. - _connectionCancellationTokens.TryRemove(connectionId, out _); + TryRemoveConnectionCancellationToken(connectionId, out _); try { cts.Cancel(); @@ -100,7 +104,7 @@ private unsafe CancellationToken CreateDisconnectToken(ulong connectionId) { // We got an unknown result, assume the connection has been closed. boundHandle.FreeNativeOverlapped(nativeOverlapped); - _connectionCancellationTokens.TryRemove(connectionId, out _); + TryRemoveConnectionCancellationToken(connectionId, out _); Log.UnknownDisconnectError(_logger, new Win32Exception((int)statusCode)); cts.Cancel(); } @@ -109,13 +113,24 @@ private unsafe CancellationToken CreateDisconnectToken(ulong connectionId) { // IO operation completed synchronously - callback won't be called to signal completion boundHandle.FreeNativeOverlapped(nativeOverlapped); - _connectionCancellationTokens.TryRemove(connectionId, out _); + TryRemoveConnectionCancellationToken(connectionId, out _); cts.Cancel(); } return returnToken; } + private bool TryRemoveConnectionCancellationToken(ulong connectionId, out ConnectionCancellation connectionCancellation) + { + bool result; + if (result = _connectionCancellationTokens.TryRemove(connectionId, out connectionCancellation!)) + { + OnDisconnect(connectionId); + } + + return result; + } + private sealed class ConnectionCancellation { private readonly DisconnectListener _parent; diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index dff8e92222fb..adc948e2a471 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -9,6 +9,7 @@ using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; @@ -172,7 +173,6 @@ internal Request(RequestContext requestContext) if (IsHttps) { GetTlsHandshakeResults(); - ParseTlsClientHello(); } // GetTlsTokenBindingInfo(); TODO: https://github.com/aspnet/HttpSysServer/issues/231 @@ -363,10 +363,8 @@ private void GetTlsHandshakeResults() SniHostName = sni.Hostname; } - private void ParseTlsClientHello() - { - TlsClientHelloMessageBytes = RequestContext.GetTlsClientHelloMessageBytes(); - } + internal bool GetAndInvokeTlsClientHelloCallback(IFeatureCollection features, Action> tlsClientHelloBytesCallback) + => RequestContext.GetAndInvokeTlsClientHelloMessageBytesCallback(features, tlsClientHelloBytesCallback); public X509Certificate2? ClientCertificate { diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs index f632d31e9eb8..e1931dc0fc6b 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs @@ -24,7 +24,6 @@ internal partial class RequestContext : IHttpResponseBodyFeature, ITlsConnectionFeature, ITlsHandshakeFeature, - ITlsAccessFeature, // ITlsTokenBindingFeature, TODO: https://github.com/aspnet/HttpSysServer/issues/231 IHttpRequestLifetimeFeature, IHttpAuthenticationFeature, @@ -383,11 +382,6 @@ string IHttpConnectionFeature.ConnectionId return Request.IsHttps ? this : null; } - internal ITlsAccessFeature? GetTlsFingerprintingFeature() - { - return Request.IsHttps ? this : null; - } - internal IHttpResponseTrailersFeature? GetResponseTrailersFeature() { if (Request.ProtocolVersion >= HttpVersion.Version20 && HttpApi.SupportsTrailers) diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index 65e1aa0917b5..05429ba15953 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using System.Security.Principal; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; @@ -235,26 +236,30 @@ internal void ForceCancelRequest() /// /// Attempts to get the client hello message bytes from the http.sys. - /// If not successful, will return `null` + /// If not successful, will return false. /// - internal unsafe byte[]? GetTlsClientHelloMessageBytes() + internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureCollection features, Action> tlsClientHelloBytesCallback) { if (!HttpApi.HttpGetRequestPropertySupported) { - return default; + // not supported, so we just return and don't invoke the callback + return false; } uint bytesReturnedValue = 0; uint* bytesReturned = &bytesReturnedValue; uint statusCode; - var buffer = ArrayPool.Shared.Rent(256); + var requestId = PinsReleased ? Request.RequestId : RequestId; + + // we will try with some "random" buffer size + var buffer = ArrayPool.Shared.Rent(512); try { fixed (byte* pBuffer = buffer) { statusCode = HttpApi.HttpGetRequestProperty( - Server.RequestQueue.Handle, RequestId, + Server.RequestQueue.Handle, requestId, 11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, qualifier: null, qualifierSize: 0, pBuffer, (uint)buffer.Length, @@ -262,7 +267,8 @@ internal void ForceCancelRequest() if (statusCode is ErrorCodes.ERROR_SUCCESS) { - return buffer.AsSpan().ToArray(); + tlsClientHelloBytesCallback(features, buffer.AsSpan(0, (int)bytesReturnedValue)); + return true; } } } @@ -283,7 +289,7 @@ internal void ForceCancelRequest() fixed (byte* pBuffer = buffer) { statusCode = HttpApi.HttpGetRequestProperty( - Server.RequestQueue.Handle, RequestId, + Server.RequestQueue.Handle, requestId, 11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, qualifier: null, qualifierSize: 0, pBuffer, (uint)buffer.Length, @@ -291,7 +297,8 @@ internal void ForceCancelRequest() if (statusCode is ErrorCodes.ERROR_SUCCESS) { - return buffer.AsSpan(0, correctSize).ToArray(); + tlsClientHelloBytesCallback(features, buffer.AsSpan(0, correctSize)); + return true; } } } @@ -302,7 +309,7 @@ internal void ForceCancelRequest() } Log.TlsClientHelloRetrieveError(Logger, "Status code: " + statusCode); - return default; + return false; } internal unsafe HTTP_REQUEST_PROPERTY_SNI GetClientSni() diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs index 2a1d06a06d26..283eb7b199de 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs @@ -48,6 +48,9 @@ public override async Task ExecuteAsync() context = application.CreateContext(Features); try { + _ = DisconnectToken; // force disconnect to be followed. Required for TlsListener to keep cache about only active connections + Server.TlsListener.InvokeTlsClientHelloCallback(Features, Request); + await application.ProcessRequestAsync(context); await CompleteAsync(); } diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs new file mode 100644 index 000000000000..993c7f83134b --- /dev/null +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; + +internal sealed class TlsListener +{ + private readonly HttpSysOptions _httpSysOptions; + private readonly ConcurrentDictionary _connectionIdsTlsClientHelloCallbackInvokedMap = new(); + + internal TlsListener(HttpSysOptions httpSysOptions) + { + _httpSysOptions = httpSysOptions; + } + + internal void InvokeTlsClientHelloCallback(IFeatureCollection features, Request request) + { + if (_connectionIdsTlsClientHelloCallbackInvokedMap.TryGetValue(request.UConnectionId, out _)) + { + // invoking TLS client hello callback per request on same connection is what we are trying to avoid + return; + } + + var success = request.GetAndInvokeTlsClientHelloCallback(features, _httpSysOptions.TlsClientHelloBytesCallback); + if (success) + { + _connectionIdsTlsClientHelloCallbackInvokedMap[request.UConnectionId] = true; + } + } + + internal void ConnectionClosed(ulong connectionId) + { + _connectionIdsTlsClientHelloCallbackInvokedMap.TryRemove(connectionId, out _); + } +} diff --git a/src/Servers/HttpSys/src/StandardFeatureCollection.cs b/src/Servers/HttpSys/src/StandardFeatureCollection.cs index 88e4e3867378..dda57166921e 100644 --- a/src/Servers/HttpSys/src/StandardFeatureCollection.cs +++ b/src/Servers/HttpSys/src/StandardFeatureCollection.cs @@ -19,7 +19,6 @@ internal sealed class StandardFeatureCollection : IFeatureCollection { typeof(IHttpResponseFeature), _identityFunc }, { typeof(IHttpResponseBodyFeature), _identityFunc }, { typeof(ITlsConnectionFeature), ctx => ctx.GetTlsConnectionFeature() }, - { typeof(ITlsAccessFeature), ctx => ctx.GetTlsFingerprintingFeature() }, { typeof(IHttpRequestLifetimeFeature), _identityFunc }, { typeof(IHttpAuthenticationFeature), _identityFunc }, { typeof(IHttpRequestIdentifierFeature), _identityFunc }, diff --git a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs index b58bad9e800e..ad4f63eaacd7 100644 --- a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs +++ b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs @@ -31,9 +31,11 @@ internal unsafe class NativeRequestContext : IDisposable private MemoryHandle _memoryHandle; private readonly int _bufferAlignment; private readonly bool _permanentlyPinned; - private bool _disposed; private IReadOnlyDictionary>? _requestInfo; + private bool _disposed; + private bool _pinsReleased; + [MemberNotNullWhen(false, nameof(_backingBuffer))] private bool PermanentlyPinned => _permanentlyPinned; @@ -168,6 +170,11 @@ internal uint Size } } + /// + /// Shows whether was already invoked on this native request context + /// + internal bool PinsReleased => _pinsReleased; + // ReleasePins() should be called exactly once. It must be called before Dispose() is called, which means it must be called // before an object (Request) which closes the RequestContext on demand is returned to the application. internal void ReleasePins() @@ -177,6 +184,7 @@ internal void ReleasePins() _memoryHandle.Dispose(); _memoryHandle = default; _nativeRequest = null; + _pinsReleased = true; } public bool TryGetTimestamp(HttpSysRequestTimingType timestampType, out long timestamp) From 6c5239de3a6fc319ad371b7b924fa30b970eca06 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 7 Apr 2025 13:05:54 +0200 Subject: [PATCH 06/24] fix warnings & publish API --- src/Servers/HttpSys/src/PublicAPI.Unshipped.txt | 2 ++ src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt b/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..b8378c6c870d 100644 --- a/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt +++ b/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.get -> System.Action>! +Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.set -> void diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs index 6def3e52483b..a8799dc33008 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs @@ -21,7 +21,7 @@ private static partial class Log [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to parse request.", EventName = "RequestParsingError")] public static partial void RequestParsingError(ILogger logger, Exception exception); - [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello.", EventName = "TlsClientHelloRetrieveError")] + [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello: {message}", EventName = "TlsClientHelloRetrieveError")] public static partial void TlsClientHelloRetrieveError(ILogger logger, string message); } } From ff9518cc01841a48e44aa1f64da79355d61196a4 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 7 Apr 2025 13:12:56 +0200 Subject: [PATCH 07/24] minimal --- .../samples/TlsFeaturesObserve/Program.cs | 51 ++++++++----------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs index d6d132697549..3742211a9a8d 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs @@ -7,45 +7,38 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.HttpSys; using Microsoft.Extensions.Hosting; +using TlsFeatureObserve; using TlsFeaturesObserve.HttpSys; -namespace TlsFeatureObserve; +HttpSysConfigurator.ConfigureCacheTlsClientHello(); +CreateHostBuilder(args).Build().Run(); -public static class Program -{ - public static void Main(string[] args) - { - HttpSysConfigurator.ConfigureCacheTlsClientHello(); - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHost(webBuilder => +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHost(webBuilder => + { + webBuilder.UseStartup() + .UseHttpSys(options => { - webBuilder.UseStartup() - .UseHttpSys(options => - { - // If you want to use https locally: https://stackoverflow.com/a/51841893 - options.UrlPrefixes.Add("https://*:6000"); // HTTPS + // If you want to use https locally: https://stackoverflow.com/a/51841893 + options.UrlPrefixes.Add("https://*:6000"); // HTTPS - options.Authentication.Schemes = AuthenticationSchemes.None; - options.Authentication.AllowAnonymous = true; + options.Authentication.Schemes = AuthenticationSchemes.None; + options.Authentication.AllowAnonymous = true; - options.TlsClientHelloBytesCallback = ProcessTlsClientHello; - }); + options.TlsClientHelloBytesCallback = ProcessTlsClientHello; }); + }); - private static void ProcessTlsClientHello(IFeatureCollection features, ReadOnlySpan tlsClientHelloBytes) - { - var httpConnectionFeature = features.Get(); +static void ProcessTlsClientHello(IFeatureCollection features, ReadOnlySpan tlsClientHelloBytes) +{ + var httpConnectionFeature = features.Get(); - var myTlsFeature = new MyTlsFeature( - connectionId: httpConnectionFeature.ConnectionId, - tlsClientHelloLength: tlsClientHelloBytes.Length); + var myTlsFeature = new MyTlsFeature( + connectionId: httpConnectionFeature.ConnectionId, + tlsClientHelloLength: tlsClientHelloBytes.Length); - features.Set(myTlsFeature); - } + features.Set(myTlsFeature); } public interface IMyTlsFeature From de70b16632c244d902ff5beeac651170cea8963a Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 7 Apr 2025 13:26:32 +0200 Subject: [PATCH 08/24] only go via callback if options has callback set; remove unused --- src/Servers/HttpSys/src/HttpSysListener.cs | 2 +- .../HttpSys/src/RequestProcessing/RequestContextOfT.cs | 7 +++++-- src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs | 9 +++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 04d6a07032da..e835c8c10ff2 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -77,7 +77,7 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) _urlGroup = new UrlGroup(_serverSession, _requestQueue, Logger); - _tlsListener = new TlsListener(options); + _tlsListener = new TlsListener(options.TlsClientHelloBytesCallback); _disconnectListener = new DisconnectListener(_requestQueue, Logger, onDisconnect: _tlsListener.ConnectionClosed); } catch (Exception exception) diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs index 283eb7b199de..c9a4f02b79e5 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs @@ -48,8 +48,11 @@ public override async Task ExecuteAsync() context = application.CreateContext(Features); try { - _ = DisconnectToken; // force disconnect to be followed. Required for TlsListener to keep cache about only active connections - Server.TlsListener.InvokeTlsClientHelloCallback(Features, Request); + if (Server.Options.TlsClientHelloBytesCallback is not null) + { + _ = DisconnectToken; // force disconnect to be followed. Required for TlsListener to keep cache about only active connections + Server.TlsListener.InvokeTlsClientHelloCallback(Features, Request); + } await application.ProcessRequestAsync(context); await CompleteAsync(); diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs index 993c7f83134b..3926d6656926 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs @@ -8,12 +8,13 @@ namespace Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; internal sealed class TlsListener { - private readonly HttpSysOptions _httpSysOptions; private readonly ConcurrentDictionary _connectionIdsTlsClientHelloCallbackInvokedMap = new(); - internal TlsListener(HttpSysOptions httpSysOptions) + private readonly Action> _tlsClientHelloBytesCallback; + + internal TlsListener(Action> tlsClientHelloBytesCallback) { - _httpSysOptions = httpSysOptions; + _tlsClientHelloBytesCallback = tlsClientHelloBytesCallback; } internal void InvokeTlsClientHelloCallback(IFeatureCollection features, Request request) @@ -24,7 +25,7 @@ internal void InvokeTlsClientHelloCallback(IFeatureCollection features, Request return; } - var success = request.GetAndInvokeTlsClientHelloCallback(features, _httpSysOptions.TlsClientHelloBytesCallback); + var success = request.GetAndInvokeTlsClientHelloCallback(features, _tlsClientHelloBytesCallback); if (success) { _connectionIdsTlsClientHelloCallbackInvokedMap[request.UConnectionId] = true; From 1441e30c3f63a5e2486b60087b88e047ccfbbd4c Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 7 Apr 2025 19:11:48 +0200 Subject: [PATCH 09/24] PR review --- src/Servers/HttpSys/src/HttpSysListener.cs | 18 ++++++++++++------ src/Servers/HttpSys/src/HttpSysOptions.cs | 9 +++++++-- .../src/NativeInterop/DisconnectListener.cs | 6 +++--- .../HttpSys/src/PublicAPI.Unshipped.txt | 2 +- .../src/RequestProcessing/RequestContext.cs | 4 ++-- .../src/RequestProcessing/RequestContextOfT.cs | 2 +- 6 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index e835c8c10ff2..a8fa0e7c57eb 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -39,7 +39,7 @@ internal sealed partial class HttpSysListener : IDisposable private readonly UrlGroup _urlGroup; private readonly RequestQueue _requestQueue; private readonly DisconnectListener _disconnectListener; - private readonly TlsListener _tlsListener; + private readonly TlsListener? _tlsListener; private readonly object _internalLock; @@ -72,13 +72,19 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) try { _serverSession = new ServerSession(); - _requestQueue = new RequestQueue(options.RequestQueueName, options.RequestQueueMode, Logger); - _urlGroup = new UrlGroup(_serverSession, _requestQueue, Logger); - _tlsListener = new TlsListener(options.TlsClientHelloBytesCallback); - _disconnectListener = new DisconnectListener(_requestQueue, Logger, onDisconnect: _tlsListener.ConnectionClosed); + if (options.TlsClientHelloBytesCallback is not null) + { + _tlsListener = new TlsListener(options.TlsClientHelloBytesCallback); + _disconnectListener = new DisconnectListener(_requestQueue, Logger, onDisconnect: _tlsListener.ConnectionClosed); + } + else + { + _tlsListener = null; + _disconnectListener = new DisconnectListener(_requestQueue, Logger); + } } catch (Exception exception) { @@ -102,7 +108,7 @@ internal enum State internal UrlGroup UrlGroup => _urlGroup; internal RequestQueue RequestQueue => _requestQueue; - internal TlsListener TlsListener => _tlsListener; + internal TlsListener? TlsListener => _tlsListener; internal DisconnectListener DisconnectListener => _disconnectListener; public HttpSysOptions Options { get; } diff --git a/src/Servers/HttpSys/src/HttpSysOptions.cs b/src/Servers/HttpSys/src/HttpSysOptions.cs index 7d89ba575e43..eb13360db4fc 100644 --- a/src/Servers/HttpSys/src/HttpSysOptions.cs +++ b/src/Servers/HttpSys/src/HttpSysOptions.cs @@ -242,7 +242,7 @@ public Http503VerbosityLevel Http503Verbosity /// Configures request headers to use encoding. /// /// - /// Defaults to `false`, in which case will be used. />. + /// Defaults to false, in which case will be used. />. /// public bool UseLatin1RequestHeaders { get; set; } @@ -250,7 +250,12 @@ public Http503VerbosityLevel Http503Verbosity /// A callback to be invoked to get the TLS client hello bytes. /// By default is null, and will not be invoked if is null. /// - public Action> TlsClientHelloBytesCallback { get; set; } = null!; + /// + /// Works only if HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO flag is set on http.sys service configuration. + /// See + /// and + /// + public Action>? TlsClientHelloBytesCallback { get; set; } // Not called when attaching to an existing queue. internal void Apply(UrlGroup urlGroup, RequestQueue? requestQueue) diff --git a/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs b/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs index e7bc40717835..24836af8afec 100644 --- a/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs +++ b/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs @@ -15,9 +15,9 @@ internal sealed partial class DisconnectListener private readonly RequestQueue _requestQueue; private readonly ILogger _logger; - internal Action OnDisconnect { get; } + internal Action? OnDisconnect { get; } - internal DisconnectListener(RequestQueue requestQueue, ILogger logger, Action onDisconnect) + internal DisconnectListener(RequestQueue requestQueue, ILogger logger, Action? onDisconnect = null) { _requestQueue = requestQueue; _logger = logger; @@ -125,7 +125,7 @@ private bool TryRemoveConnectionCancellationToken(ulong connectionId, out Connec bool result; if (result = _connectionCancellationTokens.TryRemove(connectionId, out connectionCancellation!)) { - OnDisconnect(connectionId); + OnDisconnect?.Invoke(connectionId); } return result; diff --git a/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt b/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt index b8378c6c870d..e18d576e45d3 100644 --- a/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt +++ b/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt @@ -1,3 +1,3 @@ #nullable enable -Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.get -> System.Action>! +Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.get -> System.Action>? Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.set -> void diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index 05429ba15953..f07003b80b29 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -260,7 +260,7 @@ internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureColl { statusCode = HttpApi.HttpGetRequestProperty( Server.RequestQueue.Handle, requestId, - 11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, + (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, qualifier: null, qualifierSize: 0, pBuffer, (uint)buffer.Length, bytesReturned: (IntPtr)bytesReturned, IntPtr.Zero); @@ -290,7 +290,7 @@ internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureColl { statusCode = HttpApi.HttpGetRequestProperty( Server.RequestQueue.Handle, requestId, - 11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, + (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, qualifier: null, qualifierSize: 0, pBuffer, (uint)buffer.Length, bytesReturned: (IntPtr)bytesReturned, IntPtr.Zero); diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs index c9a4f02b79e5..5b517b18ee58 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs @@ -48,7 +48,7 @@ public override async Task ExecuteAsync() context = application.CreateContext(Features); try { - if (Server.Options.TlsClientHelloBytesCallback is not null) + if (Server.Options.TlsClientHelloBytesCallback is not null && Server.TlsListener is not null) { _ = DisconnectToken; // force disconnect to be followed. Required for TlsListener to keep cache about only active connections Server.TlsListener.InvokeTlsClientHelloCallback(Features, Request); From 56704efd0b7a8e43f17a85858cd80a931f930562 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Tue, 8 Apr 2025 13:47:48 +0200 Subject: [PATCH 10/24] address PR comments x1 --- src/Servers/HttpSys/src/HttpSysOptions.cs | 2 +- .../RequestProcessing/RequestContext.Log.cs | 4 +-- .../src/RequestProcessing/RequestContext.cs | 30 ++++++++++++------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/Servers/HttpSys/src/HttpSysOptions.cs b/src/Servers/HttpSys/src/HttpSysOptions.cs index eb13360db4fc..7ab8de6b8821 100644 --- a/src/Servers/HttpSys/src/HttpSysOptions.cs +++ b/src/Servers/HttpSys/src/HttpSysOptions.cs @@ -248,7 +248,7 @@ public Http503VerbosityLevel Http503Verbosity /// /// A callback to be invoked to get the TLS client hello bytes. - /// By default is null, and will not be invoked if is null. + /// Null by default. /// /// /// Works only if HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO flag is set on http.sys service configuration. diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs index a8799dc33008..3bf35e85854c 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs @@ -21,7 +21,7 @@ private static partial class Log [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to parse request.", EventName = "RequestParsingError")] public static partial void RequestParsingError(ILogger logger, Exception exception); - [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello: {message}", EventName = "TlsClientHelloRetrieveError")] - public static partial void TlsClientHelloRetrieveError(ILogger logger, string message); + [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello: Win32 Error code: {Win32Error}", EventName = "TlsClientHelloRetrieveError")] + public static partial void TlsClientHelloRetrieveError(ILogger logger, uint win32Error); } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index f07003b80b29..c3bb5d55e67d 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -259,11 +259,15 @@ internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureColl fixed (byte* pBuffer = buffer) { statusCode = HttpApi.HttpGetRequestProperty( - Server.RequestQueue.Handle, requestId, - (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, - qualifier: null, qualifierSize: 0, - pBuffer, (uint)buffer.Length, - bytesReturned: (IntPtr)bytesReturned, IntPtr.Zero); + requestQueueHandle: Server.RequestQueue.Handle, + requestId, + propertyId: (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, + qualifier: null, + qualifierSize: 0, + output: pBuffer, + outputSize: (uint)buffer.Length, + bytesReturned: (IntPtr)bytesReturned, + overlapped: IntPtr.Zero); if (statusCode is ErrorCodes.ERROR_SUCCESS) { @@ -289,11 +293,15 @@ internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureColl fixed (byte* pBuffer = buffer) { statusCode = HttpApi.HttpGetRequestProperty( - Server.RequestQueue.Handle, requestId, - (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, - qualifier: null, qualifierSize: 0, - pBuffer, (uint)buffer.Length, - bytesReturned: (IntPtr)bytesReturned, IntPtr.Zero); + requestQueueHandle: Server.RequestQueue.Handle, + requestId, + propertyId: (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, + qualifier: null, + qualifierSize: 0, + output: pBuffer, + outputSize: (uint)buffer.Length, + bytesReturned: (IntPtr)bytesReturned, + overlapped: IntPtr.Zero); if (statusCode is ErrorCodes.ERROR_SUCCESS) { @@ -308,7 +316,7 @@ internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureColl } } - Log.TlsClientHelloRetrieveError(Logger, "Status code: " + statusCode); + Log.TlsClientHelloRetrieveError(Logger, statusCode); return false; } From d43b424c8f18f93d0b1a3fd066b7365b025d3e2c Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Tue, 8 Apr 2025 16:18:58 +0200 Subject: [PATCH 11/24] TTL & evict approach --- src/Servers/HttpSys/src/HttpSysListener.cs | 11 +-- src/Servers/HttpSys/src/LoggerEventIds.cs | 1 + .../src/NativeInterop/DisconnectListener.cs | 23 +----- .../RequestProcessing/RequestContextOfT.cs | 1 - .../src/RequestProcessing/TlsListener.Log.cs | 15 ++++ .../src/RequestProcessing/TlsListener.cs | 74 ++++++++++++++++--- 6 files changed, 89 insertions(+), 36 deletions(-) create mode 100644 src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index a8fa0e7c57eb..245fc8206120 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -75,15 +75,10 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) _requestQueue = new RequestQueue(options.RequestQueueName, options.RequestQueueMode, Logger); _urlGroup = new UrlGroup(_serverSession, _requestQueue, Logger); + _disconnectListener = new DisconnectListener(_requestQueue, Logger); if (options.TlsClientHelloBytesCallback is not null) { - _tlsListener = new TlsListener(options.TlsClientHelloBytesCallback); - _disconnectListener = new DisconnectListener(_requestQueue, Logger, onDisconnect: _tlsListener.ConnectionClosed); - } - else - { - _tlsListener = null; - _disconnectListener = new DisconnectListener(_requestQueue, Logger); + _tlsListener = new TlsListener(Logger, options.TlsClientHelloBytesCallback); } } catch (Exception exception) @@ -92,6 +87,7 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) _requestQueue?.Dispose(); _urlGroup?.Dispose(); _serverSession?.Dispose(); + _tlsListener?.Dispose(); Log.HttpSysListenerCtorError(Logger, exception); throw; } @@ -262,6 +258,7 @@ private void DisposeInternal() Debug.Assert(!_serverSession.Id.IsInvalid, "ServerSessionHandle is invalid in CloseV2Config"); _serverSession.Dispose(); + _tlsListener?.Dispose(); } /// diff --git a/src/Servers/HttpSys/src/LoggerEventIds.cs b/src/Servers/HttpSys/src/LoggerEventIds.cs index 5bc0b6b65ed6..e6d745f506be 100644 --- a/src/Servers/HttpSys/src/LoggerEventIds.cs +++ b/src/Servers/HttpSys/src/LoggerEventIds.cs @@ -59,4 +59,5 @@ internal static class LoggerEventIds public const int AcceptCancelExpectationMismatch = 52; public const int AcceptObserveExpectationMismatch = 53; public const int RequestParsingError = 54; + public const int TlsListenerError = 55; } diff --git a/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs b/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs index 24836af8afec..26f95d13674d 100644 --- a/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs +++ b/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs @@ -15,14 +15,10 @@ internal sealed partial class DisconnectListener private readonly RequestQueue _requestQueue; private readonly ILogger _logger; - internal Action? OnDisconnect { get; } - - internal DisconnectListener(RequestQueue requestQueue, ILogger logger, Action? onDisconnect = null) + internal DisconnectListener(RequestQueue requestQueue, ILogger logger) { _requestQueue = requestQueue; _logger = logger; - - OnDisconnect = onDisconnect; } internal CancellationToken GetTokenForConnection(ulong connectionId) @@ -74,7 +70,7 @@ private unsafe CancellationToken CreateDisconnectToken(ulong connectionId) boundHandle.FreeNativeOverlapped(pOverlapped); // Pull the token out of the list and Cancel it. - TryRemoveConnectionCancellationToken(connectionId, out _); + _connectionCancellationTokens.TryRemove(connectionId, out _); try { cts.Cancel(); @@ -104,7 +100,7 @@ private unsafe CancellationToken CreateDisconnectToken(ulong connectionId) { // We got an unknown result, assume the connection has been closed. boundHandle.FreeNativeOverlapped(nativeOverlapped); - TryRemoveConnectionCancellationToken(connectionId, out _); + _connectionCancellationTokens.TryRemove(connectionId, out _); Log.UnknownDisconnectError(_logger, new Win32Exception((int)statusCode)); cts.Cancel(); } @@ -113,24 +109,13 @@ private unsafe CancellationToken CreateDisconnectToken(ulong connectionId) { // IO operation completed synchronously - callback won't be called to signal completion boundHandle.FreeNativeOverlapped(nativeOverlapped); - TryRemoveConnectionCancellationToken(connectionId, out _); + _connectionCancellationTokens.TryRemove(connectionId, out _); cts.Cancel(); } return returnToken; } - private bool TryRemoveConnectionCancellationToken(ulong connectionId, out ConnectionCancellation connectionCancellation) - { - bool result; - if (result = _connectionCancellationTokens.TryRemove(connectionId, out connectionCancellation!)) - { - OnDisconnect?.Invoke(connectionId); - } - - return result; - } - private sealed class ConnectionCancellation { private readonly DisconnectListener _parent; diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs index 5b517b18ee58..3088751a212a 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs @@ -50,7 +50,6 @@ public override async Task ExecuteAsync() { if (Server.Options.TlsClientHelloBytesCallback is not null && Server.TlsListener is not null) { - _ = DisconnectToken; // force disconnect to be followed. Required for TlsListener to keep cache about only active connections Server.TlsListener.InvokeTlsClientHelloCallback(Features, Request); } diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs new file mode 100644 index 000000000000..156fe601aa30 --- /dev/null +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; + +internal sealed partial class TlsListener : IDisposable +{ + private static partial class Log + { + [LoggerMessage(LoggerEventIds.TlsListenerError, LogLevel.Error, "Error during closed connection cleanup", EventName = "TlsListenerCleanupClosedConnectionError")] + public static partial void CleanupClosedConnectionError(ILogger logger, Exception exception); + } +} diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs index 3926d6656926..982ee9429c84 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs @@ -3,37 +3,93 @@ using System.Collections.Concurrent; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; -internal sealed class TlsListener +internal sealed partial class TlsListener : IDisposable { - private readonly ConcurrentDictionary _connectionIdsTlsClientHelloCallbackInvokedMap = new(); - + private readonly ConcurrentDictionary _connectionTimestamps = new(); private readonly Action> _tlsClientHelloBytesCallback; - internal TlsListener(Action> tlsClientHelloBytesCallback) + private readonly ILogger _logger; + private readonly CancellationTokenSource _cts = new(); + private readonly Task _cleanupTask; + + private static readonly TimeSpan ConnectionIdleTime = TimeSpan.FromMinutes(10); + private static readonly TimeSpan CleanupInterval = TimeSpan.FromSeconds(30); + + internal TlsListener(ILogger logger, Action> tlsClientHelloBytesCallback) { + _logger = logger; _tlsClientHelloBytesCallback = tlsClientHelloBytesCallback; + + _cleanupTask = Task.Run(() => CleanupLoopAsync(_cts.Token)); } internal void InvokeTlsClientHelloCallback(IFeatureCollection features, Request request) { - if (_connectionIdsTlsClientHelloCallbackInvokedMap.TryGetValue(request.UConnectionId, out _)) + if (!request.IsHttps) { - // invoking TLS client hello callback per request on same connection is what we are trying to avoid + return; + } + + if (_connectionTimestamps.ContainsKey(request.RawConnectionId)) + { + // update the TTL + _connectionTimestamps[request.RawConnectionId] = DateTime.UtcNow; return; } var success = request.GetAndInvokeTlsClientHelloCallback(features, _tlsClientHelloBytesCallback); if (success) { - _connectionIdsTlsClientHelloCallbackInvokedMap[request.UConnectionId] = true; + _connectionTimestamps[request.RawConnectionId] = DateTime.UtcNow; + } + } + + private async Task CleanupLoopAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + var now = DateTime.UtcNow; + foreach (var kvp in _connectionTimestamps) + { + if (now - kvp.Value > ConnectionIdleTime) + { + _connectionTimestamps.TryRemove(kvp.Key, out _); + } + } + } + catch (Exception ex) + { + Log.CleanupClosedConnectionError(_logger, ex); + } + + try + { + await Task.Delay(CleanupInterval, cancellationToken); + } + catch (TaskCanceledException) + { + break; + } } } - internal void ConnectionClosed(ulong connectionId) + public void Dispose() { - _connectionIdsTlsClientHelloCallbackInvokedMap.TryRemove(connectionId, out _); + _cts.Cancel(); + try + { + _cleanupTask.Wait(); + } + catch + { + // ignore + } + _cts.Dispose(); } } From 6275c30f452a5030f86871dbc1fb26418502f5d7 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Tue, 8 Apr 2025 22:41:00 +0200 Subject: [PATCH 12/24] address comments 1 --- .../src/RequestProcessing/TlsListener.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs index 982ee9429c84..c166ad953315 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs @@ -34,7 +34,7 @@ internal void InvokeTlsClientHelloCallback(IFeatureCollection features, Request return; } - if (_connectionTimestamps.ContainsKey(request.RawConnectionId)) + if (!_connectionTimestamps.TryAdd(request.RawConnectionId, DateTime.UtcNow)) { // update the TTL _connectionTimestamps[request.RawConnectionId] = DateTime.UtcNow; @@ -51,22 +51,15 @@ internal void InvokeTlsClientHelloCallback(IFeatureCollection features, Request private async Task CleanupLoopAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) - { - try + { + var now = DateTime.UtcNow; + foreach (var kvp in _connectionTimestamps) { - var now = DateTime.UtcNow; - foreach (var kvp in _connectionTimestamps) + if (now - kvp.Value > ConnectionIdleTime) { - if (now - kvp.Value > ConnectionIdleTime) - { - _connectionTimestamps.TryRemove(kvp.Key, out _); - } + _connectionTimestamps.TryRemove(kvp.Key, out _); } } - catch (Exception ex) - { - Log.CleanupClosedConnectionError(_logger, ex); - } try { From c58805b1483c86c2948912d609bcf38346959eb0 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Tue, 8 Apr 2025 23:19:34 +0200 Subject: [PATCH 13/24] periodic timer --- src/Servers/HttpSys/src/HttpSysListener.cs | 2 +- .../src/RequestProcessing/TlsListener.cs | 59 ++++++++----------- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 245fc8206120..edff6fdfc521 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -73,7 +73,7 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) { _serverSession = new ServerSession(); _requestQueue = new RequestQueue(options.RequestQueueName, options.RequestQueueMode, Logger); - _urlGroup = new UrlGroup(_serverSession, _requestQueue, Logger); + _urlGroup = new UrlGroup(_serverSession, _requestQueue, Logger); _disconnectListener = new DisconnectListener(_requestQueue, Logger); if (options.TlsClientHelloBytesCallback is not null) diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs index c166ad953315..007655bf4d8f 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs @@ -11,20 +11,20 @@ internal sealed partial class TlsListener : IDisposable { private readonly ConcurrentDictionary _connectionTimestamps = new(); private readonly Action> _tlsClientHelloBytesCallback; - private readonly ILogger _logger; - private readonly CancellationTokenSource _cts = new(); + + private readonly PeriodicTimer _cleanupTimer; private readonly Task _cleanupTask; - private static readonly TimeSpan ConnectionIdleTime = TimeSpan.FromMinutes(10); - private static readonly TimeSpan CleanupInterval = TimeSpan.FromSeconds(30); + private static readonly TimeSpan ConnectionIdleTime = TimeSpan.FromMinutes(5); internal TlsListener(ILogger logger, Action> tlsClientHelloBytesCallback) { _logger = logger; _tlsClientHelloBytesCallback = tlsClientHelloBytesCallback; - _cleanupTask = Task.Run(() => CleanupLoopAsync(_cts.Token)); + _cleanupTimer = new PeriodicTimer(TimeSpan.FromSeconds(30)); + _cleanupTask = Task.Run(CleanupLoopAsync); } internal void InvokeTlsClientHelloCallback(IFeatureCollection features, Request request) @@ -34,9 +34,8 @@ internal void InvokeTlsClientHelloCallback(IFeatureCollection features, Request return; } - if (!_connectionTimestamps.TryAdd(request.RawConnectionId, DateTime.UtcNow)) + if (_connectionTimestamps.ContainsKey(request.RawConnectionId)) { - // update the TTL _connectionTimestamps[request.RawConnectionId] = DateTime.UtcNow; return; } @@ -48,41 +47,35 @@ internal void InvokeTlsClientHelloCallback(IFeatureCollection features, Request } } - private async Task CleanupLoopAsync(CancellationToken cancellationToken) + private async Task CleanupLoopAsync() { - while (!cancellationToken.IsCancellationRequested) - { - var now = DateTime.UtcNow; - foreach (var kvp in _connectionTimestamps) + try + { + while (await _cleanupTimer.WaitForNextTickAsync()) { - if (now - kvp.Value > ConnectionIdleTime) + var now = DateTime.UtcNow; + foreach (var kvp in _connectionTimestamps) { - _connectionTimestamps.TryRemove(kvp.Key, out _); + if (now - kvp.Value > ConnectionIdleTime) + { + _connectionTimestamps.TryRemove(kvp.Key, out _); + } } } - - try - { - await Task.Delay(CleanupInterval, cancellationToken); - } - catch (TaskCanceledException) - { - break; - } } - } - - public void Dispose() - { - _cts.Cancel(); - try + catch (OperationCanceledException) { - _cleanupTask.Wait(); + // expected on shutdown } - catch + catch (Exception ex) { - // ignore + Log.CleanupClosedConnectionError(_logger, ex); } - _cts.Dispose(); + } + + public void Dispose() + { + try { _cleanupTask.Wait(); } catch { } + _cleanupTimer.Dispose(); } } From 25d49553bcc77edc2ed327d2b54dd23a0ea9d9ab Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Wed, 9 Apr 2025 00:26:51 +0200 Subject: [PATCH 14/24] address comments x3 --- src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs index 007655bf4d8f..4fd4f2898fbb 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs @@ -24,7 +24,7 @@ internal TlsListener(ILogger logger, Action Date: Wed, 9 Apr 2025 00:35:08 +0200 Subject: [PATCH 15/24] TryAdd --- .../src/RequestProcessing/RequestContext.Log.cs | 4 ++-- .../HttpSys/src/RequestProcessing/RequestContext.cs | 2 +- .../HttpSys/src/RequestProcessing/TlsListener.cs | 10 +++------- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs index 3bf35e85854c..d7766698bc41 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs @@ -21,7 +21,7 @@ private static partial class Log [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to parse request.", EventName = "RequestParsingError")] public static partial void RequestParsingError(ILogger logger, Exception exception); - [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello: Win32 Error code: {Win32Error}", EventName = "TlsClientHelloRetrieveError")] - public static partial void TlsClientHelloRetrieveError(ILogger logger, uint win32Error); + [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello; RequestId: {RequestId}; Win32 Error code: {Win32Error}", EventName = "TlsClientHelloRetrieveError")] + public static partial void TlsClientHelloRetrieveError(ILogger logger, ulong requestId, uint win32Error); } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index c3bb5d55e67d..58b52474c0dd 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -316,7 +316,7 @@ internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureColl } } - Log.TlsClientHelloRetrieveError(Logger, statusCode); + Log.TlsClientHelloRetrieveError(Logger, requestId, statusCode); return false; } diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs index 4fd4f2898fbb..3498d185e0e5 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs @@ -34,17 +34,13 @@ internal void InvokeTlsClientHelloCallback(IFeatureCollection features, Request return; } - if (_connectionTimestamps.ContainsKey(request.RawConnectionId)) + if (!_connectionTimestamps.TryAdd(request.RawConnectionId, DateTime.UtcNow)) { - _connectionTimestamps[request.RawConnectionId] = DateTime.UtcNow; + _connectionTimestamps[request.RawConnectionId] = DateTime.UtcNow; // update TTL return; } - var success = request.GetAndInvokeTlsClientHelloCallback(features, _tlsClientHelloBytesCallback); - if (success) - { - _connectionTimestamps[request.RawConnectionId] = DateTime.UtcNow; - } + _ = request.GetAndInvokeTlsClientHelloCallback(features, _tlsClientHelloBytesCallback); } private async Task CleanupLoopAsync() From 7a39916d148addfe1fc87c50965d6f85cdfded6d Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Wed, 9 Apr 2025 00:36:12 +0200 Subject: [PATCH 16/24] make a static field (just in case) --- src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs index 3498d185e0e5..faf526f3c3e3 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs @@ -17,13 +17,14 @@ internal sealed partial class TlsListener : IDisposable private readonly Task _cleanupTask; private static readonly TimeSpan ConnectionIdleTime = TimeSpan.FromMinutes(5); + private static readonly TimeSpan CleanupDelay = TimeSpan.FromSeconds(30); internal TlsListener(ILogger logger, Action> tlsClientHelloBytesCallback) { _logger = logger; _tlsClientHelloBytesCallback = tlsClientHelloBytesCallback; - _cleanupTimer = new PeriodicTimer(TimeSpan.FromSeconds(30)); + _cleanupTimer = new PeriodicTimer(CleanupDelay); _cleanupTask = CleanupLoopAsync(); } From 4f85d5f021579929803478ebeed7b4545624a41f Mon Sep 17 00:00:00 2001 From: Brennan Date: Thu, 10 Apr 2025 16:34:53 -0700 Subject: [PATCH 17/24] Cache updates --- .../HttpSys/src/NativeInterop/HttpApi.cs | 1 + .../src/RequestProcessing/RequestContext.cs | 2 +- .../src/RequestProcessing/TlsListener.Log.cs | 2 +- .../src/RequestProcessing/TlsListener.cs | 56 ++++++++++++++++--- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index 51173e6a6ee6..b0bd726fdb07 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -152,6 +152,7 @@ private static void InitHttpApi(ushort majorVersion, ushort minorVersion) SupportsReset = HttpSetRequestPropertySupported; SupportsTrailers = IsFeatureSupported(HTTP_FEATURE_ID.HttpFeatureResponseTrailers); SupportsDelegation = IsFeatureSupported(HTTP_FEATURE_ID.HttpFeatureDelegateEx); + SupportsClientHello = IsFeatureSupported((HTTP_FEATURE_ID)11 /* HTTP_FEATURE_ID.HttpFeatureCacheTlsClientHello */) && HttpGetRequestPropertySupported; } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index 58b52474c0dd..56375db2c19d 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -240,7 +240,7 @@ internal void ForceCancelRequest() /// internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureCollection features, Action> tlsClientHelloBytesCallback) { - if (!HttpApi.HttpGetRequestPropertySupported) + if (!HttpApi.SupportsClientHello) { // not supported, so we just return and don't invoke the callback return false; diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs index 156fe601aa30..20ffe5c74b6f 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs @@ -9,7 +9,7 @@ internal sealed partial class TlsListener : IDisposable { private static partial class Log { - [LoggerMessage(LoggerEventIds.TlsListenerError, LogLevel.Error, "Error during closed connection cleanup", EventName = "TlsListenerCleanupClosedConnectionError")] + [LoggerMessage(LoggerEventIds.TlsListenerError, LogLevel.Error, "Error during closed connection cleanup.", EventName = "TlsListenerCleanupClosedConnectionError")] public static partial void CleanupClosedConnectionError(ILogger logger, Exception exception); } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs index faf526f3c3e3..9d862e2e544a 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs @@ -17,7 +17,8 @@ internal sealed partial class TlsListener : IDisposable private readonly Task _cleanupTask; private static readonly TimeSpan ConnectionIdleTime = TimeSpan.FromMinutes(5); - private static readonly TimeSpan CleanupDelay = TimeSpan.FromSeconds(30); + private static readonly TimeSpan CleanupDelay = TimeSpan.FromSeconds(10); + private const int CacheSizeLimit = 1_000_000; internal TlsListener(ILogger logger, Action> tlsClientHelloBytesCallback) { @@ -46,11 +47,13 @@ internal void InvokeTlsClientHelloCallback(IFeatureCollection features, Request private async Task CleanupLoopAsync() { - try + while (await _cleanupTimer.WaitForNextTickAsync()) { - while (await _cleanupTimer.WaitForNextTickAsync()) + try { var now = DateTime.UtcNow; + + // Remove idle connections foreach (var kvp in _connectionTimestamps) { if (now - kvp.Value > ConnectionIdleTime) @@ -58,11 +61,40 @@ private async Task CleanupLoopAsync() _connectionTimestamps.TryRemove(kvp.Key, out _); } } + + // Evict oldest items if above CacheSizeLimit + var currentCount = _connectionTimestamps.Count; + if (currentCount > CacheSizeLimit) + { + var excessCount = currentCount - CacheSizeLimit; + + // Find the oldest items in a single pass + var oldestTimestamps = new SortedSet>(TimeComparer.Instance); + + foreach (var kvp in _connectionTimestamps) + { + if (oldestTimestamps.Count < excessCount) + { + oldestTimestamps.Add(new KeyValuePair(kvp.Key, kvp.Value)); + } + else if (kvp.Value < oldestTimestamps.Max.Value) + { + oldestTimestamps.Remove(oldestTimestamps.Max); + oldestTimestamps.Add(new KeyValuePair(kvp.Key, kvp.Value)); + } + } + + // Remove the oldest keys + foreach (var item in oldestTimestamps) + { + _connectionTimestamps.TryRemove(item.Key, out _); + } + } + } + catch (Exception ex) + { + Log.CleanupClosedConnectionError(_logger, ex); } - } - catch (Exception ex) - { - Log.CleanupClosedConnectionError(_logger, ex); } } @@ -71,4 +103,14 @@ public void Dispose() _cleanupTimer.Dispose(); _cleanupTask.Wait(); } + + private sealed class TimeComparer : IComparer> + { + public static TimeComparer Instance { get; } = new TimeComparer(); + + public int Compare(KeyValuePair x, KeyValuePair y) + { + return x.Value.CompareTo(y.Value); + } + } } From 53721d73a773e2e9db88617f354172dbaa253d7d Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 11 Apr 2025 15:48:15 -0700 Subject: [PATCH 18/24] test --- .../RequestProcessing/RequestContextOfT.cs | 5 +- .../src/RequestProcessing/TlsListener.cs | 56 ++++--- ...Core.Server.HttpSys.FunctionalTests.csproj | 1 + .../test/FunctionalTests/TlsListenerTests.cs | 140 ++++++++++++++++++ 4 files changed, 178 insertions(+), 24 deletions(-) create mode 100644 src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs index 3088751a212a..399f1292d60d 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs @@ -48,9 +48,10 @@ public override async Task ExecuteAsync() context = application.CreateContext(Features); try { - if (Server.Options.TlsClientHelloBytesCallback is not null && Server.TlsListener is not null) + if (Server.Options.TlsClientHelloBytesCallback is not null && Server.TlsListener is not null + && Request.IsHttps) { - Server.TlsListener.InvokeTlsClientHelloCallback(Features, Request); + Server.TlsListener.InvokeTlsClientHelloCallback(Request.RawConnectionId, Features, Request.GetAndInvokeTlsClientHelloCallback); } await application.ProcessRequestAsync(context); diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs index 9d862e2e544a..be7ce9be3f3b 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; +using System.Collections.ObjectModel; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; @@ -9,54 +10,57 @@ namespace Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; internal sealed partial class TlsListener : IDisposable { - private readonly ConcurrentDictionary _connectionTimestamps = new(); + private readonly ConcurrentDictionary _connectionTimestamps = new(); private readonly Action> _tlsClientHelloBytesCallback; private readonly ILogger _logger; private readonly PeriodicTimer _cleanupTimer; private readonly Task _cleanupTask; + private readonly TimeProvider _timeProvider; private static readonly TimeSpan ConnectionIdleTime = TimeSpan.FromMinutes(5); private static readonly TimeSpan CleanupDelay = TimeSpan.FromSeconds(10); - private const int CacheSizeLimit = 1_000_000; + internal const int CacheSizeLimit = 1_000_000; - internal TlsListener(ILogger logger, Action> tlsClientHelloBytesCallback) + // Internal for testing purposes + internal ReadOnlyDictionary ConnectionTimeStamps => _connectionTimestamps.AsReadOnly(); + + internal TlsListener(ILogger logger, Action> tlsClientHelloBytesCallback, TimeProvider? timeProvider = null) { _logger = logger; _tlsClientHelloBytesCallback = tlsClientHelloBytesCallback; - _cleanupTimer = new PeriodicTimer(CleanupDelay); + _timeProvider = timeProvider ?? TimeProvider.System; + _cleanupTimer = new PeriodicTimer(CleanupDelay, _timeProvider); _cleanupTask = CleanupLoopAsync(); } - internal void InvokeTlsClientHelloCallback(IFeatureCollection features, Request request) + // Method looks weird because we want it to be testable by not directly requiring a Request object + internal void InvokeTlsClientHelloCallback(ulong connectionId, IFeatureCollection features, + Func>, bool> invokeTlsClientHelloCallback) { - if (!request.IsHttps) - { - return; - } - - if (!_connectionTimestamps.TryAdd(request.RawConnectionId, DateTime.UtcNow)) + if (!_connectionTimestamps.TryAdd(connectionId, _timeProvider.GetUtcNow())) { - _connectionTimestamps[request.RawConnectionId] = DateTime.UtcNow; // update TTL + // update TTL + _connectionTimestamps[connectionId] = _timeProvider.GetUtcNow(); return; } - _ = request.GetAndInvokeTlsClientHelloCallback(features, _tlsClientHelloBytesCallback); + _ = invokeTlsClientHelloCallback(features, _tlsClientHelloBytesCallback); } - private async Task CleanupLoopAsync() + internal async Task CleanupLoopAsync() { while (await _cleanupTimer.WaitForNextTickAsync()) { try { - var now = DateTime.UtcNow; + var now = _timeProvider.GetUtcNow(); // Remove idle connections foreach (var kvp in _connectionTimestamps) { - if (now - kvp.Value > ConnectionIdleTime) + if (now - kvp.Value >= ConnectionIdleTime) { _connectionTimestamps.TryRemove(kvp.Key, out _); } @@ -69,18 +73,18 @@ private async Task CleanupLoopAsync() var excessCount = currentCount - CacheSizeLimit; // Find the oldest items in a single pass - var oldestTimestamps = new SortedSet>(TimeComparer.Instance); + var oldestTimestamps = new SortedSet>(TimeComparer.Instance); foreach (var kvp in _connectionTimestamps) { if (oldestTimestamps.Count < excessCount) { - oldestTimestamps.Add(new KeyValuePair(kvp.Key, kvp.Value)); + oldestTimestamps.Add(new KeyValuePair(kvp.Key, kvp.Value)); } else if (kvp.Value < oldestTimestamps.Max.Value) { oldestTimestamps.Remove(oldestTimestamps.Max); - oldestTimestamps.Add(new KeyValuePair(kvp.Key, kvp.Value)); + oldestTimestamps.Add(new KeyValuePair(kvp.Key, kvp.Value)); } } @@ -104,13 +108,21 @@ public void Dispose() _cleanupTask.Wait(); } - private sealed class TimeComparer : IComparer> + private sealed class TimeComparer : IComparer> { public static TimeComparer Instance { get; } = new TimeComparer(); - public int Compare(KeyValuePair x, KeyValuePair y) + public int Compare(KeyValuePair x, KeyValuePair y) { - return x.Value.CompareTo(y.Value); + // Compare timestamps first + int timestampComparison = x.Value.CompareTo(y.Value); + if (timestampComparison != 0) + { + return timestampComparison; + } + + // Use the key as a tiebreaker to ensure uniqueness + return x.Key.CompareTo(y.Key); } } } diff --git a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj index 08276e6a23fd..56f300b89198 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj +++ b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs b/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs new file mode 100644 index 000000000000..73cc525eb8f3 --- /dev/null +++ b/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Time.Testing; +using Moq; + +namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests; + +public class TlsListenerTests +{ + [Fact] + public void AddsAndUpdatesConnectionTimestamps() + { + // Arrange + var logger = Mock.Of(); + var timeProvider = new FakeTimeProvider(); + var callbackInvoked = false; + var tlsListener = new TlsListener(logger, (_, __) => { callbackInvoked = true; }, timeProvider); + + var features = Mock.Of(); + + // Act + tlsListener.InvokeTlsClientHelloCallback(connectionId: 1UL, features, + invokeTlsClientHelloCallback: (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); + + var originalTime = timeProvider.GetUtcNow(); + + // Assert + Assert.True(callbackInvoked); + Assert.Equal(originalTime, Assert.Single(tlsListener.ConnectionTimeStamps).Value); + + timeProvider.Advance(TimeSpan.FromSeconds(1)); + callbackInvoked = false; + // Update the timestamp + tlsListener.InvokeTlsClientHelloCallback(connectionId: 1UL, features, + invokeTlsClientHelloCallback: (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); + + // Callback should not be invoked again and the timestamp should be updated + Assert.False(callbackInvoked); + Assert.Equal(timeProvider.GetUtcNow(), Assert.Single(tlsListener.ConnectionTimeStamps).Value); + Assert.NotEqual(originalTime, timeProvider.GetUtcNow()); + } + + [Fact] + public async Task RemovesIdleConnections() + { + // Arrange + var logger = Mock.Of(); + var timeProvider = new FakeTimeProvider(); + using var tlsListener = new TlsListener(logger, (_, __) => { }, timeProvider); + + var features = Mock.Of(); + + bool InvokeCallback(IFeatureCollection f, Action> cb) + { + cb(f, ReadOnlySpan.Empty); + return true; + } + + // Act + tlsListener.InvokeTlsClientHelloCallback(connectionId: 1UL, features, InvokeCallback); + + // 1 less minute than the idle time cleanup + timeProvider.Advance(TimeSpan.FromMinutes(4)); + Assert.Single(tlsListener.ConnectionTimeStamps); + + tlsListener.InvokeTlsClientHelloCallback(connectionId: 2UL, features, InvokeCallback); + Assert.Equal(2, tlsListener.ConnectionTimeStamps.Count); + + // With the previous 4 minutes, this should be 5 minutes and remove the first connection + timeProvider.Advance(TimeSpan.FromMinutes(1)); + + var timeout = TimeSpan.FromSeconds(5); + while (timeout > TimeSpan.Zero) + { + // Wait for the cleanup loop to run + if (tlsListener.ConnectionTimeStamps.Count == 1) + { + break; + } + timeout -= TimeSpan.FromMilliseconds(100); + await Task.Delay(100); + } + + // Assert + Assert.Single(tlsListener.ConnectionTimeStamps); + Assert.Contains(2UL, tlsListener.ConnectionTimeStamps.Keys); + } + + [Fact] + public async Task EvictsOldestConnectionsWhenExceedingCacheSizeLimit() + { + // Arrange + var logger = Mock.Of(); + var timeProvider = new FakeTimeProvider(); + var tlsListener = new TlsListener(logger, (_, __) => { }, timeProvider); + var features = Mock.Of(); + + ulong i = 0; + for (; i < TlsListener.CacheSizeLimit; i++) + { + tlsListener.InvokeTlsClientHelloCallback(i, features, (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); + } + + timeProvider.Advance(TimeSpan.FromSeconds(5)); + + for (; i < TlsListener.CacheSizeLimit + 3; i++) + { + tlsListener.InvokeTlsClientHelloCallback(i, features, (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); + } + + // 'touch' first connection to update its timestamp + tlsListener.InvokeTlsClientHelloCallback(0, features, (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); + + // Make sure the cleanup loop has run to evict items since we're above the cache size limit + timeProvider.Advance(TimeSpan.FromMinutes(1)); + + var timeout = TimeSpan.FromSeconds(5); + while (timeout > TimeSpan.Zero) + { + // Wait for the cleanup loop to run + if (tlsListener.ConnectionTimeStamps.Count == TlsListener.CacheSizeLimit) + { + break; + } + timeout -= TimeSpan.FromMilliseconds(100); + await Task.Delay(100); + } + + Assert.Equal(TlsListener.CacheSizeLimit, tlsListener.ConnectionTimeStamps.Count); + Assert.Contains(0UL, tlsListener.ConnectionTimeStamps.Keys); + // 3 newest connections should be present + Assert.Contains(i - 1, tlsListener.ConnectionTimeStamps.Keys); + Assert.Contains(i - 2, tlsListener.ConnectionTimeStamps.Keys); + Assert.Contains(i - 3, tlsListener.ConnectionTimeStamps.Keys); + } +} From 9b012c3ff9bf482ece152bab5146c4a05d23c179 Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 11 Apr 2025 15:49:26 -0700 Subject: [PATCH 19/24] whitespace --- src/Servers/HttpSys/src/HttpSysListener.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index edff6fdfc521..245fc8206120 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -73,7 +73,7 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) { _serverSession = new ServerSession(); _requestQueue = new RequestQueue(options.RequestQueueName, options.RequestQueueMode, Logger); - _urlGroup = new UrlGroup(_serverSession, _requestQueue, Logger); + _urlGroup = new UrlGroup(_serverSession, _requestQueue, Logger); _disconnectListener = new DisconnectListener(_requestQueue, Logger); if (options.TlsClientHelloBytesCallback is not null) From a801563ab913240a884998c732d2dfa08e33c9fd Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 11 Apr 2025 15:53:50 -0700 Subject: [PATCH 20/24] clear array --- src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index 56375db2c19d..f338d87b2c2b 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -278,7 +278,7 @@ internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureColl } finally { - ArrayPool.Shared.Return(buffer); + ArrayPool.Shared.Return(buffer, clearArray: true); } // if buffer supplied is too small, `bytesReturned` will have proper size @@ -312,7 +312,7 @@ internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureColl } finally { - ArrayPool.Shared.Return(buffer); + ArrayPool.Shared.Return(buffer, clearArray: true); } } From 03c0cfbd1ceecdd4c36a569682b5d9541fe995fc Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 11 Apr 2025 16:58:32 -0700 Subject: [PATCH 21/24] appcontext --- .../src/RequestProcessing/TlsListener.cs | 21 ++++++++++++++++--- .../test/FunctionalTests/TlsListenerTests.cs | 8 +++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs index be7ce9be3f3b..8e7edb9bb47d 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs @@ -18,15 +18,30 @@ internal sealed partial class TlsListener : IDisposable private readonly Task _cleanupTask; private readonly TimeProvider _timeProvider; - private static readonly TimeSpan ConnectionIdleTime = TimeSpan.FromMinutes(5); - private static readonly TimeSpan CleanupDelay = TimeSpan.FromSeconds(10); - internal const int CacheSizeLimit = 1_000_000; + private readonly TimeSpan ConnectionIdleTime = TimeSpan.FromMinutes(5); + private readonly TimeSpan CleanupDelay = TimeSpan.FromSeconds(10); + internal readonly int CacheSizeLimit = 1_000_000; // Internal for testing purposes internal ReadOnlyDictionary ConnectionTimeStamps => _connectionTimestamps.AsReadOnly(); internal TlsListener(ILogger logger, Action> tlsClientHelloBytesCallback, TimeProvider? timeProvider = null) { + if (AppContext.GetData("Microsoft.AspNetCore.Server.HttpSys.TlsListener.CacheSizeLimit") is int limit) + { + CacheSizeLimit = limit; + } + + if (AppContext.GetData("Microsoft.AspNetCore.Server.HttpSys.TlsListener.ConnectionIdleTime") is int idleTime) + { + ConnectionIdleTime = TimeSpan.FromSeconds(idleTime); + } + + if (AppContext.GetData("Microsoft.AspNetCore.Server.HttpSys.TlsListener.CleanupDelay") is int cleanupDelay) + { + CleanupDelay = TimeSpan.FromSeconds(cleanupDelay); + } + _logger = logger; _tlsClientHelloBytesCallback = tlsClientHelloBytesCallback; diff --git a/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs b/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs index 73cc525eb8f3..d0ff2731a017 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs @@ -100,14 +100,14 @@ public async Task EvictsOldestConnectionsWhenExceedingCacheSizeLimit() var features = Mock.Of(); ulong i = 0; - for (; i < TlsListener.CacheSizeLimit; i++) + for (; i < (ulong)tlsListener.CacheSizeLimit; i++) { tlsListener.InvokeTlsClientHelloCallback(i, features, (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); } timeProvider.Advance(TimeSpan.FromSeconds(5)); - for (; i < TlsListener.CacheSizeLimit + 3; i++) + for (; i < (ulong)tlsListener.CacheSizeLimit + 3; i++) { tlsListener.InvokeTlsClientHelloCallback(i, features, (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); } @@ -122,7 +122,7 @@ public async Task EvictsOldestConnectionsWhenExceedingCacheSizeLimit() while (timeout > TimeSpan.Zero) { // Wait for the cleanup loop to run - if (tlsListener.ConnectionTimeStamps.Count == TlsListener.CacheSizeLimit) + if (tlsListener.ConnectionTimeStamps.Count == tlsListener.CacheSizeLimit) { break; } @@ -130,7 +130,7 @@ public async Task EvictsOldestConnectionsWhenExceedingCacheSizeLimit() await Task.Delay(100); } - Assert.Equal(TlsListener.CacheSizeLimit, tlsListener.ConnectionTimeStamps.Count); + Assert.Equal(tlsListener.CacheSizeLimit, tlsListener.ConnectionTimeStamps.Count); Assert.Contains(0UL, tlsListener.ConnectionTimeStamps.Keys); // 3 newest connections should be present Assert.Contains(i - 1, tlsListener.ConnectionTimeStamps.Keys); From 370d39c76495698d3a8f8738bd9d69c8f9ebe572 Mon Sep 17 00:00:00 2001 From: Brennan Date: Mon, 14 Apr 2025 11:54:13 -0700 Subject: [PATCH 22/24] fb --- .../HttpSys/HttpSysConfigurator.cs | 37 +++++++++---------- .../TlsFeaturesObserve/HttpSys/Native.cs | 2 + 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs index e5a81b955842..3865ecd59451 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/HttpSysConfigurator.cs @@ -1,12 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Net; -using System.Net.Sockets; using System.Runtime.InteropServices; -using System.Text; -using Microsoft.AspNetCore.Http; namespace TlsFeaturesObserve.HttpSys; @@ -19,21 +15,22 @@ internal static class HttpSysConfigurator internal static void ConfigureCacheTlsClientHello() { - IPEndPoint ipPort = new IPEndPoint(new IPAddress([0, 0, 0, 0]), 6000); - string certThumbprint = "" /* your cert thumbprint here */; - Guid appId = Guid.NewGuid(); - string sslCertStoreName = "My"; + // Arbitrarily chosen port, but must match the port used in the web server. Via UrlPrefixes or launchsettings. + var ipPort = new IPEndPoint(new IPAddress([0, 0, 0, 0]), 6000); + var certThumbprint = "" /* your cert thumbprint here */; + var appId = Guid.NewGuid(); + var sslCertStoreName = "My"; CallHttpApi(() => SetConfiguration(ipPort, certThumbprint, appId, sslCertStoreName)); } static void SetConfiguration(IPEndPoint ipPort, string certThumbprint, Guid appId, string sslCertStoreName) { - GCHandle sockAddrHandle = CreateSockaddrStructure(ipPort); + var sockAddrHandle = CreateSockaddrStructure(ipPort); var pIpPort = sockAddrHandle.AddrOfPinnedObject(); var httpServiceConfigSslKey = new HTTP_SERVICE_CONFIG_SSL_KEY(pIpPort); - byte[] hash = GetHash(certThumbprint); + var hash = GetHash(certThumbprint); var handleHash = GCHandle.Alloc(hash, GCHandleType.Pinned); var configSslParam = new HTTP_SERVICE_CONFIG_SSL_PARAM { @@ -58,7 +55,7 @@ static void SetConfiguration(IPEndPoint ipPort, string certThumbprint, Guid appI Marshal.SizeOf(typeof(HTTP_SERVICE_CONFIG_SSL_SET))); Marshal.StructureToPtr(configSslSet, pInputConfigInfo, false); - uint status = HttpSetServiceConfiguration(nint.Zero, + var status = HttpSetServiceConfiguration(nint.Zero, HTTP_SERVICE_CONFIG_ID.HttpServiceConfigSSLCertInfo, pInputConfigInfo, Marshal.SizeOf(configSslSet), @@ -66,7 +63,7 @@ static void SetConfiguration(IPEndPoint ipPort, string certThumbprint, Guid appI if (status == ERROR_ALREADY_EXISTS || status == 0) // already present or success { - Console.WriteLine("HttpServiceConfiguration is correct"); + Console.WriteLine($"HttpServiceConfiguration is correct"); } else { @@ -76,9 +73,9 @@ static void SetConfiguration(IPEndPoint ipPort, string certThumbprint, Guid appI static byte[] GetHash(string thumbprint) { - int length = thumbprint.Length; - byte[] bytes = new byte[length / 2]; - for (int i = 0; i < length; i += 2) + var length = thumbprint.Length; + var bytes = new byte[length / 2]; + for (var i = 0; i < length; i += 2) { bytes[i / 2] = Convert.ToByte(thumbprint.Substring(i, 2), 16); } @@ -88,12 +85,12 @@ static byte[] GetHash(string thumbprint) static GCHandle CreateSockaddrStructure(IPEndPoint ipEndPoint) { - SocketAddress socketAddress = ipEndPoint.Serialize(); + var socketAddress = ipEndPoint.Serialize(); // use an array of bytes instead of the sockaddr structure - byte[] sockAddrStructureBytes = new byte[socketAddress.Size]; - GCHandle sockAddrHandle = GCHandle.Alloc(sockAddrStructureBytes, GCHandleType.Pinned); - for (int i = 0; i < socketAddress.Size; ++i) + var sockAddrStructureBytes = new byte[socketAddress.Size]; + var sockAddrHandle = GCHandle.Alloc(sockAddrStructureBytes, GCHandleType.Pinned); + for (var i = 0; i < socketAddress.Size; ++i) { sockAddrStructureBytes[i] = socketAddress[i]; } @@ -103,7 +100,7 @@ static GCHandle CreateSockaddrStructure(IPEndPoint ipEndPoint) static void CallHttpApi(Action body) { const uint flags = HTTP_INITIALIZE_CONFIG; - uint retVal = HttpInitialize(HttpApiVersion, flags, IntPtr.Zero); + var retVal = HttpInitialize(HttpApiVersion, flags, IntPtr.Zero); body(); } diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/Native.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/Native.cs index 291ca5c4d8dc..b939163d2252 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/Native.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/HttpSys/Native.cs @@ -8,6 +8,8 @@ namespace TlsFeaturesObserve.HttpSys; +// Http.Sys types from https://learn.microsoft.com/windows/win32/api/http/ + [StructLayout(LayoutKind.Sequential, Pack = 2)] public struct HTTPAPI_VERSION { From abd38ff6c6c97fb8ea7e8af2ee38009bf66b2ae9 Mon Sep 17 00:00:00 2001 From: Brennan Date: Mon, 14 Apr 2025 14:08:12 -0700 Subject: [PATCH 23/24] bp changes --- .../samples/TlsFeaturesObserve/Program.cs | 23 +++++++++++++------ src/Servers/HttpSys/src/HttpSysListener.cs | 1 - src/Servers/HttpSys/src/HttpSysOptions.cs | 4 +++- .../HttpSys/src/NativeInterop/HttpApi.cs | 23 ++++++++++++++++--- .../HttpSys/src/PublicAPI.Unshipped.txt | 2 -- .../HttpSys/src/RequestProcessing/Request.cs | 3 ++- .../src/RequestProcessing/RequestContext.cs | 13 +++++++---- .../src/RequestProcessing/TlsListener.cs | 7 +++--- .../test/FunctionalTests/TlsListenerTests.cs | 3 ++- .../NativeInterop/UnsafeNativeMethods.cs | 1 + 10 files changed, 57 insertions(+), 23 deletions(-) diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs index 3742211a9a8d..9551965ac398 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs @@ -26,19 +26,28 @@ static IHostBuilder CreateHostBuilder(string[] args) => options.Authentication.Schemes = AuthenticationSchemes.None; options.Authentication.AllowAnonymous = true; - options.TlsClientHelloBytesCallback = ProcessTlsClientHello; + var property = typeof(HttpSysOptions).GetProperty("TlsClientHelloBytesCallback", BindingFlags.NonPublic | BindingFlags.Instance); + var delegateType = property.PropertyType; // Get the exact delegate type + + // Create a delegate of the correct type + var callbackDelegate = Delegate.CreateDelegate(delegateType, typeof(Holder).GetMethod(nameof(Holder.ProcessTlsClientHello), BindingFlags.Static | BindingFlags.Public)); + + property?.SetValue(options, callbackDelegate); }); }); -static void ProcessTlsClientHello(IFeatureCollection features, ReadOnlySpan tlsClientHelloBytes) +public static class Holder { - var httpConnectionFeature = features.Get(); + public static void ProcessTlsClientHello(IFeatureCollection features, ReadOnlySpan tlsClientHelloBytes) + { + var httpConnectionFeature = features.Get(); - var myTlsFeature = new MyTlsFeature( - connectionId: httpConnectionFeature.ConnectionId, - tlsClientHelloLength: tlsClientHelloBytes.Length); + var myTlsFeature = new MyTlsFeature( + connectionId: httpConnectionFeature.ConnectionId, + tlsClientHelloLength: tlsClientHelloBytes.Length); - features.Set(myTlsFeature); + features.Set(myTlsFeature); + } } public interface IMyTlsFeature diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 245fc8206120..7fecff3c848d 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; -using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.HttpSys; diff --git a/src/Servers/HttpSys/src/HttpSysOptions.cs b/src/Servers/HttpSys/src/HttpSysOptions.cs index 7ab8de6b8821..dfed2d7beced 100644 --- a/src/Servers/HttpSys/src/HttpSysOptions.cs +++ b/src/Servers/HttpSys/src/HttpSysOptions.cs @@ -255,7 +255,9 @@ public Http503VerbosityLevel Http503Verbosity /// See /// and /// - public Action>? TlsClientHelloBytesCallback { get; set; } + internal TlsClientHelloCallback? TlsClientHelloBytesCallback { get; set; } + + internal delegate void TlsClientHelloCallback(IFeatureCollection features, ReadOnlySpan clientHelloBytes); // Not called when attaching to an existing queue. internal void Apply(UrlGroup urlGroup, RequestQueue? requestQueue) diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index b0bd726fdb07..efb42c87c5b5 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -122,13 +122,30 @@ internal static HTTP_API_VERSION ApiVersion } internal static SafeLibraryHandle? HttpApiModule { get; private set; } - internal static HttpGetRequestPropertyInvoker? HttpGetRequestProperty { get; private set; } - internal static HttpSetRequestPropertyInvoker? HttpSetRequestProperty { get; private set; } - [MemberNotNullWhen(true, nameof(HttpSetRequestProperty))] + private static HttpGetRequestPropertyInvoker? HttpGetRequestInvoker { get; set; } + private static HttpSetRequestPropertyInvoker? HttpSetRequestInvoker { get; set; } + + internal static bool HttpGetRequestPropertySupported => HttpGetRequestInvoker is not null; + internal static bool HttpSetRequestPropertySupported => HttpSetRequestInvoker is not null; + + internal static unsafe uint HttpGetRequestProperty(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, + void* qualifier, uint qualifierSize, void* output, uint outputSize, uint* bytesReturned, IntPtr overlapped) + { + return HttpGetRequestInvoker!(requestQueueHandle, requestId, propertyId, qualifier, qualifierSize, output, outputSize, bytesReturned, overlapped); + } + + internal static unsafe uint HttpSetRequestProperty(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, + void* input, uint inputSize, IntPtr overlapped) + { + return HttpSetRequestInvoker!(requestQueueHandle, requestId, propertyId, input, inputSize, overlapped); + } + + [MemberNotNullWhen(true, nameof(HttpSetRequestInvoker))] internal static bool SupportsTrailers { get; private set; } [MemberNotNullWhen(true, nameof(HttpSetRequestInvoker))] internal static bool SupportsReset { get; private set; } internal static bool SupportsDelegation { get; private set; } + internal static bool SupportsClientHello { get; private set; } static HttpApi() { diff --git a/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt b/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt index e18d576e45d3..7dc5c58110bf 100644 --- a/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt +++ b/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt @@ -1,3 +1 @@ #nullable enable -Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.get -> System.Action>? -Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.set -> void diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index adc948e2a471..9aa93adb508d 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; +using static Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions; namespace Microsoft.AspNetCore.Server.HttpSys; @@ -363,7 +364,7 @@ private void GetTlsHandshakeResults() SniHostName = sni.Hostname; } - internal bool GetAndInvokeTlsClientHelloCallback(IFeatureCollection features, Action> tlsClientHelloBytesCallback) + internal bool GetAndInvokeTlsClientHelloCallback(IFeatureCollection features, TlsClientHelloCallback tlsClientHelloBytesCallback) => RequestContext.GetAndInvokeTlsClientHelloMessageBytesCallback(features, tlsClientHelloBytesCallback); public X509Certificate2? ClientCertificate diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index f338d87b2c2b..dfc69e8882da 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -2,12 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Diagnostics; using System.Runtime.InteropServices; +using System.Security.Authentication.ExtendedProtection; using System.Security.Principal; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; +using static Microsoft.AspNetCore.HttpSys.Internal.HttpApiTypes; +using static Microsoft.AspNetCore.HttpSys.Internal.UnsafeNclNativeMethods; +using static Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions; namespace Microsoft.AspNetCore.Server.HttpSys; @@ -238,7 +243,7 @@ internal void ForceCancelRequest() /// Attempts to get the client hello message bytes from the http.sys. /// If not successful, will return false. /// - internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureCollection features, Action> tlsClientHelloBytesCallback) + internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureCollection features, TlsClientHelloCallback tlsClientHelloBytesCallback) { if (!HttpApi.SupportsClientHello) { @@ -266,7 +271,7 @@ internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureColl qualifierSize: 0, output: pBuffer, outputSize: (uint)buffer.Length, - bytesReturned: (IntPtr)bytesReturned, + bytesReturned: bytesReturned, overlapped: IntPtr.Zero); if (statusCode is ErrorCodes.ERROR_SUCCESS) @@ -300,7 +305,7 @@ internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureColl qualifierSize: 0, output: pBuffer, outputSize: (uint)buffer.Length, - bytesReturned: (IntPtr)bytesReturned, + bytesReturned: bytesReturned, overlapped: IntPtr.Zero); if (statusCode is ErrorCodes.ERROR_SUCCESS) @@ -338,7 +343,7 @@ internal unsafe HTTP_REQUEST_PROPERTY_SNI GetClientSni() qualifierSize: 0, pBuffer, (uint)buffer.Length, - bytesReturned: IntPtr.Zero, + bytesReturned: null, IntPtr.Zero); if (statusCode == ErrorCodes.ERROR_SUCCESS) diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs index 8e7edb9bb47d..731ecea05f6e 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs @@ -5,13 +5,14 @@ using System.Collections.ObjectModel; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; +using static Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions; namespace Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; internal sealed partial class TlsListener : IDisposable { private readonly ConcurrentDictionary _connectionTimestamps = new(); - private readonly Action> _tlsClientHelloBytesCallback; + private readonly TlsClientHelloCallback _tlsClientHelloBytesCallback; private readonly ILogger _logger; private readonly PeriodicTimer _cleanupTimer; @@ -25,7 +26,7 @@ internal sealed partial class TlsListener : IDisposable // Internal for testing purposes internal ReadOnlyDictionary ConnectionTimeStamps => _connectionTimestamps.AsReadOnly(); - internal TlsListener(ILogger logger, Action> tlsClientHelloBytesCallback, TimeProvider? timeProvider = null) + internal TlsListener(ILogger logger, TlsClientHelloCallback tlsClientHelloBytesCallback, TimeProvider? timeProvider = null) { if (AppContext.GetData("Microsoft.AspNetCore.Server.HttpSys.TlsListener.CacheSizeLimit") is int limit) { @@ -52,7 +53,7 @@ internal TlsListener(ILogger logger, Action>, bool> invokeTlsClientHelloCallback) + Func invokeTlsClientHelloCallback) { if (!_connectionTimestamps.TryAdd(connectionId, _timeProvider.GetUtcNow())) { diff --git a/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs b/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs index d0ff2731a017..00c3ac024d32 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Time.Testing; using Moq; +using static Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions; namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests; @@ -54,7 +55,7 @@ public async Task RemovesIdleConnections() var features = Mock.Of(); - bool InvokeCallback(IFeatureCollection f, Action> cb) + bool InvokeCallback(IFeatureCollection f, TlsClientHelloCallback cb) { cb(f, ReadOnlySpan.Empty); return true; diff --git a/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs b/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs index 7fed60b434b1..9fce59c69c36 100644 --- a/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs +++ b/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs @@ -28,6 +28,7 @@ internal static class ErrorCodes internal const uint ERROR_HANDLE_EOF = 38; internal const uint ERROR_NOT_SUPPORTED = 50; internal const uint ERROR_INVALID_PARAMETER = 87; + internal const uint ERROR_INSUFFICIENT_BUFFER = 122; internal const uint ERROR_INVALID_NAME = 123; internal const uint ERROR_ALREADY_EXISTS = 183; internal const uint ERROR_MORE_DATA = 234; From 909128b36a1ea8608f14f53fce7171d203cd435b Mon Sep 17 00:00:00 2001 From: Brennan Date: Mon, 14 Apr 2025 19:43:01 -0700 Subject: [PATCH 24/24] Update src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs Co-authored-by: Aditya Mandaleeka --- src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index dfc69e8882da..5c45db813880 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -240,7 +240,7 @@ internal void ForceCancelRequest() } /// - /// Attempts to get the client hello message bytes from the http.sys. + /// Attempts to get the client hello message bytes from HTTP.sys and calls the user provided callback. /// If not successful, will return false. /// internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureCollection features, TlsClientHelloCallback tlsClientHelloBytesCallback)