Skip to content

backport(net8.0): http.sys on-demand TLS client hello retrieval #62290

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand Down
46 changes: 44 additions & 2 deletions src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<IMyTlsFeature>();
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<byte>();
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<byte>.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<byte> slicing
// error CS4012: Parameters or locals of type 'Span<byte>' cannot be declared in async methods or async lambda expressions.
var message = ReadTlsClientHello(bytes, bytesReturned);
await context.Response.WriteAsync(message);
ArrayPool<byte>.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<IMyTlsFeature>();
// await context.Response.WriteAsync("TlsClientHello` data: " + $"connectionId={tlsFeature?.ConnectionId}; length={tlsFeature?.TlsClientHelloLength}");
//});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<OutputType>Exe</OutputType>
<ServerGarbageCollection>true</ServerGarbageCollection>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
38 changes: 38 additions & 0 deletions src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides API to read HTTP_REQUEST_PROPERTY value from the HTTP.SYS request.
/// <see href="https://learn.microsoft.com/windows/win32/api/http/ne-http-http_request_property"/>
/// </summary>
// internal for backport
internal interface IHttpSysRequestPropertyFeature
{
/// <summary>
/// Reads the TLS client hello from HTTP.SYS
/// </summary>
/// <param name="tlsClientHelloBytesDestination">Where the raw bytes of the TLS Client Hello message are written.</param>
/// <param name="bytesReturned">
/// Returns the number of bytes written to <paramref name="tlsClientHelloBytesDestination"/>.
/// Or can return the size of the buffer needed if <paramref name="tlsClientHelloBytesDestination"/> wasn't large enough.
/// </param>
/// <remarks>
/// Works only if <c>HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO</c> flag is set on http.sys service configuration.
/// See <see href="https://learn.microsoft.com/windows/win32/api/http/nf-http-httpsetserviceconfiguration"/>
/// and <see href="https://learn.microsoft.com/windows/win32/api/http/ne-http-http_service_config_id"/>
/// <br/><br/>
/// If you don't want to guess the required <paramref name="tlsClientHelloBytesDestination"/> size before first invocation,
/// 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"/>,
/// then allocate that amount of memory and retry the query.
/// </remarks>
/// <returns>
/// True, if fetching TLS client hello was successful, false if <paramref name="tlsClientHelloBytesDestination"/> size is not large enough.
/// If unsuccessful for other reason throws an exception.
/// </returns>
/// <exception cref="HttpSysException">Any HttpSys error except for ERROR_INSUFFICIENT_BUFFER or ERROR_MORE_DATA.</exception>
/// <exception cref="InvalidOperationException">If HttpSys does not support querying the TLS Client Hello.</exception>
// has byte[] (not Span<byte>) for reflection-based invocation
bool TryGetTlsClientHello(byte[] tlsClientHelloBytesDestination, out int bytesReturned);
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ internal partial class RequestContext :
IHttpResponseTrailersFeature,
IHttpResetFeature,
IHttpSysRequestDelegationFeature,
IHttpSysRequestPropertyFeature,
IConnectionLifetimeNotificationFeature
{
private IFeatureCollection? _features;
Expand Down Expand Up @@ -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);
}
}
54 changes: 54 additions & 0 deletions src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,60 @@ internal void ForceCancelRequest()
}
}

/// <summary>
/// Attempts to get the client hello message bytes from the http.sys.
/// If successful writes the bytes into <paramref name="destination"/>, and shows how many bytes were written in <paramref name="bytesReturned"/>.
/// 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"/>.
/// If not successful for other reason - throws exception with message/errorCode.
/// </summary>
internal unsafe bool TryGetTlsClientHelloMessageBytes(
Span<byte> 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);
}

/// <summary>
/// Attempts to get the client hello message bytes from HTTP.sys and calls the user provided callback.
/// If not successful, will return false.
Expand Down
1 change: 1 addition & 0 deletions src/Servers/HttpSys/src/StandardFeatureCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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() },
Expand Down
22 changes: 22 additions & 0 deletions src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IHttpSysRequestPropertyFeature>();
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()
Expand Down
Loading