Skip to content

Commit d17cc21

Browse files
authored
Add HttpSys wrapper interface for HttpQueryRequestProperty Windows API (#66700)
1 parent 0fcd398 commit d17cc21

9 files changed

Lines changed: 215 additions & 1 deletion

File tree

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
var app = builder.Build();
2929

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

4647
await context.Response.WriteAsync(
4748
$"""
49+
TryGetTlsClientHello
50+
--------------------
4851
connectionId = {connectionFeature.ConnectionId};
4952
negotiated cipher suite = {tlsHandshakeFeature.NegotiatedCipherSuite};
5053
tlsClientHello.length = {bytesReturned};
5154
tlsclienthello start = {string.Join(' ', bytes.AsSpan(0, 30).ToArray())}
55+
56+
5257
""");
5358

5459
await next(context);
5560
});
5661

62+
// Example middleware exercising the generic IHttpSysRequestPropertyFeature.TryGetRequestProperty API.
63+
app.Use(async (context, next) =>
64+
{
65+
// From Win SDK http.h
66+
const int HttpRequestPropertyTlsClientHello = 11;
67+
68+
var httpSysPropFeature = context.Features.GetRequiredFeature<IHttpSysRequestPropertyFeature>();
69+
70+
try
71+
{
72+
// probe required size with empty output buffer and empty qualifier
73+
var success = httpSysPropFeature.TryGetRequestProperty(
74+
HttpRequestPropertyTlsClientHello,
75+
qualifier: default,
76+
output: default,
77+
out var requiredSize);
78+
Debug.Assert(!success);
79+
Debug.Assert(requiredSize > 0);
80+
81+
var rented = ArrayPool<byte>.Shared.Rent(requiredSize);
82+
try
83+
{
84+
success = httpSysPropFeature.TryGetRequestProperty(
85+
HttpRequestPropertyTlsClientHello,
86+
qualifier: default,
87+
output: rented.AsSpan(0, requiredSize),
88+
out var written);
89+
Debug.Assert(success);
90+
91+
await context.Response.WriteAsync(
92+
$"""
93+
TryGetRequestProperty(HttpRequestPropertyTlsClientHello)
94+
--------------------------------------------------------
95+
requiredSize = {requiredSize}
96+
bytesReturned = {written}
97+
first 30 bytes = {string.Join(' ', rented.AsSpan(0, Math.Min(30, written)).ToArray())}
98+
99+
100+
""");
101+
}
102+
finally
103+
{
104+
ArrayPool<byte>.Shared.Return(rented);
105+
}
106+
}
107+
catch (Exception ex)
108+
{
109+
await context.Response.WriteAsync(
110+
$"""
111+
112+
TryGetRequestProperty(HttpRequestPropertyTlsClientHello) threw: {ex.GetType().Name}: {ex.Message}
113+
""");
114+
}
115+
116+
await next(context);
117+
});
118+
57119
app.Run();

src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,39 @@ public interface IHttpSysRequestPropertyFeature
3333
/// <exception cref="HttpSysException">Any HttpSys error except for ERROR_INSUFFICIENT_BUFFER or ERROR_MORE_DATA.</exception>
3434
/// <exception cref="InvalidOperationException">If HttpSys does not support querying the TLS Client Hello.</exception>
3535
bool TryGetTlsClientHello(Span<byte> tlsClientHelloBytesDestination, out int bytesReturned);
36+
37+
/// <summary>
38+
/// Reads an arbitrary HTTP_REQUEST_PROPERTY value from HTTP.SYS using the
39+
/// <see href="https://learn.microsoft.com/windows/win32/api/http/nf-http-httpqueryrequestproperty">HttpQueryRequestProperty</see> Windows API.
40+
/// </summary>
41+
/// <param name="propertyId">
42+
/// The HTTP_REQUEST_PROPERTY identifier to query. The set of supported values is defined by the
43+
/// <c>HTTP_REQUEST_PROPERTY</c> enum in <c>http.h</c>; the caller is responsible for parsing the bytes returned in
44+
/// <paramref name="output"/> using the corresponding native struct.
45+
/// </param>
46+
/// <param name="qualifier">
47+
/// Optional property-specific qualifier bytes. Pass an empty span for properties that do not require a qualifier;
48+
/// it will be mapped to a null pointer when calling the underlying API.
49+
/// </param>
50+
/// <param name="output">
51+
/// Destination buffer that receives the property value. Pass an empty span to query the required buffer size via <paramref name="bytesReturned"/>.
52+
/// </param>
53+
/// <param name="bytesReturned">
54+
/// Returns the number of bytes written to <paramref name="output"/>.
55+
/// If <paramref name="output"/> was too small (or empty), returns the size of the buffer required to hold the value.
56+
/// </param>
57+
/// <remarks>
58+
/// If the required buffer size is not known up front, first call this method with an empty <paramref name="output"/>
59+
/// to retrieve the required size in <paramref name="bytesReturned"/>, then allocate that many bytes and retry the query.
60+
/// </remarks>
61+
/// <returns>
62+
/// True if the property was successfully read into <paramref name="output"/>.
63+
/// False if <paramref name="output"/> is not large enough to hold the value; in that case <paramref name="bytesReturned"/>
64+
/// contains the required buffer size.
65+
/// For any other failure, an exception is thrown.
66+
/// </returns>
67+
/// <exception cref="HttpSysException">Any HttpSys error except for ERROR_INSUFFICIENT_BUFFER or ERROR_MORE_DATA.</exception>
68+
/// <exception cref="InvalidOperationException">If the installed Windows HTTP Server API does not support HttpQueryRequestProperty.</exception>
69+
bool TryGetRequestProperty(int propertyId, ReadOnlySpan<byte> qualifier, Span<byte> output, out int bytesReturned)
70+
=> throw new NotSupportedException();
3671
}

src/Servers/HttpSys/src/LoggerEventIds.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,5 @@ internal static class LoggerEventIds
6161
public const int RequestParsingError = 54;
6262
public const int TlsListenerError = 55;
6363
public const int QueryTlsCipherSuiteError = 56;
64+
public const int QueryRequestPropertyError = 57;
6465
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Server.HttpSys.IHttpSysRequestPropertyFeature.TryGetRequestProperty(int propertyId, System.ReadOnlySpan<byte> qualifier, System.Span<byte> output, out int bytesReturned) -> bool

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Microsoft.AspNetCore.Http.Features;
1515
using Microsoft.AspNetCore.Http.Features.Authentication;
1616
using Microsoft.Net.Http.Headers;
17+
using Windows.Win32.Networking.HttpServer;
1718

1819
namespace Microsoft.AspNetCore.Server.HttpSys;
1920

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

768+
public bool TryGetRequestProperty(int propertyId, ReadOnlySpan<byte> qualifier, Span<byte> output, out int bytesReturned)
769+
{
770+
return TryGetRequestPropertyCore((HTTP_REQUEST_PROPERTY)propertyId, qualifier, output, out bytesReturned);
771+
}
772+
767773
EndPoint? IConnectionEndPointFeature.LocalEndPoint
768774
{
769775
get

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ private static partial class Log
2121
[LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to parse request.", EventName = "RequestParsingError")]
2222
public static partial void RequestParsingError(ILogger logger, Exception exception);
2323

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

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

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,62 @@ internal unsafe bool TryGetTlsClientHelloMessageBytes(
312312
throw new HttpSysException((int)statusCode);
313313
}
314314

315+
/// <summary>
316+
/// Generic synchronous wrapper around <c>HttpQueryRequestProperty</c>.
317+
/// Returns true on success, false if <paramref name="output"/> is too small (with the required size in <paramref name="bytesReturned"/>),
318+
/// and throws for any other failure.
319+
/// </summary>
320+
internal unsafe bool TryGetRequestPropertyCore(
321+
HTTP_REQUEST_PROPERTY propertyId,
322+
ReadOnlySpan<byte> qualifier,
323+
Span<byte> output,
324+
out int bytesReturned)
325+
{
326+
bytesReturned = default;
327+
if (!HttpApi.HttpGetRequestPropertySupported)
328+
{
329+
throw new InvalidOperationException("Windows HTTP Server API does not support HttpQueryRequestProperty.");
330+
}
331+
332+
uint statusCode;
333+
var requestId = PinsReleased ? Request.RequestId : RequestId;
334+
335+
uint bytesReturnedValue = 0;
336+
uint* bytesReturnedPointer = &bytesReturnedValue;
337+
338+
// `fixed` on an empty span yields a null pointer, which is what HttpQueryRequestProperty
339+
// requires for unused qualifier/output parameters.
340+
fixed (byte* pQualifier = qualifier)
341+
fixed (byte* pOutput = output)
342+
{
343+
statusCode = HttpApi.HttpGetRequestProperty(
344+
requestQueueHandle: Server.RequestQueue.Handle,
345+
requestId,
346+
propertyId: propertyId,
347+
qualifier: pQualifier,
348+
qualifierSize: (uint)qualifier.Length,
349+
output: pOutput,
350+
outputSize: (uint)output.Length,
351+
bytesReturned: (IntPtr)bytesReturnedPointer,
352+
overlapped: IntPtr.Zero);
353+
354+
bytesReturned = checked((int)bytesReturnedValue);
355+
356+
if (statusCode is ErrorCodes.ERROR_SUCCESS)
357+
{
358+
return true;
359+
}
360+
361+
if (statusCode is ErrorCodes.ERROR_MORE_DATA or ErrorCodes.ERROR_INSUFFICIENT_BUFFER)
362+
{
363+
return false;
364+
}
365+
}
366+
367+
Log.QueryRequestPropertyError(Logger, requestId, statusCode);
368+
throw new HttpSysException((int)statusCode);
369+
}
370+
315371
internal unsafe HTTP_REQUEST_PROPERTY_SNI GetClientSni()
316372
{
317373
if (!HttpApi.HttpGetRequestPropertySupported)

src/Servers/HttpSys/src/SourceBuildStubs.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,19 @@ public interface IHttpSysRequestPropertyFeature
337337
/// If unsuccessful for other reason throws an exception.
338338
/// </returns>
339339
bool TryGetTlsClientHello(Span<byte> tlsClientHelloBytesDestination, out int bytesReturned);
340+
341+
/// <summary>
342+
/// Reads an arbitrary HTTP_REQUEST_PROPERTY value from HTTP.SYS using HttpQueryRequestProperty.
343+
/// </summary>
344+
/// <param name="propertyId">The HTTP_REQUEST_PROPERTY identifier to query.</param>
345+
/// <param name="qualifier">Optional property-specific qualifier bytes. Pass an empty span when not required.</param>
346+
/// <param name="output">Destination buffer for the property value. Pass an empty span to query the required size.</param>
347+
/// <param name="bytesReturned">
348+
/// Bytes written to <paramref name="output"/>, or the required buffer size when <paramref name="output"/> is too small.
349+
/// </param>
350+
/// <returns>True on success; false when <paramref name="output"/> is too small.</returns>
351+
bool TryGetRequestProperty(int propertyId, ReadOnlySpan<byte> qualifier, Span<byte> output, out int bytesReturned)
352+
=> throw new NotSupportedException();
340353
}
341354

342355
/// <summary>

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,43 @@ public async Task Https_SetsIHttpSysRequestPropertyFeature()
261261
}
262262
}
263263

264+
[ConditionalFact]
265+
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)]
266+
public async Task Https_TryGetRequestProperty_TlsCipherInfo_RoundTrips()
267+
{
268+
using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
269+
{
270+
try
271+
{
272+
var feature = httpContext.Features.Get<IHttpSysRequestPropertyFeature>();
273+
Assert.NotNull(feature);
274+
275+
// TlsCipherInfo is available on any HTTPS request without per-binding configuration,
276+
// unlike TlsClientHello which requires HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO.
277+
// The buffer is generously sized so the API can write its (fixed-size) struct without
278+
// us having to know the exact size up front.
279+
var propertyId = (int)HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsCipherInfo;
280+
281+
var buffer = new byte[4096];
282+
Assert.True(feature.TryGetRequestProperty(propertyId, qualifier: default, output: buffer, out var written));
283+
Assert.InRange(written, 1, buffer.Length);
284+
285+
// Buffer too small returns false. Some HTTP_REQUEST_PROPERTY values report the required
286+
// size in `bytesReturned`, others do not, so we only assert the false return here.
287+
var tooSmall = new byte[1];
288+
Assert.False(feature.TryGetRequestProperty(propertyId, qualifier: default, output: tooSmall, out _));
289+
}
290+
catch (Exception ex)
291+
{
292+
await httpContext.Response.WriteAsync(ex.ToString());
293+
}
294+
}, LoggerFactory))
295+
{
296+
string response = await SendRequestAsync(address);
297+
Assert.Equal(string.Empty, response);
298+
}
299+
}
300+
264301
[ConditionalFact]
265302
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)]
266303
public async Task Https_SetsIHttpSysRequestTimingFeature()

0 commit comments

Comments
 (0)