Skip to content
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
62 changes: 62 additions & 0 deletions src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

var app = builder.Build();

// Example middleware using TryGetTlsClientHello API to query TLS Client Hello raw bytes.
app.Use(async (context, next) =>
{
var connectionFeature = context.Features.GetRequiredFeature<IHttpConnectionFeature>();
Expand All @@ -45,13 +46,74 @@

await context.Response.WriteAsync(
$"""
TryGetTlsClientHello
--------------------
connectionId = {connectionFeature.ConnectionId};
negotiated cipher suite = {tlsHandshakeFeature.NegotiatedCipherSuite};
tlsClientHello.length = {bytesReturned};
tlsclienthello start = {string.Join(' ', bytes.AsSpan(0, 30).ToArray())}


""");

await next(context);
});

// Example middleware exercising the generic IHttpSysRequestPropertyFeature.TryGetRequestProperty API.
app.Use(async (context, next) =>
{
// From Win SDK http.h
const int HttpRequestPropertyTlsClientHello = 11;

var httpSysPropFeature = context.Features.GetRequiredFeature<IHttpSysRequestPropertyFeature>();

try
{
// probe required size with empty output buffer and empty qualifier
var success = httpSysPropFeature.TryGetRequestProperty(
HttpRequestPropertyTlsClientHello,
qualifier: default,
output: default,
out var requiredSize);
Debug.Assert(!success);
Debug.Assert(requiredSize > 0);

var rented = ArrayPool<byte>.Shared.Rent(requiredSize);
try
{
success = httpSysPropFeature.TryGetRequestProperty(
HttpRequestPropertyTlsClientHello,
qualifier: default,
output: rented.AsSpan(0, requiredSize),
out var written);
Debug.Assert(success);

await context.Response.WriteAsync(
$"""
TryGetRequestProperty(HttpRequestPropertyTlsClientHello)
--------------------------------------------------------
requiredSize = {requiredSize}
bytesReturned = {written}
first 30 bytes = {string.Join(' ', rented.AsSpan(0, Math.Min(30, written)).ToArray())}


""");
}
finally
{
ArrayPool<byte>.Shared.Return(rented);
}
}
catch (Exception ex)
{
await context.Response.WriteAsync(
$"""

TryGetRequestProperty(HttpRequestPropertyTlsClientHello) threw: {ex.GetType().Name}: {ex.Message}
""");
}

await next(context);
});

app.Run();
35 changes: 35 additions & 0 deletions src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,39 @@ public interface IHttpSysRequestPropertyFeature
/// <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>
bool TryGetTlsClientHello(Span<byte> tlsClientHelloBytesDestination, out int bytesReturned);

/// <summary>
/// Reads an arbitrary HTTP_REQUEST_PROPERTY value from HTTP.SYS using the
/// <see href="https://learn.microsoft.com/windows/win32/api/http/nf-http-httpqueryrequestproperty">HttpQueryRequestProperty</see> Windows API.
/// </summary>
/// <param name="propertyId">
/// The HTTP_REQUEST_PROPERTY identifier to query. The set of supported values is defined by the
/// <c>HTTP_REQUEST_PROPERTY</c> enum in <c>http.h</c>; the caller is responsible for parsing the bytes returned in
/// <paramref name="output"/> using the corresponding native struct.
/// </param>
/// <param name="qualifier">
/// Optional property-specific qualifier bytes. Pass an empty span for properties that do not require a qualifier;
/// it will be mapped to a null pointer when calling the underlying API.
/// </param>
/// <param name="output">
/// Destination buffer that receives the property value. Pass an empty span to query the required buffer size via <paramref name="bytesReturned"/>.
/// </param>
/// <param name="bytesReturned">
/// Returns the number of bytes written to <paramref name="output"/>.
/// If <paramref name="output"/> was too small (or empty), returns the size of the buffer required to hold the value.
/// </param>
/// <remarks>
/// If the required buffer size is not known up front, first call this method with an empty <paramref name="output"/>
/// to retrieve the required size in <paramref name="bytesReturned"/>, then allocate that many bytes and retry the query.
/// </remarks>
/// <returns>
/// True if the property was successfully read into <paramref name="output"/>.
/// False if <paramref name="output"/> is not large enough to hold the value; in that case <paramref name="bytesReturned"/>
/// contains the required buffer size.
/// For any other failure, an exception is thrown.
/// </returns>
/// <exception cref="HttpSysException">Any HttpSys error except for ERROR_INSUFFICIENT_BUFFER or ERROR_MORE_DATA.</exception>
/// <exception cref="InvalidOperationException">If the installed Windows HTTP Server API does not support HttpQueryRequestProperty.</exception>
bool TryGetRequestProperty(int propertyId, ReadOnlySpan<byte> qualifier, Span<byte> output, out int bytesReturned)
=> throw new NotSupportedException();
}
1 change: 1 addition & 0 deletions src/Servers/HttpSys/src/LoggerEventIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ internal static class LoggerEventIds
public const int RequestParsingError = 54;
public const int TlsListenerError = 55;
public const int QueryTlsCipherSuiteError = 56;
public const int QueryRequestPropertyError = 57;
}
1 change: 1 addition & 0 deletions src/Servers/HttpSys/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.AspNetCore.Server.HttpSys.IHttpSysRequestPropertyFeature.TryGetRequestProperty(int propertyId, System.ReadOnlySpan<byte> qualifier, System.Span<byte> output, out int bytesReturned) -> bool
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.Net.Http.Headers;
using Windows.Win32.Networking.HttpServer;

namespace Microsoft.AspNetCore.Server.HttpSys;

Expand Down Expand Up @@ -764,6 +765,11 @@ public bool TryGetTlsClientHello(Span<byte> tlsClientHelloBytesDestination, out
return TryGetTlsClientHelloMessageBytes(tlsClientHelloBytesDestination, out bytesReturned);
}

public bool TryGetRequestProperty(int propertyId, ReadOnlySpan<byte> qualifier, Span<byte> output, out int bytesReturned)
{
return TryGetRequestPropertyCore((HTTP_REQUEST_PROPERTY)propertyId, qualifier, output, out bytesReturned);
}

EndPoint? IConnectionEndPointFeature.LocalEndPoint
{
get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ private static partial class Log
[LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to parse request.", EventName = "RequestParsingError")]
public static partial void RequestParsingError(ILogger logger, Exception exception);

[LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello; RequestId: {RequestId}; Win32 Error code: {Win32Error}", EventName = "TlsClientHelloRetrieveError")]
[LoggerMessage(LoggerEventIds.TlsListenerError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello; RequestId: {RequestId}; Win32 Error code: {Win32Error}", EventName = "TlsClientHelloRetrieveError")]
public static partial void TlsClientHelloRetrieveError(ILogger logger, ulong requestId, uint win32Error);
Comment on lines +24 to 25

[LoggerMessage(LoggerEventIds.QueryRequestPropertyError, LogLevel.Debug, "Failed to invoke HttpQueryRequestProperty; RequestId: {RequestId}; Win32 Error code: {Win32Error}", EventName = "QueryRequestPropertyError")]
public static partial void QueryRequestPropertyError(ILogger logger, ulong requestId, uint win32Error);

[LoggerMessage(LoggerEventIds.QueryTlsCipherSuiteError, LogLevel.Debug, "Failed to invoke QueryTlsCipherSuite; RequestId: {RequestId}; Win32 Error code: {Win32Error}", EventName = "QueryTlsCipherSuiteError")]
public static partial void QueryTlsCipherSuiteError(ILogger logger, ulong requestId, uint win32Error);
}
Expand Down
56 changes: 56 additions & 0 deletions src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,62 @@ internal unsafe bool TryGetTlsClientHelloMessageBytes(
throw new HttpSysException((int)statusCode);
}

/// <summary>
/// Generic synchronous wrapper around <c>HttpQueryRequestProperty</c>.
/// Returns true on success, false if <paramref name="output"/> is too small (with the required size in <paramref name="bytesReturned"/>),
/// and throws for any other failure.
/// </summary>
internal unsafe bool TryGetRequestPropertyCore(
HTTP_REQUEST_PROPERTY propertyId,
ReadOnlySpan<byte> qualifier,
Span<byte> output,
out int bytesReturned)
{
bytesReturned = default;
if (!HttpApi.HttpGetRequestPropertySupported)
{
throw new InvalidOperationException("Windows HTTP Server API does not support HttpQueryRequestProperty.");
}

uint statusCode;
var requestId = PinsReleased ? Request.RequestId : RequestId;

uint bytesReturnedValue = 0;
uint* bytesReturnedPointer = &bytesReturnedValue;

// `fixed` on an empty span yields a null pointer, which is what HttpQueryRequestProperty
// requires for unused qualifier/output parameters.
fixed (byte* pQualifier = qualifier)
fixed (byte* pOutput = output)
{
statusCode = HttpApi.HttpGetRequestProperty(
requestQueueHandle: Server.RequestQueue.Handle,
requestId,
propertyId: propertyId,
qualifier: pQualifier,
qualifierSize: (uint)qualifier.Length,
output: pOutput,
outputSize: (uint)output.Length,
bytesReturned: (IntPtr)bytesReturnedPointer,
overlapped: IntPtr.Zero);

bytesReturned = checked((int)bytesReturnedValue);

if (statusCode is ErrorCodes.ERROR_SUCCESS)
{
return true;
}

if (statusCode is ErrorCodes.ERROR_MORE_DATA or ErrorCodes.ERROR_INSUFFICIENT_BUFFER)
{
return false;
}
}

Log.QueryRequestPropertyError(Logger, requestId, statusCode);
throw new HttpSysException((int)statusCode);
}

internal unsafe HTTP_REQUEST_PROPERTY_SNI GetClientSni()
{
if (!HttpApi.HttpGetRequestPropertySupported)
Expand Down
13 changes: 13 additions & 0 deletions src/Servers/HttpSys/src/SourceBuildStubs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,19 @@ public interface IHttpSysRequestPropertyFeature
/// If unsuccessful for other reason throws an exception.
/// </returns>
bool TryGetTlsClientHello(Span<byte> tlsClientHelloBytesDestination, out int bytesReturned);

/// <summary>
/// Reads an arbitrary HTTP_REQUEST_PROPERTY value from HTTP.SYS using HttpQueryRequestProperty.
/// </summary>
/// <param name="propertyId">The HTTP_REQUEST_PROPERTY identifier to query.</param>
/// <param name="qualifier">Optional property-specific qualifier bytes. Pass an empty span when not required.</param>
/// <param name="output">Destination buffer for the property value. Pass an empty span to query the required size.</param>
/// <param name="bytesReturned">
/// Bytes written to <paramref name="output"/>, or the required buffer size when <paramref name="output"/> is too small.
/// </param>
/// <returns>True on success; false when <paramref name="output"/> is too small.</returns>
bool TryGetRequestProperty(int propertyId, ReadOnlySpan<byte> qualifier, Span<byte> output, out int bytesReturned)
=> throw new NotSupportedException();
}

/// <summary>
Expand Down
37 changes: 37 additions & 0 deletions src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,43 @@ public async Task Https_SetsIHttpSysRequestPropertyFeature()
}
}

[ConditionalFact]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)]
public async Task Https_TryGetRequestProperty_TlsCipherInfo_RoundTrips()
{
using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
{
try
{
var feature = httpContext.Features.Get<IHttpSysRequestPropertyFeature>();
Assert.NotNull(feature);

// TlsCipherInfo is available on any HTTPS request without per-binding configuration,
// unlike TlsClientHello which requires HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO.
// The buffer is generously sized so the API can write its (fixed-size) struct without
// us having to know the exact size up front.
var propertyId = (int)HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsCipherInfo;

var buffer = new byte[4096];
Assert.True(feature.TryGetRequestProperty(propertyId, qualifier: default, output: buffer, out var written));
Assert.InRange(written, 1, buffer.Length);

// Buffer too small returns false. Some HTTP_REQUEST_PROPERTY values report the required
// size in `bytesReturned`, others do not, so we only assert the false return here.
var tooSmall = new byte[1];
Assert.False(feature.TryGetRequestProperty(propertyId, qualifier: default, output: tooSmall, out _));
}
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