From 25aac493db2e7dbfb18ecab2af46f7ab12cd836a Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Sat, 7 Jun 2025 12:29:58 +0200 Subject: [PATCH 1/5] feat(HTTP.SYS): on-demand TLS client hello retrieval (#62209) --- .../samples/TlsFeaturesObserve/Program.cs | 39 +++++-------- .../samples/TlsFeaturesObserve/Startup.cs | 28 ---------- .../TlsFeaturesObserve.csproj | 1 + src/Servers/HttpSys/src/HttpSysListener.cs | 9 --- .../src/IHttpSysRequestPropertyFeature.cs | 36 ++++++++++++ .../HttpSys/src/RequestProcessing/Request.cs | 1 - .../RequestContext.FeatureCollection.cs | 6 ++ .../src/RequestProcessing/RequestContext.cs | 55 ++++++++++++++++++- .../RequestProcessing/RequestContextOfT.cs | 6 -- .../src/RequestProcessing/TlsListener.Log.cs | 15 ----- .../HttpSys/src/StandardFeatureCollection.cs | 1 + .../test/FunctionalTests/HttpsTests.cs | 22 ++++++++ 12 files changed, 133 insertions(+), 86 deletions(-) delete mode 100644 src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs create mode 100644 src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs delete mode 100644 src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs index 9551965ac398..d11d9938eef7 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs @@ -1,27 +1,21 @@ // 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.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.HttpSys; using Microsoft.Extensions.Hosting; -using TlsFeatureObserve; using TlsFeaturesObserve.HttpSys; HttpSysConfigurator.ConfigureCacheTlsClientHello(); -CreateHostBuilder(args).Build().Run(); -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 +var builder = WebApplication.CreateBuilder(args); options.Authentication.Schemes = AuthenticationSchemes.None; options.Authentication.AllowAnonymous = true; @@ -50,20 +44,13 @@ public static void ProcessTlsClientHello(IFeatureCollection features, ReadOnlySp } } -public interface IMyTlsFeature -{ - string ConnectionId { get; } - int TlsClientHelloLength { get; } -} + // rent with enough memory span and invoke + var bytes = ArrayPool.Shared.Rent(bytesReturned); + success = httpSysPropFeature.TryGetTlsClientHello(bytes, out _); + Debug.Assert(success); -public class MyTlsFeature : IMyTlsFeature -{ - public string ConnectionId { get; } - public int TlsClientHelloLength { get; } + await context.Response.WriteAsync($"[Response] connectionId={connectionFeature.ConnectionId}; tlsClientHello.length={bytesReturned}; tlsclienthello start={string.Join(' ', bytes.AsSpan(0, 30).ToArray())}"); + await next(context); +}); - public MyTlsFeature(string connectionId, int tlsClientHelloLength) - { - ConnectionId = connectionId; - TlsClientHelloLength = tlsClientHelloLength; - } -} +app.Run(); diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs deleted file mode 100644 index 8ba6d27aef98..000000000000 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs +++ /dev/null @@ -1,28 +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 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 tlsFeature = context.Features.Get(); - await context.Response.WriteAsync("TlsClientHello data: " + $"connectionId={tlsFeature?.ConnectionId}; length={tlsFeature?.TlsClientHelloLength}"); - }); - } -} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj b/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj index f65f8a98a72a..3ba4390b4e73 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 7fecff3c848d..60e1b173ff62 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -38,7 +38,6 @@ 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; @@ -73,12 +72,7 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) _serverSession = new ServerSession(); _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(Logger, options.TlsClientHelloBytesCallback); - } } catch (Exception exception) { @@ -86,7 +80,6 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) _requestQueue?.Dispose(); _urlGroup?.Dispose(); _serverSession?.Dispose(); - _tlsListener?.Dispose(); Log.HttpSysListenerCtorError(Logger, exception); throw; } @@ -103,7 +96,6 @@ internal enum State internal UrlGroup UrlGroup => _urlGroup; internal RequestQueue RequestQueue => _requestQueue; - internal TlsListener? TlsListener => _tlsListener; internal DisconnectListener DisconnectListener => _disconnectListener; public HttpSysOptions Options { get; } @@ -257,7 +249,6 @@ private void DisposeInternal() Debug.Assert(!_serverSession.Id.IsInvalid, "ServerSessionHandle is invalid in CloseV2Config"); _serverSession.Dispose(); - _tlsListener?.Dispose(); } /// diff --git a/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs b/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs new file mode 100644 index 000000000000..392e11cb73e6 --- /dev/null +++ b/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Server.HttpSys; + +/// +/// Provides API to read HTTP_REQUEST_PROPERTY value from the HTTP.SYS request. +/// +/// +public interface IHttpSysRequestPropertyFeature +{ + /// + /// Reads the TLS client hello from HTTP.SYS + /// + /// Where the raw bytes of the TLS Client Hello message are written. + /// + /// Returns the number of bytes written to . + /// Or can return the size of the buffer needed if wasn't large enough. + /// + /// + /// Works only if HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO flag is set on http.sys service configuration. + /// See + /// and + ///

+ /// If you don't want to guess the required size before first invocation, + /// you should first call with set to empty size, so that you can retrieve the required buffer size from , + /// then allocate that amount of memory and retry the query. + ///
+ /// + /// True, if fetching TLS client hello was successful, false if size is not large enough. + /// If unsuccessful for other reason throws an exception. + /// + /// Any HttpSys error except for ERROR_INSUFFICIENT_BUFFER or ERROR_MORE_DATA. + /// If HttpSys does not support querying the TLS Client Hello. + bool TryGetTlsClientHello(Span tlsClientHelloBytesDestination, out int bytesReturned); +} diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 9aa93adb508d..338f14832e62 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -9,7 +9,6 @@ using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs index e1931dc0fc6b..22e333074c78 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs @@ -36,6 +36,7 @@ internal partial class RequestContext : IHttpResponseTrailersFeature, IHttpResetFeature, IHttpSysRequestDelegationFeature, + IHttpSysRequestPropertyFeature, IConnectionLifetimeNotificationFeature { private IFeatureCollection? _features; @@ -751,4 +752,9 @@ void IConnectionLifetimeNotificationFeature.RequestClose() Response.Headers[HeaderNames.Connection] = "close"; } } + + public bool TryGetTlsClientHello(Span tlsClientHelloBytesDestination, out int bytesReturned) + { + return TryGetTlsClientHelloMessageBytes(tlsClientHelloBytesDestination, out bytesReturned); + } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index 5c45db813880..bb7e961f0ef2 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -7,7 +7,6 @@ using System.Security.Authentication.ExtendedProtection; using System.Security.Principal; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; using static Microsoft.AspNetCore.HttpSys.Internal.HttpApiTypes; @@ -239,6 +238,60 @@ internal void ForceCancelRequest() } } + /// + /// Attempts to get the client hello message bytes from the http.sys. + /// If successful writes the bytes into , and shows how many bytes were written in . + /// If not successful because is not large enough, returns false and shows a size of required in . + /// If not successful for other reason - throws exception with message/errorCode. + /// + internal unsafe bool TryGetTlsClientHelloMessageBytes( + Span destination, + out int bytesReturned) + { + bytesReturned = default; + if (!HttpApi.SupportsClientHello) + { + // not supported, so we just return and don't invoke the callback + throw new InvalidOperationException("Windows HTTP Server API does not support HTTP_FEATURE_ID.HttpFeatureCacheTlsClientHello or HttpQueryRequestProperty. See HTTP_FEATURE_ID for details."); + } + + uint statusCode; + var requestId = PinsReleased ? Request.RequestId : RequestId; + + uint bytesReturnedValue = 0; + uint* bytesReturnedPointer = &bytesReturnedValue; + + fixed (byte* pBuffer = destination) + { + statusCode = HttpApi.HttpGetRequestProperty( + requestQueueHandle: Server.RequestQueue.Handle, + requestId, + propertyId: (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, + qualifier: null, + qualifierSize: 0, + output: pBuffer, + outputSize: (uint)destination.Length, + bytesReturned: (IntPtr)bytesReturnedPointer, + overlapped: IntPtr.Zero); + + bytesReturned = checked((int)bytesReturnedValue); + + if (statusCode is ErrorCodes.ERROR_SUCCESS) + { + return true; + } + + // if buffer supplied is too small, `bytesReturned` has proper size + if (statusCode is ErrorCodes.ERROR_MORE_DATA or ErrorCodes.ERROR_INSUFFICIENT_BUFFER) + { + return false; + } + } + + Log.TlsClientHelloRetrieveError(Logger, requestId, statusCode); + throw new HttpSysException((int)statusCode); + } + /// /// Attempts to get the client hello message bytes from HTTP.sys and calls the user provided callback. /// If not successful, will return false. diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs index 399f1292d60d..2a1d06a06d26 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs @@ -48,12 +48,6 @@ public override async Task ExecuteAsync() context = application.CreateContext(Features); try { - if (Server.Options.TlsClientHelloBytesCallback is not null && Server.TlsListener is not null - && Request.IsHttps) - { - Server.TlsListener.InvokeTlsClientHelloCallback(Request.RawConnectionId, Features, Request.GetAndInvokeTlsClientHelloCallback); - } - await application.ProcessRequestAsync(context); await CompleteAsync(); } diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs deleted file mode 100644 index 20ffe5c74b6f..000000000000 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs +++ /dev/null @@ -1,15 +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 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/StandardFeatureCollection.cs b/src/Servers/HttpSys/src/StandardFeatureCollection.cs index dda57166921e..1c7d078d8253 100644 --- a/src/Servers/HttpSys/src/StandardFeatureCollection.cs +++ b/src/Servers/HttpSys/src/StandardFeatureCollection.cs @@ -27,6 +27,7 @@ internal sealed class StandardFeatureCollection : IFeatureCollection { typeof(IHttpBodyControlFeature), _identityFunc }, { typeof(IHttpSysRequestInfoFeature), _identityFunc }, { typeof(IHttpSysRequestTimingFeature), _identityFunc }, + { typeof(IHttpSysRequestPropertyFeature), _identityFunc }, { typeof(IHttpResponseTrailersFeature), ctx => ctx.GetResponseTrailersFeature() }, { typeof(IHttpResetFeature), ctx => ctx.GetResetFeature() }, { typeof(IConnectionLifetimeNotificationFeature), ctx => ctx.GetConnectionLifetimeNotificationFeature() }, diff --git a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs index 0ccb71964b74..91af51fd8b56 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs @@ -234,6 +234,28 @@ public async Task Https_ITlsHandshakeFeature_MatchesIHttpSysExtensionInfoFeature } } + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)] + public async Task Https_SetsIHttpSysRequestPropertyFeature() + { + using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext => + { + try + { + var requestPropertyFeature = httpContext.Features.Get(); + Assert.NotNull(requestPropertyFeature); + } + catch (Exception ex) + { + await httpContext.Response.WriteAsync(ex.ToString()); + } + }, LoggerFactory)) + { + string response = await SendRequestAsync(address); + Assert.Equal(string.Empty, response); + } + } + [ConditionalFact] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)] public async Task Https_SetsIHttpSysRequestTimingFeature() From 355240179c05256994ea84f11ce1624a31f8a2be Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 9 Jun 2025 17:21:01 -0700 Subject: [PATCH 2/5] fix cherry-pick --- .../samples/TlsFeaturesObserve/Program.cs | 39 ++++++++++++------- .../samples/TlsFeaturesObserve/Startup.cs | 28 +++++++++++++ .../TlsFeaturesObserve.csproj | 1 - src/Servers/HttpSys/src/HttpSysListener.cs | 9 +++++ .../HttpSys/src/RequestProcessing/Request.cs | 1 + .../src/RequestProcessing/RequestContext.cs | 3 +- .../RequestProcessing/RequestContextOfT.cs | 6 +++ .../src/RequestProcessing/TlsListener.Log.cs | 15 +++++++ 8 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs create mode 100644 src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs index d11d9938eef7..9551965ac398 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs @@ -1,21 +1,27 @@ // 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.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.HttpSys; using Microsoft.Extensions.Hosting; +using TlsFeatureObserve; using TlsFeaturesObserve.HttpSys; HttpSysConfigurator.ConfigureCacheTlsClientHello(); +CreateHostBuilder(args).Build().Run(); -var builder = WebApplication.CreateBuilder(args); +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; @@ -44,13 +50,20 @@ public static void ProcessTlsClientHello(IFeatureCollection features, ReadOnlySp } } - // rent with enough memory span and invoke - var bytes = ArrayPool.Shared.Rent(bytesReturned); - success = httpSysPropFeature.TryGetTlsClientHello(bytes, out _); - Debug.Assert(success); +public interface IMyTlsFeature +{ + string ConnectionId { get; } + int TlsClientHelloLength { get; } +} - await context.Response.WriteAsync($"[Response] connectionId={connectionFeature.ConnectionId}; tlsClientHello.length={bytesReturned}; tlsclienthello start={string.Join(' ', bytes.AsSpan(0, 30).ToArray())}"); - await next(context); -}); +public class MyTlsFeature : IMyTlsFeature +{ + public string ConnectionId { get; } + public int TlsClientHelloLength { get; } -app.Run(); + 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 new file mode 100644 index 000000000000..8ba6d27aef98 --- /dev/null +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs @@ -0,0 +1,28 @@ +// 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 tlsFeature = context.Features.Get(); + await context.Response.WriteAsync("TlsClientHello data: " + $"connectionId={tlsFeature?.ConnectionId}; length={tlsFeature?.TlsClientHelloLength}"); + }); + } +} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj b/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj index 3ba4390b4e73..f65f8a98a72a 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj @@ -7,7 +7,6 @@ - diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 60e1b173ff62..7fecff3c848d 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -38,6 +38,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; @@ -72,7 +73,12 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) _serverSession = new ServerSession(); _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(Logger, options.TlsClientHelloBytesCallback); + } } catch (Exception exception) { @@ -80,6 +86,7 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) _requestQueue?.Dispose(); _urlGroup?.Dispose(); _serverSession?.Dispose(); + _tlsListener?.Dispose(); Log.HttpSysListenerCtorError(Logger, exception); throw; } @@ -96,6 +103,7 @@ internal enum State internal UrlGroup UrlGroup => _urlGroup; internal RequestQueue RequestQueue => _requestQueue; + internal TlsListener? TlsListener => _tlsListener; internal DisconnectListener DisconnectListener => _disconnectListener; public HttpSysOptions Options { get; } @@ -249,6 +257,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/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 338f14832e62..9aa93adb508d 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -9,6 +9,7 @@ using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index bb7e961f0ef2..124a9e8dcd02 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -7,6 +7,7 @@ using System.Security.Authentication.ExtendedProtection; using System.Security.Principal; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; using static Microsoft.AspNetCore.HttpSys.Internal.HttpApiTypes; @@ -271,7 +272,7 @@ internal unsafe bool TryGetTlsClientHelloMessageBytes( qualifierSize: 0, output: pBuffer, outputSize: (uint)destination.Length, - bytesReturned: (IntPtr)bytesReturnedPointer, + bytesReturned: bytesReturnedPointer, overlapped: IntPtr.Zero); bytesReturned = checked((int)bytesReturnedValue); diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs index 2a1d06a06d26..399f1292d60d 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs @@ -48,6 +48,12 @@ public override async Task ExecuteAsync() context = application.CreateContext(Features); try { + if (Server.Options.TlsClientHelloBytesCallback is not null && Server.TlsListener is not null + && Request.IsHttps) + { + Server.TlsListener.InvokeTlsClientHelloCallback(Request.RawConnectionId, Features, Request.GetAndInvokeTlsClientHelloCallback); + } + await application.ProcessRequestAsync(context); await CompleteAsync(); } 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..20ffe5c74b6f --- /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); + } +} From d1315f0dc4f6683e2032ea490de27ccd21f4f9b8 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 9 Jun 2025 17:59:29 -0700 Subject: [PATCH 3/5] setup sample --- .../samples/TlsFeaturesObserve/Program.cs | 14 +++++++++----- .../samples/TlsFeaturesObserve/Startup.cs | 18 ++++++++++++++++-- .../src/IHttpSysRequestPropertyFeature.cs | 4 +++- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs index 9551965ac398..13a31cb59b23 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs @@ -26,13 +26,17 @@ static IHostBuilder CreateHostBuilder(string[] args) => options.Authentication.Schemes = AuthenticationSchemes.None; options.Authentication.AllowAnonymous = true; - var property = typeof(HttpSysOptions).GetProperty("TlsClientHelloBytesCallback", BindingFlags.NonPublic | BindingFlags.Instance); - var delegateType = property.PropertyType; // Get the exact delegate type + // If you want to resolve a callback API, uncomment. + // Recommended approach is to use the on-demand API to fetch TLS client hello bytes, + // look into Startup.cs for details. - // Create a delegate of the correct type - var callbackDelegate = Delegate.CreateDelegate(delegateType, typeof(Holder).GetMethod(nameof(Holder.ProcessTlsClientHello), BindingFlags.Static | BindingFlags.Public)); + //var property = typeof(HttpSysOptions).GetProperty("TlsClientHelloBytesCallback", BindingFlags.NonPublic | BindingFlags.Instance); + //var delegateType = property.PropertyType; // Get the exact delegate type - property?.SetValue(options, callbackDelegate); + //// Create a delegate of the correct type + //var callbackDelegate = Delegate.CreateDelegate(delegateType, typeof(Holder).GetMethod(nameof(Holder.ProcessTlsClientHello), BindingFlags.Static | BindingFlags.Public)); + + //property?.SetValue(options, callbackDelegate); }); }); diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs index 8ba6d27aef98..bbf205e711fd 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs @@ -17,12 +17,26 @@ public class Startup { public void Configure(IApplicationBuilder app) { + // recommended approach to fetch TLS client hello bytes + // is via on-demand API per request or by building own connection-lifecycle manager app.Run(async (HttpContext context) => { context.Response.ContentType = "text/plain"; - var tlsFeature = context.Features.Get(); - await context.Response.WriteAsync("TlsClientHello data: " + $"connectionId={tlsFeature?.ConnectionId}; length={tlsFeature?.TlsClientHelloLength}"); + var httpSysAssembly = typeof(Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions).Assembly; + var httpSysPropertyFeatureType = httpSysAssembly.GetType("Microsoft.AspNetCore.Server.HttpSys.IHttpSysRequestPropertyFeature"); + var httpSysPropertyFeature = context.Features[httpSysPropertyFeatureType]!; + + await context.Response.WriteAsync(""); }); + + // middleware compatible with callback API + //app.Run(async (HttpContext context) => + //{ + // context.Response.ContentType = "text/plain"; + + // var tlsFeature = context.Features.Get(); + // await context.Response.WriteAsync("TlsClientHello` data: " + $"connectionId={tlsFeature?.ConnectionId}; length={tlsFeature?.TlsClientHelloLength}"); + //}); } } diff --git a/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs b/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs index 392e11cb73e6..e24e16bf298f 100644 --- a/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs +++ b/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs @@ -6,8 +6,10 @@ namespace Microsoft.AspNetCore.Server.HttpSys; /// /// Provides API to read HTTP_REQUEST_PROPERTY value from the HTTP.SYS request. /// +///
+/// internal for backport ///
-public interface IHttpSysRequestPropertyFeature +internal interface IHttpSysRequestPropertyFeature { /// /// Reads the TLS client hello from HTTP.SYS From b6a8d2e2a7eac26ba5d5c2606bdadcccca25a70c Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 9 Jun 2025 19:05:10 -0700 Subject: [PATCH 4/5] provide example --- .../samples/TlsFeaturesObserve/Startup.cs | 24 ++++++++++++++++++- .../TlsFeaturesObserve.csproj | 1 + .../src/IHttpSysRequestPropertyFeature.cs | 6 ++--- .../RequestContext.FeatureCollection.cs | 4 ++-- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs index bbf205e711fd..90bc6643660a 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; +using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting; @@ -27,7 +29,27 @@ public void Configure(IApplicationBuilder app) var httpSysPropertyFeatureType = httpSysAssembly.GetType("Microsoft.AspNetCore.Server.HttpSys.IHttpSysRequestPropertyFeature"); var httpSysPropertyFeature = context.Features[httpSysPropertyFeatureType]!; - await context.Response.WriteAsync(""); + var method = httpSysPropertyFeature.GetType().GetMethod( + "TryGetTlsClientHello", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic + ); + + // invoke first time to get required size + byte[] bytes = Array.Empty(); + var parameters = new object[] { bytes, 0 }; + var res = (bool)method.Invoke(httpSysPropertyFeature, parameters); + + // fetching out parameter only works by looking into parameters array of objects + var bytesReturned = (int)parameters[1]; + bytes = ArrayPool.Shared.Rent(bytesReturned); + parameters = [bytes, 0]; // correct input now + res = (bool)method.Invoke(httpSysPropertyFeature, parameters); + + // this is the span representing the TLS Client Hello bytes only + var tlsClientHelloBytes = ((byte[])parameters[0]).AsSpan(0, bytesReturned); + await context.Response.WriteAsync($"TlsBytes: {string.Join(" ", tlsClientHelloBytes.Slice(0, 10).ToArray())}; full length = {bytesReturned}"); + + ArrayPool.Shared.Return(bytes); }); // middleware compatible with callback API diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj b/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj index f65f8a98a72a..57b6cef72608 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj @@ -4,6 +4,7 @@ $(DefaultNetCoreTargetFramework) Exe true + latest diff --git a/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs b/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs index e24e16bf298f..16a20abaea8d 100644 --- a/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs +++ b/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs @@ -6,9 +6,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys; /// /// Provides API to read HTTP_REQUEST_PROPERTY value from the HTTP.SYS request. /// -///
-/// internal for backport ///
+// internal for backport internal interface IHttpSysRequestPropertyFeature { /// @@ -34,5 +33,6 @@ internal interface IHttpSysRequestPropertyFeature /// /// Any HttpSys error except for ERROR_INSUFFICIENT_BUFFER or ERROR_MORE_DATA. /// If HttpSys does not support querying the TLS Client Hello. - bool TryGetTlsClientHello(Span tlsClientHelloBytesDestination, out int bytesReturned); + // has byte[] (not Span) for reflection-based invocation + bool TryGetTlsClientHello(byte[] tlsClientHelloBytesDestination, out int bytesReturned); } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs index 22e333074c78..337afcb09451 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs @@ -753,8 +753,8 @@ void IConnectionLifetimeNotificationFeature.RequestClose() } } - public bool TryGetTlsClientHello(Span tlsClientHelloBytesDestination, out int bytesReturned) + public bool TryGetTlsClientHello(byte[] tlsClientHelloBytesDestination, out int bytesReturned) { - return TryGetTlsClientHelloMessageBytes(tlsClientHelloBytesDestination, out bytesReturned); + return TryGetTlsClientHelloMessageBytes(tlsClientHelloBytesDestination.AsSpan(), out bytesReturned); } } From e9164eac9d7f35a7b621a180789cde159bb67e5f Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Tue, 10 Jun 2025 08:48:20 -0700 Subject: [PATCH 5/5] fix build error --- .../HttpSys/samples/TlsFeaturesObserve/Startup.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs index 90bc6643660a..4440149c3552 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs @@ -45,13 +45,19 @@ public void Configure(IApplicationBuilder app) parameters = [bytes, 0]; // correct input now res = (bool)method.Invoke(httpSysPropertyFeature, parameters); - // this is the span representing the TLS Client Hello bytes only - var tlsClientHelloBytes = ((byte[])parameters[0]).AsSpan(0, bytesReturned); - await context.Response.WriteAsync($"TlsBytes: {string.Join(" ", tlsClientHelloBytes.Slice(0, 10).ToArray())}; full length = {bytesReturned}"); - + // to avoid CS4012 use a method which accepts a byte[] and length, where you can do Span slicing + // error CS4012: Parameters or locals of type 'Span' cannot be declared in async methods or async lambda expressions. + var message = ReadTlsClientHello(bytes, bytesReturned); + await context.Response.WriteAsync(message); ArrayPool.Shared.Return(bytes); }); + static string ReadTlsClientHello(byte[] bytes, int bytesReturned) + { + var tlsClientHelloBytes = bytes.AsSpan(0, bytesReturned); + return $"TlsClientHello bytes: {string.Join(" ", tlsClientHelloBytes.ToArray())}, length={bytesReturned}"; + } + // middleware compatible with callback API //app.Run(async (HttpContext context) => //{