diff --git a/src/Servers/HttpSys/src/AsyncAcceptContext.cs b/src/Servers/HttpSys/src/AsyncAcceptContext.cs
index f3f5bc14cded..c313f9124e94 100644
--- a/src/Servers/HttpSys/src/AsyncAcceptContext.cs
+++ b/src/Servers/HttpSys/src/AsyncAcceptContext.cs
@@ -127,7 +127,9 @@ internal uint QueueBeginGetContext()
statusCode = HttpApi.HttpReceiveHttpRequest(
Server.RequestQueue.Handle,
_nativeRequestContext.RequestId,
- (uint)HttpApiTypes.HTTP_FLAGS.HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY,
+ // Small perf impact by not using HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY
+ // if the request sends header+body in a single TCP packet
+ (uint)HttpApiTypes.HTTP_FLAGS.NONE,
_nativeRequestContext.NativeRequest,
_nativeRequestContext.Size,
&bytesTransferred,
diff --git a/src/Servers/HttpSys/src/DelegationRule.cs b/src/Servers/HttpSys/src/DelegationRule.cs
new file mode 100644
index 000000000000..593b88456a25
--- /dev/null
+++ b/src/Servers/HttpSys/src/DelegationRule.cs
@@ -0,0 +1,40 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ ///
+ /// Rule that maintains a handle to the Request Queue and UrlPrefix to
+ /// delegate to.
+ ///
+ public class DelegationRule : IDisposable
+ {
+ private readonly ILogger _logger;
+ ///
+ /// The name of the Http.Sys request queue
+ ///
+ public string QueueName { get; }
+ ///
+ /// The URL of the Http.Sys Url Prefix
+ ///
+ public string UrlPrefix { get; }
+ internal RequestQueue Queue { get; }
+
+ internal DelegationRule(string queueName, string urlPrefix, ILogger logger)
+ {
+ _logger = logger;
+ QueueName = queueName;
+ UrlPrefix = urlPrefix;
+ Queue = new RequestQueue(queueName, UrlPrefix, _logger, receiver: true);
+ }
+
+ public void Dispose()
+ {
+ Queue.UrlGroup?.Dispose();
+ Queue?.Dispose();
+ }
+ }
+}
diff --git a/src/Servers/HttpSys/src/FeatureContext.cs b/src/Servers/HttpSys/src/FeatureContext.cs
index 4845013a6b13..3d505c157871 100644
--- a/src/Servers/HttpSys/src/FeatureContext.cs
+++ b/src/Servers/HttpSys/src/FeatureContext.cs
@@ -37,7 +37,8 @@ internal class FeatureContext :
IHttpBodyControlFeature,
IHttpSysRequestInfoFeature,
IHttpResponseTrailersFeature,
- IHttpResetFeature
+ IHttpResetFeature,
+ IHttpSysRequestDelegationFeature
{
private RequestContext _requestContext;
private IFeatureCollection _features;
@@ -591,6 +592,8 @@ IHeaderDictionary IHttpResponseTrailersFeature.Trailers
set => _responseTrailers = value;
}
+ public bool CanDelegate => Request.CanDelegate;
+
internal async Task OnResponseStart()
{
if (_responseStarted)
@@ -711,5 +714,11 @@ private async Task NotifyOnCompletedAsync()
await actionPair.Item1(actionPair.Item2);
}
}
+
+ public void DelegateRequest(DelegationRule destination)
+ {
+ _requestContext.Delegate(destination);
+ _responseStarted = true;
+ }
}
}
diff --git a/src/Servers/HttpSys/src/IHttpSysRequestDelegationFeature.cs b/src/Servers/HttpSys/src/IHttpSysRequestDelegationFeature.cs
new file mode 100644
index 000000000000..a581cc8683de
--- /dev/null
+++ b/src/Servers/HttpSys/src/IHttpSysRequestDelegationFeature.cs
@@ -0,0 +1,20 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public interface IHttpSysRequestDelegationFeature
+ {
+ ///
+ /// Indicates if the server can delegate this request to another HttpSys request queue.
+ ///
+ bool CanDelegate { get; }
+
+ ///
+ /// Attempt to delegate the request to another Http.Sys request queue. The request body
+ /// must not be read nor the response started before this is invoked. Check
+ /// before invoking.
+ ///
+ void DelegateRequest(DelegationRule destination);
+ }
+}
diff --git a/src/Servers/HttpSys/src/IServerDelegationFeature.cs b/src/Servers/HttpSys/src/IServerDelegationFeature.cs
new file mode 100644
index 000000000000..7353f9f05345
--- /dev/null
+++ b/src/Servers/HttpSys/src/IServerDelegationFeature.cs
@@ -0,0 +1,16 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public interface IServerDelegationFeature
+ {
+ ///
+ /// Create a delegation rule on request queue owned by the server.
+ ///
+ ///
+ /// Creates a that can used to delegate individual requests.
+ ///
+ DelegationRule CreateDelegationRule(string queueName, string urlPrefix);
+ }
+}
diff --git a/src/Servers/HttpSys/src/MessagePump.cs b/src/Servers/HttpSys/src/MessagePump.cs
index 8de84fbb0517..c3ccdda86f45 100644
--- a/src/Servers/HttpSys/src/MessagePump.cs
+++ b/src/Servers/HttpSys/src/MessagePump.cs
@@ -55,6 +55,12 @@ public MessagePump(IOptions options, ILoggerFactory loggerFactor
_serverAddresses = new ServerAddressesFeature();
Features.Set(_serverAddresses);
+ if (HttpApi.IsFeatureSupported(HttpApiTypes.HTTP_FEATURE_ID.HttpFeatureDelegateEx))
+ {
+ var delegationProperty = new ServerDelegationPropertyFeature(Listener.RequestQueue, _logger);
+ Features.Set(delegationProperty);
+ }
+
_maxAccepts = _options.MaxAccepts;
}
diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs
index 6781465bc24f..afa2c7c2ff7f 100644
--- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs
+++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs
@@ -45,6 +45,9 @@ internal static unsafe class HttpApi
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
internal static extern uint HttpCreateUrlGroup(ulong serverSessionId, ulong* urlGroupId, uint reserved);
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern uint HttpFindUrlGroupId(string pFullyQualifiedUrl, SafeHandle requestQueueHandle, ulong* urlGroupId);
+
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern uint HttpAddUrlToUrlGroup(ulong urlGroupId, string pFullyQualifiedUrl, ulong context, uint pReserved);
@@ -70,6 +73,13 @@ internal static extern unsafe uint HttpCreateRequestQueue(HTTPAPI_VERSION versio
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
internal static extern unsafe uint HttpCloseRequestQueue(IntPtr pReqQueueHandle);
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern bool HttpIsFeatureSupported(HTTP_FEATURE_ID feature);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern unsafe uint HttpDelegateRequestEx(SafeHandle pReqQueueHandle, SafeHandle pDelegateQueueHandle, ulong requestId,
+ ulong delegateUrlGroupId, ulong propertyInfoSetSize, HTTP_DELEGATE_REQUEST_PROPERTY_INFO* pRequestPropertyBuffer);
+
internal delegate uint HttpSetRequestPropertyInvoker(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, void* input, uint inputSize, IntPtr overlapped);
private static HTTPAPI_VERSION version;
@@ -145,5 +155,16 @@ internal static bool Supported
return supported;
}
}
+
+ internal static bool IsFeatureSupported(HTTP_FEATURE_ID feature)
+ {
+ try
+ {
+ return HttpIsFeatureSupported(feature);
+ }
+ catch (EntryPointNotFoundException) { }
+
+ return false;
+ }
}
}
diff --git a/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs b/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs
index 4fb2d602808a..ca3d2e79407a 100644
--- a/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs
+++ b/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs
@@ -16,22 +16,44 @@ internal class RequestQueue
Marshal.SizeOf();
private readonly RequestQueueMode _mode;
- private readonly UrlGroup _urlGroup;
private readonly ILogger _logger;
private bool _disposed;
+ internal RequestQueue(string requestQueueName, string urlPrefix, ILogger logger, bool receiver)
+ : this(urlGroup: null, requestQueueName, RequestQueueMode.Attach, logger, receiver)
+ {
+ try
+ {
+ UrlGroup = new UrlGroup(this, UrlPrefix.Create(urlPrefix));
+ }
+ catch
+ {
+ Dispose();
+ throw;
+ }
+ }
+
internal RequestQueue(UrlGroup urlGroup, string requestQueueName, RequestQueueMode mode, ILogger logger)
+ : this(urlGroup, requestQueueName, mode, logger, false)
+ { }
+
+ private RequestQueue(UrlGroup urlGroup, string requestQueueName, RequestQueueMode mode, ILogger logger, bool receiver)
{
_mode = mode;
- _urlGroup = urlGroup;
+ UrlGroup = urlGroup;
_logger = logger;
var flags = HttpApiTypes.HTTP_CREATE_REQUEST_QUEUE_FLAG.None;
Created = true;
+
if (_mode == RequestQueueMode.Attach)
{
flags = HttpApiTypes.HTTP_CREATE_REQUEST_QUEUE_FLAG.OpenExisting;
Created = false;
+ if (receiver)
+ {
+ flags |= HttpApiTypes.HTTP_CREATE_REQUEST_QUEUE_FLAG.Delegation;
+ }
}
var statusCode = HttpApi.HttpCreateRequestQueue(
@@ -54,7 +76,7 @@ internal RequestQueue(UrlGroup urlGroup, string requestQueueName, RequestQueueMo
out requestQueueHandle);
}
- if (flags == HttpApiTypes.HTTP_CREATE_REQUEST_QUEUE_FLAG.OpenExisting && statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_FILE_NOT_FOUND)
+ if (flags.HasFlag(HttpApiTypes.HTTP_CREATE_REQUEST_QUEUE_FLAG.OpenExisting) && statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_FILE_NOT_FOUND)
{
throw new HttpSysException((int)statusCode, $"Failed to attach to the given request queue '{requestQueueName}', the queue could not be found.");
}
@@ -95,6 +117,8 @@ internal RequestQueue(UrlGroup urlGroup, string requestQueueName, RequestQueueMo
internal SafeHandle Handle { get; }
internal ThreadPoolBoundHandle BoundHandle { get; }
+ internal UrlGroup UrlGroup { get; }
+
internal unsafe void AttachToUrlGroup()
{
Debug.Assert(Created);
@@ -108,7 +132,7 @@ internal unsafe void AttachToUrlGroup()
var infoptr = new IntPtr(&info);
- _urlGroup.SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerBindingProperty,
+ UrlGroup.SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerBindingProperty,
infoptr, (uint)BindingInfoSize);
}
@@ -128,7 +152,7 @@ internal unsafe void DetachFromUrlGroup()
var infoptr = new IntPtr(&info);
- _urlGroup.SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerBindingProperty,
+ UrlGroup.SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerBindingProperty,
infoptr, (uint)BindingInfoSize, throwOnError: false);
}
diff --git a/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs b/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs
index 081c2d7e1591..de348d3786ee 100644
--- a/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs
+++ b/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs
@@ -13,6 +13,8 @@ internal class UrlGroup : IDisposable
{
private static readonly int QosInfoSize =
Marshal.SizeOf();
+ private static readonly int RequestPropertyInfoSize =
+ Marshal.SizeOf();
private ServerSession _serverSession;
private ILogger _logger;
@@ -36,6 +38,21 @@ internal unsafe UrlGroup(ServerSession serverSession, ILogger logger)
Id = urlGroupId;
}
+ internal unsafe UrlGroup(RequestQueue requestQueue, UrlPrefix url)
+ {
+ ulong urlGroupId = 0;
+ var statusCode = HttpApi.HttpFindUrlGroupId(
+ url.FullPrefix, requestQueue.Handle, &urlGroupId);
+
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
+ {
+ throw new HttpSysException((int)statusCode);
+ }
+
+ Debug.Assert(urlGroupId != 0, "Invalid id returned by HttpCreateUrlGroup");
+ Id = urlGroupId;
+ }
+
internal ulong Id { get; private set; }
internal unsafe void SetMaxConnections(long maxConnections)
@@ -51,6 +68,15 @@ internal unsafe void SetMaxConnections(long maxConnections)
SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerQosProperty, new IntPtr(&qosSettings), (uint)QosInfoSize);
}
+ internal unsafe void SetDelegationProperty(RequestQueue destination)
+ {
+ var propertyInfo = new HttpApiTypes.HTTP_BINDING_INFO();
+ propertyInfo.Flags = HttpApiTypes.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT;
+ propertyInfo.RequestQueueHandle = destination.Handle.DangerousGetHandle();
+
+ SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerDelegationProperty, new IntPtr(&propertyInfo), (uint)RequestPropertyInfoSize);
+ }
+
internal void SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY property, IntPtr info, uint infosize, bool throwOnError = true)
{
Debug.Assert(info != IntPtr.Zero, "SetUrlGroupProperty called with invalid pointer");
diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs
index 84fb0173933b..494f3e5d6deb 100644
--- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs
+++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs
@@ -61,43 +61,40 @@ internal Request(RequestContext requestContext, NativeRequestContext nativeReque
var rawUrlInBytes = _nativeRequestContext.GetRawUrlInBytes();
var originalPath = RequestUriBuilder.DecodeAndUnescapePath(rawUrlInBytes);
+ PathBase = string.Empty;
+ Path = originalPath;
+
// 'OPTIONS * HTTP/1.1'
if (KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbOPTIONS && string.Equals(RawUrl, "*", StringComparison.Ordinal))
{
PathBase = string.Empty;
Path = string.Empty;
}
- else if (requestContext.Server.RequestQueue.Created)
+ else
{
var prefix = requestContext.Server.Options.UrlPrefixes.GetPrefix((int)nativeRequestContext.UrlContext);
-
- if (originalPath.Length == prefix.PathWithoutTrailingSlash.Length)
- {
- // They matched exactly except for the trailing slash.
- PathBase = originalPath;
- Path = string.Empty;
- }
- else
+ // Prefix may be null if the requested has been transfered to our queue
+ if (!(prefix is null))
{
- // url: /base/path, prefix: /base/, base: /base, path: /path
- // url: /, prefix: /, base: , path: /
- PathBase = originalPath.Substring(0, prefix.PathWithoutTrailingSlash.Length); // Preserve the user input casing
- Path = originalPath.Substring(prefix.PathWithoutTrailingSlash.Length);
+ if (originalPath.Length == prefix.PathWithoutTrailingSlash.Length)
+ {
+ // They matched exactly except for the trailing slash.
+ PathBase = originalPath;
+ Path = string.Empty;
+ }
+ else
+ {
+ // url: /base/path, prefix: /base/, base: /base, path: /path
+ // url: /, prefix: /, base: , path: /
+ PathBase = originalPath.Substring(0, prefix.PathWithoutTrailingSlash.Length); // Preserve the user input casing
+ Path = originalPath.Substring(prefix.PathWithoutTrailingSlash.Length);
+ }
}
- }
- else
- {
- // When attaching to an existing queue, the UrlContext hint may not match our configuration. Search manualy.
- if (requestContext.Server.Options.UrlPrefixes.TryMatchLongestPrefix(IsHttps, cookedUrl.GetHost(), originalPath, out var pathBase, out var path))
+ else if (requestContext.Server.Options.UrlPrefixes.TryMatchLongestPrefix(IsHttps, cookedUrl.GetHost(), originalPath, out var pathBase, out var path))
{
PathBase = pathBase;
Path = path;
}
- else
- {
- PathBase = string.Empty;
- Path = originalPath;
- }
}
ProtocolVersion = _nativeRequestContext.GetVersion();
@@ -350,6 +347,8 @@ public X509Certificate2 ClientCertificate
}
}
+ public bool CanDelegate => !(HasRequestBodyStarted || RequestContext.Response.HasStarted);
+
// Populates the client certificate. The result may be null if there is no client cert.
// TODO: Does it make sense for this to be invoked multiple times (e.g. renegotiate)? Client and server code appear to
// enable this, but it's unclear what Http.Sys would do.
diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs
index 997339ac1458..cc7cee0df359 100644
--- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs
+++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs
@@ -2,8 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
+using System.Runtime.InteropServices;
using System.Security.Authentication.ExtendedProtection;
using System.Security.Principal;
using System.Threading;
@@ -17,7 +19,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys
internal sealed class RequestContext : IDisposable, IThreadPoolWorkItem
{
private static readonly Action