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..4440149c3552 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; @@ -17,12 +19,52 @@ 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]!; + + 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); + + // 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) => + //{ + // 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..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 new file mode 100644 index 000000000000..16a20abaea8d --- /dev/null +++ b/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.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. + +namespace Microsoft.AspNetCore.Server.HttpSys; + +/// +/// Provides API to read HTTP_REQUEST_PROPERTY value from the HTTP.SYS request. +/// +/// +// internal for backport +internal 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. + // 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 e1931dc0fc6b..337afcb09451 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(byte[] tlsClientHelloBytesDestination, out int bytesReturned) + { + return TryGetTlsClientHelloMessageBytes(tlsClientHelloBytesDestination.AsSpan(), out bytesReturned); + } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index 5c45db813880..124a9e8dcd02 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -239,6 +239,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: 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/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()