Skip to content

feat(HTTP.SYS): on-demand TLS client hello retrieval #62209

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
Merged
Show file tree
Hide file tree
Changes from 2 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
34 changes: 34 additions & 0 deletions src/Http/Http.Features/src/IHttpSysRequestPropertyFeature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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.Http.Features;

/// <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>
public interface IHttpSysRequestPropertyFeature
{
/// <summary>
/// Reads the TLS client hello from HTTP.SYS
/// </summary>
/// <param name="tlsClientHelloBytesDestination">where raw bytes of tls client hello message will be written</param>
/// <param name="bytesReturned">
/// Returns the number of bytes written to <paramref name="tlsClientHelloBytesDestination"/>.
/// Or can return the size of 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 required <paramref name="tlsClientHelloBytesDestination"/> size before first invocation,
/// you should call first with <paramref name="tlsClientHelloBytesDestination"/> set to empty size, so that you can retrieve through <paramref name="bytesReturned"/> the buffer size you need,
/// then allocate that amount of memory, then retry the query.
/// </remarks>
/// <returns>
/// True, if fetching TLS client hello was successful, false if <paramref name="tlsClientHelloBytesDestination"/> size is not large enough.
/// If non-successful for other reason throws an exception.
/// </returns>
bool TryGetTlsClientHello(Span<byte> tlsClientHelloBytesDestination, out int bytesReturned);
}
2 changes: 2 additions & 0 deletions src/Http/Http.Features/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.AspNetCore.Http.Features.IHttpSysRequestPropertyFeature
Microsoft.AspNetCore.Http.Features.IHttpSysRequestPropertyFeature.TryGetTlsClientHello(System.Span<byte> tlsClientHelloBytesDestination, out int bytesReturned) -> bool
74 changes: 31 additions & 43 deletions src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs
Original file line number Diff line number Diff line change
@@ -1,60 +1,48 @@
// 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<Startup>()
.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;

options.TlsClientHelloBytesCallback = ProcessTlsClientHello;
});
});

static void ProcessTlsClientHello(IFeatureCollection features, ReadOnlySpan<byte> tlsClientHelloBytes)
{
var httpConnectionFeature = features.Get<IHttpConnectionFeature>();

var myTlsFeature = new MyTlsFeature(
connectionId: httpConnectionFeature.ConnectionId,
tlsClientHelloLength: tlsClientHelloBytes.Length);

features.Set<IMyTlsFeature>(myTlsFeature);
}
var builder = WebApplication.CreateBuilder(args);

public interface IMyTlsFeature
builder.WebHost.UseHttpSys(options =>
{
string ConnectionId { get; }
int TlsClientHelloLength { get; }
}
options.UrlPrefixes.Add("https://*:6000");
options.Authentication.Schemes = AuthenticationSchemes.None;
options.Authentication.AllowAnonymous = true;
});

public class MyTlsFeature : IMyTlsFeature
var app = builder.Build();

app.Use(async (context, next) =>
{
public string ConnectionId { get; }
public int TlsClientHelloLength { get; }

public MyTlsFeature(string connectionId, int tlsClientHelloLength)
{
ConnectionId = connectionId;
TlsClientHelloLength = tlsClientHelloLength;
}
}
var connectionFeature = context.Features.GetRequiredFeature<IHttpConnectionFeature>();
var httpSysPropFeature = context.Features.GetRequiredFeature<IHttpSysRequestPropertyFeature>();

// first time invocation to find out required size
var success = httpSysPropFeature.TryGetTlsClientHello(Array.Empty<byte>(), out var bytesReturned);
Debug.Assert(!success);
Debug.Assert(bytesReturned > 0);

// rent with enough memory span and invoke
var bytes = ArrayPool<byte>.Shared.Rent(bytesReturned);
success = httpSysPropFeature.TryGetTlsClientHello(bytes, out _);
Debug.Assert(success);

await context.Response.WriteAsync($"[Response] connectionId={connectionFeature.ConnectionId}; tlsClientHello.length={bytesReturned}; tlsclienthello start={string.Join(' ', bytes.AsSpan(0, 30).ToArray())}");
await next(context);
});

app.Run();
28 changes: 0 additions & 28 deletions src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Server.HttpSys" />
<Reference Include="Microsoft.Extensions.Hosting" />
<Reference Include="Microsoft.Extensions.Logging.Console" />
Expand Down
10 changes: 0 additions & 10 deletions src/Servers/HttpSys/src/HttpSysListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
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;
Expand Down Expand Up @@ -42,7 +41,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;

Expand Down Expand Up @@ -76,20 +74,14 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory)
_requestQueue = new RequestQueue(options.RequestQueueName, options.RequestQueueMode,
options.RequestQueueSecurityDescriptor, 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)
{
// If Url group or request queue creation failed, close server session before throwing.
_requestQueue?.Dispose();
_urlGroup?.Dispose();
_serverSession?.Dispose();
_tlsListener?.Dispose();
Log.HttpSysListenerCtorError(Logger, exception);
throw;
}
Expand All @@ -106,7 +98,6 @@ internal enum State

internal UrlGroup UrlGroup => _urlGroup;
internal RequestQueue RequestQueue => _requestQueue;
internal TlsListener? TlsListener => _tlsListener;
internal DisconnectListener DisconnectListener => _disconnectListener;

public HttpSysOptions Options { get; }
Expand Down Expand Up @@ -260,7 +251,6 @@ private void DisposeInternal()
Debug.Assert(!_serverSession.Id.IsInvalid, "ServerSessionHandle is invalid in CloseV2Config");

_serverSession.Dispose();
_tlsListener?.Dispose();
}

/// <summary>
Expand Down
11 changes: 0 additions & 11 deletions src/Servers/HttpSys/src/HttpSysOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,17 +250,6 @@ public Http503VerbosityLevel Http503Verbosity
/// </remarks>
public bool UseLatin1RequestHeaders { get; set; }

/// <summary>
/// A callback to be invoked to get the TLS client hello bytes.
/// Null by default.
/// </summary>
/// <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"/>
/// </remarks>
public Action<IFeatureCollection, ReadOnlySpan<byte>>? TlsClientHelloBytesCallback { get; set; }

// Not called when attaching to an existing queue.
internal void Apply(UrlGroup urlGroup, RequestQueue? requestQueue)
{
Expand Down
2 changes: 0 additions & 2 deletions src/Servers/HttpSys/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#nullable enable
Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.get -> System.Action<Microsoft.AspNetCore.Http.Features.IFeatureCollection!, System.ReadOnlySpan<byte>>?
Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.set -> void
Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.RequestQueueSecurityDescriptor.get -> System.Security.AccessControl.GenericSecurityDescriptor?
Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.RequestQueueSecurityDescriptor.set -> void
4 changes: 0 additions & 4 deletions src/Servers/HttpSys/src/RequestProcessing/Request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.AspNetCore.Shared;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -370,9 +369,6 @@ private void GetTlsHandshakeResults()
SniHostName = sni.Hostname.ToString();
}

internal bool GetAndInvokeTlsClientHelloCallback(IFeatureCollection features, Action<IFeatureCollection, ReadOnlySpan<byte>> tlsClientHelloBytesCallback)
=> RequestContext.GetAndInvokeTlsClientHelloMessageBytesCallback(features, tlsClientHelloBytesCallback);

public X509Certificate2? ClientCertificate
{
get
Expand Down
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 @@ -753,4 +754,9 @@ void IConnectionLifetimeNotificationFeature.RequestClose()
Response.Headers[HeaderNames.Connection] = "close";
}
}

public bool TryGetTlsClientHello(Span<byte> tlsClientHelloBytesDestination, out int bytesReturned)
{
return TryGetTlsClientHelloMessageBytes(tlsClientHelloBytesDestination, out bytesReturned);
}
}
Loading
Loading