From d7953f614e66c90b83b517f0a0416825ff0e4aa8 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/ErrorCodes.cs | 1 + .../HttpSys/src/NativeInterop/HttpApi.cs | 59 +++++++-- .../HttpSys/src/RequestProcessing/Request.cs | 9 ++ .../RequestContext.FeatureCollection.cs | 12 ++ .../RequestProcessing/RequestContext.Log.cs | 6 + .../src/RequestProcessing/RequestContext.cs | 108 +++++++++++++++- .../HttpSys/src/StandardFeatureCollection.cs | 1 + 18 files changed, 559 insertions(+), 18 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 09105a846cb0..42203b549e91 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1772,6 +1772,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.R EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.ValidationsGenerator", "src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.ValidationsGenerator\Microsoft.AspNetCore.Http.ValidationsGenerator.csproj", "{7899F5DD-AA7C-4561-BAC4-E2EC78B7D157}" 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 @@ -10983,6 +10985,22 @@ Global {7899F5DD-AA7C-4561-BAC4-E2EC78B7D157}.Release|x64.Build.0 = Release|Any CPU {7899F5DD-AA7C-4561-BAC4-E2EC78B7D157}.Release|x86.ActiveCfg = Release|Any CPU {7899F5DD-AA7C-4561-BAC4-E2EC78B7D157}.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 @@ -11777,6 +11795,7 @@ Global {01A75167-DF5A-AF38-8700-C3FBB2C2CFF5} = {225AEDCF-7162-4A86-AC74-06B84660B379} {E6D564C0-4CA5-411C-BF40-9802AF7900CB} = {01A75167-DF5A-AF38-8700-C3FBB2C2CFF5} {7899F5DD-AA7C-4561-BAC4-E2EC78B7D157} = {01A75167-DF5A-AF38-8700-C3FBB2C2CFF5} + {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 492b2dfc1077..2a5c9dcf692a 100644 --- a/src/Servers/HttpSys/HttpSysServer.slnf +++ b/src/Servers/HttpSys/HttpSysServer.slnf @@ -38,6 +38,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", @@ -54,4 +55,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/ErrorCodes.cs b/src/Servers/HttpSys/src/NativeInterop/ErrorCodes.cs index d66397390430..b8263ef12a1a 100644 --- a/src/Servers/HttpSys/src/NativeInterop/ErrorCodes.cs +++ b/src/Servers/HttpSys/src/NativeInterop/ErrorCodes.cs @@ -13,6 +13,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; diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index 1fec2ffea7e6..460d5570d75d 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -13,6 +13,9 @@ internal static partial class HttpApi private const string api_ms_win_core_io_LIB = "api-ms-win-core-io-l1-1-0.dll"; private const string HTTPAPI = "httpapi.dll"; + [LibraryImport(HTTPAPI, SetLastError = true)] + internal static partial uint HttpSetServiceConfiguration(IntPtr ServiceHandle, ulong configId, IntPtr pConfigInformation, ulong configInformationLength, IntPtr overlapped); + [LibraryImport(HTTPAPI, SetLastError = true)] internal static partial uint HttpReceiveRequestEntityBody(SafeHandle requestQueueHandle, ulong requestId, uint flags, IntPtr pEntityBuffer, uint entityBufferLength, out uint bytesReturned, SafeNativeOverlapped pOverlapped); @@ -34,21 +37,57 @@ internal static partial class HttpApi [LibraryImport(api_ms_win_core_io_LIB, SetLastError = true)] internal static partial uint CancelIoEx(SafeHandle handle, SafeNativeOverlapped overlapped); - internal unsafe delegate uint HttpGetRequestPropertyInvoker(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, - void* qualifier, uint qualifierSize, void* output, uint outputSize, uint* bytesReturned, IntPtr overlapped); + internal unsafe delegate uint HttpGetRequestPropertyInvoker(SafeHandle requestQueueHandle, ulong requestId, uint propertyId, + void* qualifier, uint qualifierSize, void* output, uint outputSize, IntPtr bytesReturned, IntPtr overlapped); - internal unsafe delegate uint HttpSetRequestPropertyInvoker(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, void* input, uint inputSize, IntPtr overlapped); + internal unsafe delegate uint HttpSetRequestPropertyInvoker(SafeHandle requestQueueHandle, ulong requestId, int propertyId, + void* input, uint inputSize, IntPtr overlapped); // HTTP_PROPERTY_FLAGS.Present (1) internal static HTTP_PROPERTY_FLAGS HTTP_PROPERTY_FLAGS_PRESENT { get; } = new() { _bitfield = 0x00000001 }; // This property is used by HttpListener to pass the version structure to the native layer in API calls. internal static HTTPAPI_VERSION Version { get; } = new () { HttpApiMajorVersion = 2 }; internal static SafeLibraryHandle? HttpApiModule { get; } - internal static HttpGetRequestPropertyInvoker? HttpGetRequestProperty { get; } - internal static HttpSetRequestPropertyInvoker? HttpSetRequestProperty { get; } - [MemberNotNullWhen(true, nameof(HttpSetRequestProperty))] + + private static HttpGetRequestPropertyInvoker? HttpGetRequestInvoker { get; } + private static HttpSetRequestPropertyInvoker? HttpSetRequestInvoker { get; } + + 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, IntPtr bytesReturned, IntPtr overlapped) + => HttpGetRequestProperty(requestQueueHandle, requestId, (uint)propertyId, qualifier, qualifierSize, output, outputSize, bytesReturned, overlapped); + + internal static unsafe uint HttpGetRequestProperty(SafeHandle requestQueueHandle, ulong requestId, uint propertyId, + void* qualifier, uint qualifierSize, void* output, uint outputSize, IntPtr bytesReturned, IntPtr overlapped) + { + if (!HttpGetRequestPropertySupported) + { + return default; + } + + 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) + => HttpSetRequestProperty(requestQueueHandle, requestId, (int)propertyId, input, inputSize, overlapped); + + internal static unsafe uint HttpSetRequestProperty(SafeHandle requestQueueHandle, ulong requestId, int propertyId, + void* input, uint inputSize, IntPtr overlapped) + { + if (!HttpSetRequestPropertySupported) + { + return default; + } + + return HttpSetRequestInvoker!(requestQueueHandle, requestId, propertyId, input, inputSize, overlapped); + } + + [MemberNotNullWhen(true, nameof(HttpSetRequestInvoker))] internal static bool SupportsTrailers { get; } - [MemberNotNullWhen(true, nameof(HttpSetRequestProperty))] + [MemberNotNullWhen(true, nameof(HttpSetRequestInvoker))] internal static bool SupportsReset { get; } internal static bool SupportsDelegation { get; } internal static bool Supported { get; } @@ -61,9 +100,9 @@ static unsafe HttpApi() { Supported = true; 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 b32a973a5fb5..b86063091a13 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.AspNetCore.Shared; @@ -174,6 +175,7 @@ internal Request(RequestContext requestContext) if (IsHttps) { GetTlsHandshakeResults(); + ParseTlsClientHello(); } // GetTlsTokenBindingInfo(); TODO: https://github.com/aspnet/HttpSysServer/issues/231 @@ -337,6 +339,8 @@ private AspNetCore.HttpSys.Internal.SocketAddress LocalEndPoint public SslProtocols Protocol { get; private set; } + public TLS_CLIENT_HELLO TlsClientHelloMessage { get; private set; } + [Obsolete(Obsoletions.RuntimeTlsCipherAlgorithmEnumsMessage, DiagnosticId = Obsoletions.RuntimeTlsCipherAlgorithmEnumsDiagId, UrlFormat = Obsoletions.RuntimeSharedUrlFormat)] public CipherAlgorithmType CipherAlgorithm { get; private set; } @@ -372,6 +376,11 @@ private void GetTlsHandshakeResults() SniHostName = sni.Hostname.ToString(); } + 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 2751a1025a89..001b680c2b21 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) @@ -592,6 +599,11 @@ bool IHttpBodyControlFeature.AllowSynchronousIO SslProtocols ITlsHandshakeFeature.Protocol => Request.Protocol; + TLS_CLIENT_HELLO ITlsFingerprintingFeature.GetTlsClientHello() + { + return Request.TlsClientHelloMessage; + } + #pragma warning disable SYSLIB0058 // Type or member is obsolete CipherAlgorithmType ITlsHandshakeFeature.CipherAlgorithm => Request.CipherAlgorithm; 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 cd39566c5266..ac291acbcc51 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -1,8 +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.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.AspNetCore.WebUtilities; @@ -219,28 +222,119 @@ internal void ForceCancelRequest() } } - internal unsafe 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, - 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 is ErrorCodes.ERROR_MORE_DATA or ErrorCodes.ERROR_INSUFFICIENT_BUFFER) + { + // 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) { - return Marshal.PtrToStructure((IntPtr)pBuffer); + 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 aa3ae320771ef8a24c1f9858aae55d7aa4121fcd 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 | 7 +- .../RequestContext.FeatureCollection.cs | 9 +- .../RequestProcessing/RequestContext.Log.cs | 3 - .../src/RequestProcessing/RequestContext.cs | 109 ++++++++---------- .../HttpSys/src/StandardFeatureCollection.cs | 2 +- 7 files changed, 63 insertions(+), 83 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 b86063091a13..6746ee9150ac 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -339,7 +339,10 @@ private AspNetCore.HttpSys.Internal.SocketAddress LocalEndPoint public SslProtocols Protocol { get; private set; } - public TLS_CLIENT_HELLO TlsClientHelloMessage { get; private set; } + /// + /// Raw bytes of TLS client hello message + /// + public byte[]? TlsClientHelloMessageBytes { get; private set; } [Obsolete(Obsoletions.RuntimeTlsCipherAlgorithmEnumsMessage, DiagnosticId = Obsoletions.RuntimeTlsCipherAlgorithmEnumsDiagId, UrlFormat = Obsoletions.RuntimeSharedUrlFormat)] public CipherAlgorithmType CipherAlgorithm { get; private set; } @@ -378,7 +381,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 001b680c2b21..58376a9c8672 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; } @@ -599,9 +598,9 @@ bool IHttpBodyControlFeature.AllowSynchronousIO SslProtocols ITlsHandshakeFeature.Protocol => Request.Protocol; - TLS_CLIENT_HELLO ITlsFingerprintingFeature.GetTlsClientHello() + byte[]? ITlsAccessFeature.GetTlsClientHelloMessageBytes() { - return Request.TlsClientHelloMessage; + return Request.TlsClientHelloMessageBytes; } #pragma warning disable SYSLIB0058 // Type or member is obsolete 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 ac291acbcc51..4eed35b3a23f 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.AspNetCore.WebUtilities; @@ -222,7 +220,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) { @@ -231,84 +233,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 840cdac8ca0e180782bc875a0d505bd1a46ab057 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 c9431ece6ab58797113e37cf72ccb7bfabc669fa 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 6746ee9150ac..fc498f4d1661 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.AspNetCore.Shared; From 1d0f0d89bbbdce32362f818606f14ab37bc99f21 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 | 23 ++++------- src/Servers/HttpSys/src/HttpSysOptions.cs | 6 +++ .../src/NativeInterop/DisconnectListener.cs | 23 +++++++++-- .../HttpSys/src/RequestProcessing/Request.cs | 13 ++----- .../RequestContext.FeatureCollection.cs | 11 ------ .../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, 136 insertions(+), 83 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 933c627cd56e..b84a26c2312f 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -5,6 +5,7 @@ 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; using Windows.Win32; @@ -41,6 +42,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; @@ -76,7 +78,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) { @@ -98,20 +101,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 44bc0bc5faa8..2bbd926aa229 100644 --- a/src/Servers/HttpSys/src/HttpSysOptions.cs +++ b/src/Servers/HttpSys/src/HttpSysOptions.cs @@ -241,6 +241,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 f4dc688f3478..d94cbaaefb84 100644 --- a/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs +++ b/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs @@ -14,10 +14,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) @@ -69,7 +73,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(); @@ -99,7 +103,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(); } @@ -108,13 +112,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 fc498f4d1661..295972f037fb 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.AspNetCore.Shared; using Microsoft.Extensions.Logging; @@ -174,7 +175,6 @@ internal Request(RequestContext requestContext) if (IsHttps) { GetTlsHandshakeResults(); - ParseTlsClientHello(); } // GetTlsTokenBindingInfo(); TODO: https://github.com/aspnet/HttpSysServer/issues/231 @@ -338,11 +338,6 @@ private AspNetCore.HttpSys.Internal.SocketAddress LocalEndPoint public SslProtocols Protocol { get; private set; } - /// - /// Raw bytes of TLS client hello message - /// - public byte[]? TlsClientHelloMessageBytes { get; private set; } - [Obsolete(Obsoletions.RuntimeTlsCipherAlgorithmEnumsMessage, DiagnosticId = Obsoletions.RuntimeTlsCipherAlgorithmEnumsDiagId, UrlFormat = Obsoletions.RuntimeSharedUrlFormat)] public CipherAlgorithmType CipherAlgorithm { get; private set; } @@ -378,10 +373,8 @@ private void GetTlsHandshakeResults() SniHostName = sni.Hostname.ToString(); } - 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 58376a9c8672..2751a1025a89 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) @@ -598,11 +592,6 @@ bool IHttpBodyControlFeature.AllowSynchronousIO SslProtocols ITlsHandshakeFeature.Protocol => Request.Protocol; - byte[]? ITlsAccessFeature.GetTlsClientHelloMessageBytes() - { - return Request.TlsClientHelloMessageBytes; - } - #pragma warning disable SYSLIB0058 // Type or member is obsolete CipherAlgorithmType ITlsHandshakeFeature.CipherAlgorithm => Request.CipherAlgorithm; diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index 4eed35b3a23f..b50d97146dc3 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.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; @@ -222,26 +223,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, @@ -249,7 +254,8 @@ internal void ForceCancelRequest() if (statusCode is ErrorCodes.ERROR_SUCCESS) { - return buffer.AsSpan().ToArray(); + tlsClientHelloBytesCallback(features, buffer.AsSpan(0, (int)bytesReturnedValue)); + return true; } } } @@ -270,7 +276,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, @@ -278,7 +284,8 @@ internal void ForceCancelRequest() if (statusCode is ErrorCodes.ERROR_SUCCESS) { - return buffer.AsSpan(0, correctSize).ToArray(); + tlsClientHelloBytesCallback(features, buffer.AsSpan(0, correctSize)); + return true; } } } @@ -289,7 +296,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 882afdcf1e37..06dc3f5b1882 100644 --- a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs +++ b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs @@ -33,9 +33,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; @@ -170,6 +172,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() @@ -179,6 +186,7 @@ internal void ReleasePins() _memoryHandle.Dispose(); _memoryHandle = default; _nativeRequest = null; + _pinsReleased = true; } public bool TryGetTimestamp(HttpSysRequestTimingType timestampType, out long timestamp) From 3c4d6fc453d3ea93d3f6c2d179c38f0aae055427 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 0c31882a5a4b2ac2ba95998f65f88d8d909aeccf 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 5d7d18733c61924034a099101acefef62ca6cecf 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 +- src/Servers/HttpSys/src/NativeInterop/HttpApi.cs | 3 --- .../HttpSys/src/RequestProcessing/RequestContextOfT.cs | 7 +++++-- src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs | 9 +++++---- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index b84a26c2312f..152cc3fb6bbb 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -78,7 +78,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/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index 460d5570d75d..12c36488c46b 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -13,9 +13,6 @@ internal static partial class HttpApi private const string api_ms_win_core_io_LIB = "api-ms-win-core-io-l1-1-0.dll"; private const string HTTPAPI = "httpapi.dll"; - [LibraryImport(HTTPAPI, SetLastError = true)] - internal static partial uint HttpSetServiceConfiguration(IntPtr ServiceHandle, ulong configId, IntPtr pConfigInformation, ulong configInformationLength, IntPtr overlapped); - [LibraryImport(HTTPAPI, SetLastError = true)] internal static partial uint HttpReceiveRequestEntityBody(SafeHandle requestQueueHandle, ulong requestId, uint flags, IntPtr pEntityBuffer, uint entityBufferLength, out uint bytesReturned, SafeNativeOverlapped pOverlapped); 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 8cf4cea8f7461a931225684f02366f9548319c32 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 7 Apr 2025 13:32:10 +0200 Subject: [PATCH 09/24] a bit refactor --- .../HttpSys/src/NativeInterop/HttpApi.cs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index 12c36488c46b..450a08fd250d 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -54,31 +54,25 @@ internal unsafe delegate uint HttpSetRequestPropertyInvoker(SafeHandle requestQu internal static unsafe uint HttpGetRequestProperty(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, void* qualifier, uint qualifierSize, void* output, uint outputSize, IntPtr bytesReturned, IntPtr overlapped) - => HttpGetRequestProperty(requestQueueHandle, requestId, (uint)propertyId, qualifier, qualifierSize, output, outputSize, bytesReturned, overlapped); + { + return HttpGetRequestProperty(requestQueueHandle, requestId, (uint)propertyId, qualifier, qualifierSize, output, outputSize, bytesReturned, overlapped); + } internal static unsafe uint HttpGetRequestProperty(SafeHandle requestQueueHandle, ulong requestId, uint propertyId, void* qualifier, uint qualifierSize, void* output, uint outputSize, IntPtr bytesReturned, IntPtr overlapped) { - if (!HttpGetRequestPropertySupported) - { - return default; - } - 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) - => HttpSetRequestProperty(requestQueueHandle, requestId, (int)propertyId, input, inputSize, overlapped); + { + return HttpSetRequestProperty(requestQueueHandle, requestId, (int)propertyId, input, inputSize, overlapped); + } internal static unsafe uint HttpSetRequestProperty(SafeHandle requestQueueHandle, ulong requestId, int propertyId, void* input, uint inputSize, IntPtr overlapped) { - if (!HttpSetRequestPropertySupported) - { - return default; - } - return HttpSetRequestInvoker!(requestQueueHandle, requestId, propertyId, input, inputSize, overlapped); } From 7566aec2b2d479a027e2624183dd0eda376ab9f4 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 7 Apr 2025 13:36:00 +0200 Subject: [PATCH 10/24] update in slnx ! --- AspNetCore.slnx | 1 + src/Servers/HttpSys/HttpSysServer.slnf | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/AspNetCore.slnx b/AspNetCore.slnx index 76933202a2d8..42988a869787 100644 --- a/AspNetCore.slnx +++ b/AspNetCore.slnx @@ -868,6 +868,7 @@ + diff --git a/src/Servers/HttpSys/HttpSysServer.slnf b/src/Servers/HttpSys/HttpSysServer.slnf index b9171033dd0d..7c081bb272c4 100644 --- a/src/Servers/HttpSys/HttpSysServer.slnf +++ b/src/Servers/HttpSys/HttpSysServer.slnf @@ -55,4 +55,4 @@ "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} +} \ No newline at end of file From 946440a3285ae9ceaade04e6f367513ad9156bcd Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 7 Apr 2025 19:11:48 +0200 Subject: [PATCH 11/24] PR review --- src/Servers/HttpSys/src/HttpSysListener.cs | 18 ++++++++++++------ src/Servers/HttpSys/src/HttpSysOptions.cs | 9 +++++++-- .../src/NativeInterop/DisconnectListener.cs | 6 +++--- .../HttpSys/src/NativeInterop/HttpApi.cs | 16 ++-------------- .../HttpSys/src/PublicAPI.Unshipped.txt | 2 +- .../src/RequestProcessing/RequestContext.cs | 4 ++-- .../src/RequestProcessing/RequestContextOfT.cs | 2 +- 7 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 152cc3fb6bbb..fbe72ae4604f 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -42,7 +42,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; @@ -73,13 +73,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) { @@ -103,7 +109,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 2bbd926aa229..8875319a90c0 100644 --- a/src/Servers/HttpSys/src/HttpSysOptions.cs +++ b/src/Servers/HttpSys/src/HttpSysOptions.cs @@ -237,7 +237,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; } @@ -245,7 +245,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 d94cbaaefb84..c75a52bf46ff 100644 --- a/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs +++ b/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs @@ -14,9 +14,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; @@ -124,7 +124,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/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index 450a08fd250d..2d237ae605a0 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -34,10 +34,10 @@ internal static partial class HttpApi [LibraryImport(api_ms_win_core_io_LIB, SetLastError = true)] internal static partial uint CancelIoEx(SafeHandle handle, SafeNativeOverlapped overlapped); - internal unsafe delegate uint HttpGetRequestPropertyInvoker(SafeHandle requestQueueHandle, ulong requestId, uint propertyId, + internal unsafe delegate uint HttpGetRequestPropertyInvoker(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, void* qualifier, uint qualifierSize, void* output, uint outputSize, IntPtr bytesReturned, IntPtr overlapped); - internal unsafe delegate uint HttpSetRequestPropertyInvoker(SafeHandle requestQueueHandle, ulong requestId, int propertyId, + internal unsafe delegate uint HttpSetRequestPropertyInvoker(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, void* input, uint inputSize, IntPtr overlapped); // HTTP_PROPERTY_FLAGS.Present (1) @@ -54,24 +54,12 @@ internal unsafe delegate uint HttpSetRequestPropertyInvoker(SafeHandle requestQu internal static unsafe uint HttpGetRequestProperty(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, void* qualifier, uint qualifierSize, void* output, uint outputSize, IntPtr bytesReturned, IntPtr overlapped) - { - return HttpGetRequestProperty(requestQueueHandle, requestId, (uint)propertyId, qualifier, qualifierSize, output, outputSize, bytesReturned, overlapped); - } - - internal static unsafe uint HttpGetRequestProperty(SafeHandle requestQueueHandle, ulong requestId, uint propertyId, - void* qualifier, uint qualifierSize, void* output, uint outputSize, IntPtr 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 HttpSetRequestProperty(requestQueueHandle, requestId, (int)propertyId, input, inputSize, overlapped); - } - - internal static unsafe uint HttpSetRequestProperty(SafeHandle requestQueueHandle, ulong requestId, int propertyId, - void* input, uint inputSize, IntPtr overlapped) { return HttpSetRequestInvoker!(requestQueueHandle, requestId, propertyId, input, inputSize, overlapped); } 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 b50d97146dc3..56b9f44a2f83 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -247,7 +247,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); @@ -277,7 +277,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 0ad12caf688131795957a52ca9dd7c1052ec36e2 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Tue, 8 Apr 2025 13:47:48 +0200 Subject: [PATCH 12/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 8875319a90c0..3e83e10212f9 100644 --- a/src/Servers/HttpSys/src/HttpSysOptions.cs +++ b/src/Servers/HttpSys/src/HttpSysOptions.cs @@ -243,7 +243,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 56b9f44a2f83..8b66a811769c 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -246,11 +246,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) { @@ -276,11 +280,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) { @@ -295,7 +303,7 @@ internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureColl } } - Log.TlsClientHelloRetrieveError(Logger, "Status code: " + statusCode); + Log.TlsClientHelloRetrieveError(Logger, statusCode); return false; } From e9c7eade2929b28f6adcc828c620566f51208839 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Tue, 8 Apr 2025 16:18:58 +0200 Subject: [PATCH 13/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 fbe72ae4604f..0f168e53bd2f 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -76,15 +76,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) @@ -93,6 +88,7 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) _requestQueue?.Dispose(); _urlGroup?.Dispose(); _serverSession?.Dispose(); + _tlsListener?.Dispose(); Log.HttpSysListenerCtorError(Logger, exception); throw; } @@ -263,6 +259,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 c75a52bf46ff..f4dc688f3478 100644 --- a/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs +++ b/src/Servers/HttpSys/src/NativeInterop/DisconnectListener.cs @@ -14,14 +14,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) @@ -73,7 +69,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(); @@ -103,7 +99,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(); } @@ -112,24 +108,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 e1704547fa401259c434d7bbe8ee399ff669f70b Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Tue, 8 Apr 2025 22:41:00 +0200 Subject: [PATCH 14/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 b830fca21a1f45a24662b064e917263e48b6191d Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Tue, 8 Apr 2025 23:19:34 +0200 Subject: [PATCH 15/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 0f168e53bd2f..077268edc779 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -74,7 +74,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 e1bdba473dba5ce884d8b5ff1c4bd98b48f818b6 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Wed, 9 Apr 2025 00:26:51 +0200 Subject: [PATCH 16/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 17/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 8b66a811769c..43ac3bf94250 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -303,7 +303,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 23bfa1ba6d5d8c77e28f28d5a84163753adebd2a Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Wed, 9 Apr 2025 00:36:12 +0200 Subject: [PATCH 18/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 c69797feb8ce7701822a2c8850a231bd306f049a Mon Sep 17 00:00:00 2001 From: Brennan Date: Thu, 10 Apr 2025 16:34:53 -0700 Subject: [PATCH 19/24] Cache updates --- .../HttpSys/src/NativeInterop/HttpApi.cs | 2 + .../src/RequestProcessing/RequestContext.cs | 2 +- .../src/RequestProcessing/TlsListener.Log.cs | 2 +- .../src/RequestProcessing/TlsListener.cs | 56 ++++++++++++++++--- 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index 2d237ae605a0..f5dfbc96a6cd 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -69,6 +69,7 @@ internal static unsafe uint HttpSetRequestProperty(SafeHandle requestQueueHandle [MemberNotNullWhen(true, nameof(HttpSetRequestInvoker))] internal static bool SupportsReset { get; } internal static bool SupportsDelegation { get; } + internal static bool SupportsClientHello { get; } internal static bool Supported { get; } static unsafe HttpApi() @@ -84,6 +85,7 @@ static unsafe HttpApi() 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 43ac3bf94250..b2e8419631ad 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -227,7 +227,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 886de4f697768cc31d138c7019553c0811f5836d Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 11 Apr 2025 15:48:15 -0700 Subject: [PATCH 20/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 3fe78717530394cc94708a29b48f3b4fecf9c131 Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 11 Apr 2025 15:49:26 -0700 Subject: [PATCH 21/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 077268edc779..0f168e53bd2f 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -74,7 +74,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 8dc51f8d269f50d4be9e18119ab375dd853c5955 Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 11 Apr 2025 15:53:50 -0700 Subject: [PATCH 22/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 b2e8419631ad..9e34d23f8584 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -265,7 +265,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 @@ -299,7 +299,7 @@ internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureColl } finally { - ArrayPool.Shared.Return(buffer); + ArrayPool.Shared.Return(buffer, clearArray: true); } } From 54a9c75fb1405ffcc8c4daf588407302d3dc7ab9 Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 11 Apr 2025 16:58:32 -0700 Subject: [PATCH 23/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 9eeaee3bb0ecac4af28aefceace7b2139f1d87ca Mon Sep 17 00:00:00 2001 From: Brennan Date: Mon, 14 Apr 2025 11:54:13 -0700 Subject: [PATCH 24/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 {