Skip to content

Commit b140c09

Browse files
authored
Add support delegating requests in HttpSysServer (#24857)
* Add new ctors for RequestQueue and UrlGroup * Add DelegateRequest pinvokes * Add Request Transfer Feature * Fix accessibility of feature * Test cleanup * Update ref assembly * hack: Make HttpSysServer packable * Cleanup based on PR feedback * Avoid sending headers after transfer * Fix ref assembly * Fix rebase conflict * Switch to DelegateRequestEx * Add feature detection * Delete ref folder * Add server feature * s/RequestQueueWrapper/DelegationRule * Fix UrlGroup was null issue * Add light-up for ServerDelegationPropertyFeature * Revert changes to sample * Revert changes to sample take 2 * PR feedback * s/Transfered/Transferred * DelegateAfterRequestBodyReadShouldThrow * Make DelegationRule disposable * More license headers * Incomplete XML doc * PR feedback * Fix broken test * PR feedback * Fixup test * s/Transfer/Delegate * s/transfer/delegate * PR feedback
1 parent c6814f4 commit b140c09

18 files changed

+511
-31
lines changed

src/Servers/HttpSys/src/AsyncAcceptContext.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ internal uint QueueBeginGetContext()
127127
statusCode = HttpApi.HttpReceiveHttpRequest(
128128
Server.RequestQueue.Handle,
129129
_nativeRequestContext.RequestId,
130-
(uint)HttpApiTypes.HTTP_FLAGS.HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY,
130+
// Small perf impact by not using HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY
131+
// if the request sends header+body in a single TCP packet
132+
(uint)HttpApiTypes.HTTP_FLAGS.NONE,
131133
_nativeRequestContext.NativeRequest,
132134
_nativeRequestContext.Size,
133135
&bytesTransferred,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace Microsoft.AspNetCore.Server.HttpSys
8+
{
9+
/// <summary>
10+
/// Rule that maintains a handle to the Request Queue and UrlPrefix to
11+
/// delegate to.
12+
/// </summary>
13+
public class DelegationRule : IDisposable
14+
{
15+
private readonly ILogger _logger;
16+
/// <summary>
17+
/// The name of the Http.Sys request queue
18+
/// </summary>
19+
public string QueueName { get; }
20+
/// <summary>
21+
/// The URL of the Http.Sys Url Prefix
22+
/// </summary>
23+
public string UrlPrefix { get; }
24+
internal RequestQueue Queue { get; }
25+
26+
internal DelegationRule(string queueName, string urlPrefix, ILogger logger)
27+
{
28+
_logger = logger;
29+
QueueName = queueName;
30+
UrlPrefix = urlPrefix;
31+
Queue = new RequestQueue(queueName, UrlPrefix, _logger, receiver: true);
32+
}
33+
34+
public void Dispose()
35+
{
36+
Queue.UrlGroup?.Dispose();
37+
Queue?.Dispose();
38+
}
39+
}
40+
}

src/Servers/HttpSys/src/FeatureContext.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ internal class FeatureContext :
3737
IHttpBodyControlFeature,
3838
IHttpSysRequestInfoFeature,
3939
IHttpResponseTrailersFeature,
40-
IHttpResetFeature
40+
IHttpResetFeature,
41+
IHttpSysRequestDelegationFeature
4142
{
4243
private RequestContext _requestContext;
4344
private IFeatureCollection _features;
@@ -591,6 +592,8 @@ IHeaderDictionary IHttpResponseTrailersFeature.Trailers
591592
set => _responseTrailers = value;
592593
}
593594

595+
public bool CanDelegate => Request.CanDelegate;
596+
594597
internal async Task OnResponseStart()
595598
{
596599
if (_responseStarted)
@@ -711,5 +714,11 @@ private async Task NotifyOnCompletedAsync()
711714
await actionPair.Item1(actionPair.Item2);
712715
}
713716
}
717+
718+
public void DelegateRequest(DelegationRule destination)
719+
{
720+
_requestContext.Delegate(destination);
721+
_responseStarted = true;
722+
}
714723
}
715724
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Server.HttpSys
5+
{
6+
public interface IHttpSysRequestDelegationFeature
7+
{
8+
/// <summary>
9+
/// Indicates if the server can delegate this request to another HttpSys request queue.
10+
/// </summary>
11+
bool CanDelegate { get; }
12+
13+
/// <summary>
14+
/// Attempt to delegate the request to another Http.Sys request queue. The request body
15+
/// must not be read nor the response started before this is invoked. Check <see cref="CanDelegate"/>
16+
/// before invoking.
17+
/// </summary>
18+
void DelegateRequest(DelegationRule destination);
19+
}
20+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Server.HttpSys
5+
{
6+
public interface IServerDelegationFeature
7+
{
8+
/// <summary>
9+
/// Create a delegation rule on request queue owned by the server.
10+
/// </summary>
11+
/// <returns>
12+
/// Creates a <see cref="DelegationRule"/> that can used to delegate individual requests.
13+
/// </returns>
14+
DelegationRule CreateDelegationRule(string queueName, string urlPrefix);
15+
}
16+
}

src/Servers/HttpSys/src/MessagePump.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ public MessagePump(IOptions<HttpSysOptions> options, ILoggerFactory loggerFactor
5555
_serverAddresses = new ServerAddressesFeature();
5656
Features.Set<IServerAddressesFeature>(_serverAddresses);
5757

58+
if (HttpApi.IsFeatureSupported(HttpApiTypes.HTTP_FEATURE_ID.HttpFeatureDelegateEx))
59+
{
60+
var delegationProperty = new ServerDelegationPropertyFeature(Listener.RequestQueue, _logger);
61+
Features.Set<IServerDelegationFeature>(delegationProperty);
62+
}
63+
5864
_maxAccepts = _options.MaxAccepts;
5965
}
6066

src/Servers/HttpSys/src/NativeInterop/HttpApi.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ internal static unsafe class HttpApi
4545
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
4646
internal static extern uint HttpCreateUrlGroup(ulong serverSessionId, ulong* urlGroupId, uint reserved);
4747

48+
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
49+
internal static extern uint HttpFindUrlGroupId(string pFullyQualifiedUrl, SafeHandle requestQueueHandle, ulong* urlGroupId);
50+
4851
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
4952
internal static extern uint HttpAddUrlToUrlGroup(ulong urlGroupId, string pFullyQualifiedUrl, ulong context, uint pReserved);
5053

@@ -70,6 +73,13 @@ internal static extern unsafe uint HttpCreateRequestQueue(HTTPAPI_VERSION versio
7073
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
7174
internal static extern unsafe uint HttpCloseRequestQueue(IntPtr pReqQueueHandle);
7275

76+
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
77+
internal static extern bool HttpIsFeatureSupported(HTTP_FEATURE_ID feature);
78+
79+
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
80+
internal static extern unsafe uint HttpDelegateRequestEx(SafeHandle pReqQueueHandle, SafeHandle pDelegateQueueHandle, ulong requestId,
81+
ulong delegateUrlGroupId, ulong propertyInfoSetSize, HTTP_DELEGATE_REQUEST_PROPERTY_INFO* pRequestPropertyBuffer);
82+
7383
internal delegate uint HttpSetRequestPropertyInvoker(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, void* input, uint inputSize, IntPtr overlapped);
7484

7585
private static HTTPAPI_VERSION version;
@@ -145,5 +155,16 @@ internal static bool Supported
145155
return supported;
146156
}
147157
}
158+
159+
internal static bool IsFeatureSupported(HTTP_FEATURE_ID feature)
160+
{
161+
try
162+
{
163+
return HttpIsFeatureSupported(feature);
164+
}
165+
catch (EntryPointNotFoundException) { }
166+
167+
return false;
168+
}
148169
}
149170
}

src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,44 @@ internal class RequestQueue
1616
Marshal.SizeOf<HttpApiTypes.HTTP_BINDING_INFO>();
1717

1818
private readonly RequestQueueMode _mode;
19-
private readonly UrlGroup _urlGroup;
2019
private readonly ILogger _logger;
2120
private bool _disposed;
2221

22+
internal RequestQueue(string requestQueueName, string urlPrefix, ILogger logger, bool receiver)
23+
: this(urlGroup: null, requestQueueName, RequestQueueMode.Attach, logger, receiver)
24+
{
25+
try
26+
{
27+
UrlGroup = new UrlGroup(this, UrlPrefix.Create(urlPrefix));
28+
}
29+
catch
30+
{
31+
Dispose();
32+
throw;
33+
}
34+
}
35+
2336
internal RequestQueue(UrlGroup urlGroup, string requestQueueName, RequestQueueMode mode, ILogger logger)
37+
: this(urlGroup, requestQueueName, mode, logger, false)
38+
{ }
39+
40+
private RequestQueue(UrlGroup urlGroup, string requestQueueName, RequestQueueMode mode, ILogger logger, bool receiver)
2441
{
2542
_mode = mode;
26-
_urlGroup = urlGroup;
43+
UrlGroup = urlGroup;
2744
_logger = logger;
2845

2946
var flags = HttpApiTypes.HTTP_CREATE_REQUEST_QUEUE_FLAG.None;
3047
Created = true;
48+
3149
if (_mode == RequestQueueMode.Attach)
3250
{
3351
flags = HttpApiTypes.HTTP_CREATE_REQUEST_QUEUE_FLAG.OpenExisting;
3452
Created = false;
53+
if (receiver)
54+
{
55+
flags |= HttpApiTypes.HTTP_CREATE_REQUEST_QUEUE_FLAG.Delegation;
56+
}
3557
}
3658

3759
var statusCode = HttpApi.HttpCreateRequestQueue(
@@ -54,7 +76,7 @@ internal RequestQueue(UrlGroup urlGroup, string requestQueueName, RequestQueueMo
5476
out requestQueueHandle);
5577
}
5678

57-
if (flags == HttpApiTypes.HTTP_CREATE_REQUEST_QUEUE_FLAG.OpenExisting && statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_FILE_NOT_FOUND)
79+
if (flags.HasFlag(HttpApiTypes.HTTP_CREATE_REQUEST_QUEUE_FLAG.OpenExisting) && statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_FILE_NOT_FOUND)
5880
{
5981
throw new HttpSysException((int)statusCode, $"Failed to attach to the given request queue '{requestQueueName}', the queue could not be found.");
6082
}
@@ -95,6 +117,8 @@ internal RequestQueue(UrlGroup urlGroup, string requestQueueName, RequestQueueMo
95117
internal SafeHandle Handle { get; }
96118
internal ThreadPoolBoundHandle BoundHandle { get; }
97119

120+
internal UrlGroup UrlGroup { get; }
121+
98122
internal unsafe void AttachToUrlGroup()
99123
{
100124
Debug.Assert(Created);
@@ -108,7 +132,7 @@ internal unsafe void AttachToUrlGroup()
108132

109133
var infoptr = new IntPtr(&info);
110134

111-
_urlGroup.SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerBindingProperty,
135+
UrlGroup.SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerBindingProperty,
112136
infoptr, (uint)BindingInfoSize);
113137
}
114138

@@ -128,7 +152,7 @@ internal unsafe void DetachFromUrlGroup()
128152

129153
var infoptr = new IntPtr(&info);
130154

131-
_urlGroup.SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerBindingProperty,
155+
UrlGroup.SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerBindingProperty,
132156
infoptr, (uint)BindingInfoSize, throwOnError: false);
133157
}
134158

src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ internal class UrlGroup : IDisposable
1313
{
1414
private static readonly int QosInfoSize =
1515
Marshal.SizeOf<HttpApiTypes.HTTP_QOS_SETTING_INFO>();
16+
private static readonly int RequestPropertyInfoSize =
17+
Marshal.SizeOf<HttpApiTypes.HTTP_BINDING_INFO>();
1618

1719
private ServerSession _serverSession;
1820
private ILogger _logger;
@@ -36,6 +38,21 @@ internal unsafe UrlGroup(ServerSession serverSession, ILogger logger)
3638
Id = urlGroupId;
3739
}
3840

41+
internal unsafe UrlGroup(RequestQueue requestQueue, UrlPrefix url)
42+
{
43+
ulong urlGroupId = 0;
44+
var statusCode = HttpApi.HttpFindUrlGroupId(
45+
url.FullPrefix, requestQueue.Handle, &urlGroupId);
46+
47+
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
48+
{
49+
throw new HttpSysException((int)statusCode);
50+
}
51+
52+
Debug.Assert(urlGroupId != 0, "Invalid id returned by HttpCreateUrlGroup");
53+
Id = urlGroupId;
54+
}
55+
3956
internal ulong Id { get; private set; }
4057

4158
internal unsafe void SetMaxConnections(long maxConnections)
@@ -51,6 +68,15 @@ internal unsafe void SetMaxConnections(long maxConnections)
5168
SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerQosProperty, new IntPtr(&qosSettings), (uint)QosInfoSize);
5269
}
5370

71+
internal unsafe void SetDelegationProperty(RequestQueue destination)
72+
{
73+
var propertyInfo = new HttpApiTypes.HTTP_BINDING_INFO();
74+
propertyInfo.Flags = HttpApiTypes.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT;
75+
propertyInfo.RequestQueueHandle = destination.Handle.DangerousGetHandle();
76+
77+
SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerDelegationProperty, new IntPtr(&propertyInfo), (uint)RequestPropertyInfoSize);
78+
}
79+
5480
internal void SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY property, IntPtr info, uint infosize, bool throwOnError = true)
5581
{
5682
Debug.Assert(info != IntPtr.Zero, "SetUrlGroupProperty called with invalid pointer");

src/Servers/HttpSys/src/RequestProcessing/Request.cs

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -61,43 +61,40 @@ internal Request(RequestContext requestContext, NativeRequestContext nativeReque
6161
var rawUrlInBytes = _nativeRequestContext.GetRawUrlInBytes();
6262
var originalPath = RequestUriBuilder.DecodeAndUnescapePath(rawUrlInBytes);
6363

64+
PathBase = string.Empty;
65+
Path = originalPath;
66+
6467
// 'OPTIONS * HTTP/1.1'
6568
if (KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbOPTIONS && string.Equals(RawUrl, "*", StringComparison.Ordinal))
6669
{
6770
PathBase = string.Empty;
6871
Path = string.Empty;
6972
}
70-
else if (requestContext.Server.RequestQueue.Created)
73+
else
7174
{
7275
var prefix = requestContext.Server.Options.UrlPrefixes.GetPrefix((int)nativeRequestContext.UrlContext);
73-
74-
if (originalPath.Length == prefix.PathWithoutTrailingSlash.Length)
75-
{
76-
// They matched exactly except for the trailing slash.
77-
PathBase = originalPath;
78-
Path = string.Empty;
79-
}
80-
else
76+
// Prefix may be null if the requested has been transfered to our queue
77+
if (!(prefix is null))
8178
{
82-
// url: /base/path, prefix: /base/, base: /base, path: /path
83-
// url: /, prefix: /, base: , path: /
84-
PathBase = originalPath.Substring(0, prefix.PathWithoutTrailingSlash.Length); // Preserve the user input casing
85-
Path = originalPath.Substring(prefix.PathWithoutTrailingSlash.Length);
79+
if (originalPath.Length == prefix.PathWithoutTrailingSlash.Length)
80+
{
81+
// They matched exactly except for the trailing slash.
82+
PathBase = originalPath;
83+
Path = string.Empty;
84+
}
85+
else
86+
{
87+
// url: /base/path, prefix: /base/, base: /base, path: /path
88+
// url: /, prefix: /, base: , path: /
89+
PathBase = originalPath.Substring(0, prefix.PathWithoutTrailingSlash.Length); // Preserve the user input casing
90+
Path = originalPath.Substring(prefix.PathWithoutTrailingSlash.Length);
91+
}
8692
}
87-
}
88-
else
89-
{
90-
// When attaching to an existing queue, the UrlContext hint may not match our configuration. Search manualy.
91-
if (requestContext.Server.Options.UrlPrefixes.TryMatchLongestPrefix(IsHttps, cookedUrl.GetHost(), originalPath, out var pathBase, out var path))
93+
else if (requestContext.Server.Options.UrlPrefixes.TryMatchLongestPrefix(IsHttps, cookedUrl.GetHost(), originalPath, out var pathBase, out var path))
9294
{
9395
PathBase = pathBase;
9496
Path = path;
9597
}
96-
else
97-
{
98-
PathBase = string.Empty;
99-
Path = originalPath;
100-
}
10198
}
10299

103100
ProtocolVersion = _nativeRequestContext.GetVersion();
@@ -350,6 +347,8 @@ public X509Certificate2 ClientCertificate
350347
}
351348
}
352349

350+
public bool CanDelegate => !(HasRequestBodyStarted || RequestContext.Response.HasStarted);
351+
353352
// Populates the client certificate. The result may be null if there is no client cert.
354353
// TODO: Does it make sense for this to be invoked multiple times (e.g. renegotiate)? Client and server code appear to
355354
// enable this, but it's unclear what Http.Sys would do.

0 commit comments

Comments
 (0)