Skip to content

Commit 9e8ebbf

Browse files
authored
backport(net8.0): http.sys on-demand TLS client hello retrieval (#62290)
* feat(HTTP.SYS): on-demand TLS client hello retrieval (#62209) * fix cherry-pick * setup sample * provide example * fix build error
1 parent 7dd498b commit 9e8ebbf

File tree

8 files changed

+175
-7
lines changed

8 files changed

+175
-7
lines changed

src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ static IHostBuilder CreateHostBuilder(string[] args) =>
2626
options.Authentication.Schemes = AuthenticationSchemes.None;
2727
options.Authentication.AllowAnonymous = true;
2828

29-
var property = typeof(HttpSysOptions).GetProperty("TlsClientHelloBytesCallback", BindingFlags.NonPublic | BindingFlags.Instance);
30-
var delegateType = property.PropertyType; // Get the exact delegate type
29+
// If you want to resolve a callback API, uncomment.
30+
// Recommended approach is to use the on-demand API to fetch TLS client hello bytes,
31+
// look into Startup.cs for details.
3132

32-
// Create a delegate of the correct type
33-
var callbackDelegate = Delegate.CreateDelegate(delegateType, typeof(Holder).GetMethod(nameof(Holder.ProcessTlsClientHello), BindingFlags.Static | BindingFlags.Public));
33+
//var property = typeof(HttpSysOptions).GetProperty("TlsClientHelloBytesCallback", BindingFlags.NonPublic | BindingFlags.Instance);
34+
//var delegateType = property.PropertyType; // Get the exact delegate type
3435

35-
property?.SetValue(options, callbackDelegate);
36+
//// Create a delegate of the correct type
37+
//var callbackDelegate = Delegate.CreateDelegate(delegateType, typeof(Holder).GetMethod(nameof(Holder.ProcessTlsClientHello), BindingFlags.Static | BindingFlags.Public));
38+
39+
//property?.SetValue(options, callbackDelegate);
3640
});
3741
});
3842

src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Buffers;
6+
using System.Reflection;
57
using Microsoft.AspNetCore.Builder;
68
using Microsoft.AspNetCore.Connections.Features;
79
using Microsoft.AspNetCore.Hosting;
@@ -17,12 +19,52 @@ public class Startup
1719
{
1820
public void Configure(IApplicationBuilder app)
1921
{
22+
// recommended approach to fetch TLS client hello bytes
23+
// is via on-demand API per request or by building own connection-lifecycle manager
2024
app.Run(async (HttpContext context) =>
2125
{
2226
context.Response.ContentType = "text/plain";
2327

24-
var tlsFeature = context.Features.Get<IMyTlsFeature>();
25-
await context.Response.WriteAsync("TlsClientHello data: " + $"connectionId={tlsFeature?.ConnectionId}; length={tlsFeature?.TlsClientHelloLength}");
28+
var httpSysAssembly = typeof(Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions).Assembly;
29+
var httpSysPropertyFeatureType = httpSysAssembly.GetType("Microsoft.AspNetCore.Server.HttpSys.IHttpSysRequestPropertyFeature");
30+
var httpSysPropertyFeature = context.Features[httpSysPropertyFeatureType]!;
31+
32+
var method = httpSysPropertyFeature.GetType().GetMethod(
33+
"TryGetTlsClientHello",
34+
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
35+
);
36+
37+
// invoke first time to get required size
38+
byte[] bytes = Array.Empty<byte>();
39+
var parameters = new object[] { bytes, 0 };
40+
var res = (bool)method.Invoke(httpSysPropertyFeature, parameters);
41+
42+
// fetching out parameter only works by looking into parameters array of objects
43+
var bytesReturned = (int)parameters[1];
44+
bytes = ArrayPool<byte>.Shared.Rent(bytesReturned);
45+
parameters = [bytes, 0]; // correct input now
46+
res = (bool)method.Invoke(httpSysPropertyFeature, parameters);
47+
48+
// to avoid CS4012 use a method which accepts a byte[] and length, where you can do Span<byte> slicing
49+
// error CS4012: Parameters or locals of type 'Span<byte>' cannot be declared in async methods or async lambda expressions.
50+
var message = ReadTlsClientHello(bytes, bytesReturned);
51+
await context.Response.WriteAsync(message);
52+
ArrayPool<byte>.Shared.Return(bytes);
2653
});
54+
55+
static string ReadTlsClientHello(byte[] bytes, int bytesReturned)
56+
{
57+
var tlsClientHelloBytes = bytes.AsSpan(0, bytesReturned);
58+
return $"TlsClientHello bytes: {string.Join(" ", tlsClientHelloBytes.ToArray())}, length={bytesReturned}";
59+
}
60+
61+
// middleware compatible with callback API
62+
//app.Run(async (HttpContext context) =>
63+
//{
64+
// context.Response.ContentType = "text/plain";
65+
66+
// var tlsFeature = context.Features.Get<IMyTlsFeature>();
67+
// await context.Response.WriteAsync("TlsClientHello` data: " + $"connectionId={tlsFeature?.ConnectionId}; length={tlsFeature?.TlsClientHelloLength}");
68+
//});
2769
}
2870
}

src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
55
<OutputType>Exe</OutputType>
66
<ServerGarbageCollection>true</ServerGarbageCollection>
7+
<LangVersion>latest</LangVersion>
78
</PropertyGroup>
89

910
<ItemGroup>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Server.HttpSys;
5+
6+
/// <summary>
7+
/// Provides API to read HTTP_REQUEST_PROPERTY value from the HTTP.SYS request.
8+
/// <see href="https://learn.microsoft.com/windows/win32/api/http/ne-http-http_request_property"/>
9+
/// </summary>
10+
// internal for backport
11+
internal interface IHttpSysRequestPropertyFeature
12+
{
13+
/// <summary>
14+
/// Reads the TLS client hello from HTTP.SYS
15+
/// </summary>
16+
/// <param name="tlsClientHelloBytesDestination">Where the raw bytes of the TLS Client Hello message are written.</param>
17+
/// <param name="bytesReturned">
18+
/// Returns the number of bytes written to <paramref name="tlsClientHelloBytesDestination"/>.
19+
/// Or can return the size of the buffer needed if <paramref name="tlsClientHelloBytesDestination"/> wasn't large enough.
20+
/// </param>
21+
/// <remarks>
22+
/// Works only if <c>HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO</c> flag is set on http.sys service configuration.
23+
/// See <see href="https://learn.microsoft.com/windows/win32/api/http/nf-http-httpsetserviceconfiguration"/>
24+
/// and <see href="https://learn.microsoft.com/windows/win32/api/http/ne-http-http_service_config_id"/>
25+
/// <br/><br/>
26+
/// If you don't want to guess the required <paramref name="tlsClientHelloBytesDestination"/> size before first invocation,
27+
/// you should first call with <paramref name="tlsClientHelloBytesDestination"/> set to empty size, so that you can retrieve the required buffer size from <paramref name="bytesReturned"/>,
28+
/// then allocate that amount of memory and retry the query.
29+
/// </remarks>
30+
/// <returns>
31+
/// True, if fetching TLS client hello was successful, false if <paramref name="tlsClientHelloBytesDestination"/> size is not large enough.
32+
/// If unsuccessful for other reason throws an exception.
33+
/// </returns>
34+
/// <exception cref="HttpSysException">Any HttpSys error except for ERROR_INSUFFICIENT_BUFFER or ERROR_MORE_DATA.</exception>
35+
/// <exception cref="InvalidOperationException">If HttpSys does not support querying the TLS Client Hello.</exception>
36+
// has byte[] (not Span<byte>) for reflection-based invocation
37+
bool TryGetTlsClientHello(byte[] tlsClientHelloBytesDestination, out int bytesReturned);
38+
}

src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ internal partial class RequestContext :
3636
IHttpResponseTrailersFeature,
3737
IHttpResetFeature,
3838
IHttpSysRequestDelegationFeature,
39+
IHttpSysRequestPropertyFeature,
3940
IConnectionLifetimeNotificationFeature
4041
{
4142
private IFeatureCollection? _features;
@@ -751,4 +752,9 @@ void IConnectionLifetimeNotificationFeature.RequestClose()
751752
Response.Headers[HeaderNames.Connection] = "close";
752753
}
753754
}
755+
756+
public bool TryGetTlsClientHello(byte[] tlsClientHelloBytesDestination, out int bytesReturned)
757+
{
758+
return TryGetTlsClientHelloMessageBytes(tlsClientHelloBytesDestination.AsSpan(), out bytesReturned);
759+
}
754760
}

src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,60 @@ internal void ForceCancelRequest()
239239
}
240240
}
241241

242+
/// <summary>
243+
/// Attempts to get the client hello message bytes from the http.sys.
244+
/// If successful writes the bytes into <paramref name="destination"/>, and shows how many bytes were written in <paramref name="bytesReturned"/>.
245+
/// If not successful because <paramref name="destination"/> is not large enough, returns false and shows a size of <paramref name="destination"/> required in <paramref name="bytesReturned"/>.
246+
/// If not successful for other reason - throws exception with message/errorCode.
247+
/// </summary>
248+
internal unsafe bool TryGetTlsClientHelloMessageBytes(
249+
Span<byte> destination,
250+
out int bytesReturned)
251+
{
252+
bytesReturned = default;
253+
if (!HttpApi.SupportsClientHello)
254+
{
255+
// not supported, so we just return and don't invoke the callback
256+
throw new InvalidOperationException("Windows HTTP Server API does not support HTTP_FEATURE_ID.HttpFeatureCacheTlsClientHello or HttpQueryRequestProperty. See HTTP_FEATURE_ID for details.");
257+
}
258+
259+
uint statusCode;
260+
var requestId = PinsReleased ? Request.RequestId : RequestId;
261+
262+
uint bytesReturnedValue = 0;
263+
uint* bytesReturnedPointer = &bytesReturnedValue;
264+
265+
fixed (byte* pBuffer = destination)
266+
{
267+
statusCode = HttpApi.HttpGetRequestProperty(
268+
requestQueueHandle: Server.RequestQueue.Handle,
269+
requestId,
270+
propertyId: (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */,
271+
qualifier: null,
272+
qualifierSize: 0,
273+
output: pBuffer,
274+
outputSize: (uint)destination.Length,
275+
bytesReturned: bytesReturnedPointer,
276+
overlapped: IntPtr.Zero);
277+
278+
bytesReturned = checked((int)bytesReturnedValue);
279+
280+
if (statusCode is ErrorCodes.ERROR_SUCCESS)
281+
{
282+
return true;
283+
}
284+
285+
// if buffer supplied is too small, `bytesReturned` has proper size
286+
if (statusCode is ErrorCodes.ERROR_MORE_DATA or ErrorCodes.ERROR_INSUFFICIENT_BUFFER)
287+
{
288+
return false;
289+
}
290+
}
291+
292+
Log.TlsClientHelloRetrieveError(Logger, requestId, statusCode);
293+
throw new HttpSysException((int)statusCode);
294+
}
295+
242296
/// <summary>
243297
/// Attempts to get the client hello message bytes from HTTP.sys and calls the user provided callback.
244298
/// If not successful, will return false.

src/Servers/HttpSys/src/StandardFeatureCollection.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ internal sealed class StandardFeatureCollection : IFeatureCollection
2727
{ typeof(IHttpBodyControlFeature), _identityFunc },
2828
{ typeof(IHttpSysRequestInfoFeature), _identityFunc },
2929
{ typeof(IHttpSysRequestTimingFeature), _identityFunc },
30+
{ typeof(IHttpSysRequestPropertyFeature), _identityFunc },
3031
{ typeof(IHttpResponseTrailersFeature), ctx => ctx.GetResponseTrailersFeature() },
3132
{ typeof(IHttpResetFeature), ctx => ctx.GetResetFeature() },
3233
{ typeof(IConnectionLifetimeNotificationFeature), ctx => ctx.GetConnectionLifetimeNotificationFeature() },

src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,28 @@ public async Task Https_ITlsHandshakeFeature_MatchesIHttpSysExtensionInfoFeature
234234
}
235235
}
236236

237+
[ConditionalFact]
238+
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)]
239+
public async Task Https_SetsIHttpSysRequestPropertyFeature()
240+
{
241+
using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
242+
{
243+
try
244+
{
245+
var requestPropertyFeature = httpContext.Features.Get<IHttpSysRequestPropertyFeature>();
246+
Assert.NotNull(requestPropertyFeature);
247+
}
248+
catch (Exception ex)
249+
{
250+
await httpContext.Response.WriteAsync(ex.ToString());
251+
}
252+
}, LoggerFactory))
253+
{
254+
string response = await SendRequestAsync(address);
255+
Assert.Equal(string.Empty, response);
256+
}
257+
}
258+
237259
[ConditionalFact]
238260
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)]
239261
public async Task Https_SetsIHttpSysRequestTimingFeature()

0 commit comments

Comments
 (0)