diff --git a/src/Servers/HttpSys/HttpSysServer.sln b/src/Servers/HttpSys/HttpSysServer.sln index b966124ae3..bdd6b9c522 100644 --- a/src/Servers/HttpSys/HttpSysServer.sln +++ b/src/Servers/HttpSys/HttpSysServer.sln @@ -70,6 +70,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Hostin EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Connections.Abstractions", "..\Connections.Abstractions\src\Microsoft.AspNetCore.Connections.Abstractions.csproj", "{00A88B8D-D539-45DD-B071-1E955AF89A4A}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueueSharing", "samples\QueueSharing\QueueSharing.csproj", "{9B58DF76-DC6D-4728-86B7-40087BDDC897}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -304,6 +306,18 @@ Global {00A88B8D-D539-45DD-B071-1E955AF89A4A}.Release|Mixed Platforms.Build.0 = Release|Any CPU {00A88B8D-D539-45DD-B071-1E955AF89A4A}.Release|x86.ActiveCfg = Release|Any CPU {00A88B8D-D539-45DD-B071-1E955AF89A4A}.Release|x86.Build.0 = Release|Any CPU + {9B58DF76-DC6D-4728-86B7-40087BDDC897}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B58DF76-DC6D-4728-86B7-40087BDDC897}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B58DF76-DC6D-4728-86B7-40087BDDC897}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {9B58DF76-DC6D-4728-86B7-40087BDDC897}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {9B58DF76-DC6D-4728-86B7-40087BDDC897}.Debug|x86.ActiveCfg = Debug|Any CPU + {9B58DF76-DC6D-4728-86B7-40087BDDC897}.Debug|x86.Build.0 = Debug|Any CPU + {9B58DF76-DC6D-4728-86B7-40087BDDC897}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B58DF76-DC6D-4728-86B7-40087BDDC897}.Release|Any CPU.Build.0 = Release|Any CPU + {9B58DF76-DC6D-4728-86B7-40087BDDC897}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {9B58DF76-DC6D-4728-86B7-40087BDDC897}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {9B58DF76-DC6D-4728-86B7-40087BDDC897}.Release|x86.ActiveCfg = Release|Any CPU + {9B58DF76-DC6D-4728-86B7-40087BDDC897}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -329,6 +343,7 @@ Global {19DC60DE-C413-43A2-985E-0D0F20AD2302} = {4DA3C456-5050-4AC0-A554-795F6DEC8660} {D93575B3-BFA3-4523-B060-D268D6A0A66B} = {4DA3C456-5050-4AC0-A554-795F6DEC8660} {00A88B8D-D539-45DD-B071-1E955AF89A4A} = {4DA3C456-5050-4AC0-A554-795F6DEC8660} + {9B58DF76-DC6D-4728-86B7-40087BDDC897} = {3A1E31E3-2794-4CA3-B8E2-253E96BDE514} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {34B42B42-FA09-41AB-9216-14073990C504} diff --git a/src/Servers/HttpSys/ref/Microsoft.AspNetCore.Server.HttpSys.netcoreapp.cs b/src/Servers/HttpSys/ref/Microsoft.AspNetCore.Server.HttpSys.netcoreapp.cs index e339f777fa..f25f75f41f 100644 --- a/src/Servers/HttpSys/ref/Microsoft.AspNetCore.Server.HttpSys.netcoreapp.cs +++ b/src/Servers/HttpSys/ref/Microsoft.AspNetCore.Server.HttpSys.netcoreapp.cs @@ -52,6 +52,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys public long? MaxConnections { get { throw null; } set { } } public long? MaxRequestBodySize { get { throw null; } set { } } public long RequestQueueLimit { get { throw null; } set { } } + public Microsoft.AspNetCore.Server.HttpSys.RequestQueueMode RequestQueueMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public string RequestQueueName { get { throw null; } set { } } public bool ThrowWriteExceptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Server.HttpSys.TimeoutManager Timeouts { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } @@ -61,6 +62,12 @@ namespace Microsoft.AspNetCore.Server.HttpSys { System.Collections.Generic.IReadOnlyDictionary> RequestInfo { get; } } + public enum RequestQueueMode + { + Create = 0, + Attach = 1, + CreateOrAttach = 2, + } public sealed partial class TimeoutManager { internal TimeoutManager() { } diff --git a/src/Servers/HttpSys/samples/QueueSharing/Program.cs b/src/Servers/HttpSys/samples/QueueSharing/Program.cs new file mode 100644 index 0000000000..544f04beb9 --- /dev/null +++ b/src/Servers/HttpSys/samples/QueueSharing/Program.cs @@ -0,0 +1,74 @@ +using System; +using System.Xml.Schema; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.HttpSys; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace QueueSharing +{ + public static class Program + { + public static void Main(string[] args) + { + Console.Write("Create and (c)reate, (a)ttach to existing, or attach (o)r create? "); + var key = Console.ReadKey(); + Console.WriteLine(); + var mode = RequestQueueMode.Create; + switch (key.KeyChar) + { + case 'a': + mode = RequestQueueMode.Attach; + break; + case 'o': + mode = RequestQueueMode.CreateOrAttach; + break; + case 'c': + mode = RequestQueueMode.Create; + break; + default: + Console.WriteLine("Unknown option, defaulting to (c)create."); + break; + } + + var host = new HostBuilder() + .ConfigureLogging(factory => factory.AddConsole()) + .ConfigureWebHost(webHost => + { + webHost.UseHttpSys(options => + { + // Skipping this to ensure the server works without any prefixes in attach mode. + if (mode != RequestQueueMode.Attach) + { + options.UrlPrefixes.Add("http://localhost:5002"); + } + options.RequestQueueName = "QueueName"; + options.RequestQueueMode = mode; + options.MaxAccepts = 1; // Better load rotation between instances. + }).ConfigureServices(services => + { + + }).Configure(app => + { + app.Run(async context => + { + context.Response.ContentType = "text/plain"; + // There's a strong connection affinity between processes. Close the connection so the next request can be dispatched to a random instance. + // It appears to be round robin based and switch instances roughly every 30 requests when using new connections. + // This seems related to the default MaxAccepts (5 * processor count). + // I'm told this connection affinity does not apply to HTTP/2. + context.Response.Headers["Connection"] = "close"; + await context.Response.WriteAsync("Hello world from " + context.Request.Host + " at " + DateTime.Now); + }); + }); + + }) + .Build(); + + host.Run(); + } + } +} diff --git a/src/Servers/HttpSys/samples/QueueSharing/QueueSharing.csproj b/src/Servers/HttpSys/samples/QueueSharing/QueueSharing.csproj new file mode 100644 index 0000000000..50b95bc6ce --- /dev/null +++ b/src/Servers/HttpSys/samples/QueueSharing/QueueSharing.csproj @@ -0,0 +1,14 @@ + + + + $(DefaultNetCoreTargetFramework) + Exe + true + + + + + + + + diff --git a/src/Servers/HttpSys/src/AuthenticationManager.cs b/src/Servers/HttpSys/src/AuthenticationManager.cs index b9594d1143..29f5a3495a 100644 --- a/src/Servers/HttpSys/src/AuthenticationManager.cs +++ b/src/Servers/HttpSys/src/AuthenticationManager.cs @@ -32,6 +32,9 @@ namespace Microsoft.AspNetCore.Server.HttpSys { } + /// + /// When attaching to an existing queue this setting must match the one used to create the queue. + /// public AuthenticationSchemes Schemes { get { return _authSchemes; } diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index e8d56eb9bd..24f6c61955 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys // V2 initialization sequence: // 1. Create server session // 2. Create url group - // 3. Create request queue - Done in Start() + // 3. Create request queue // 4. Add urls to url group - Done in Start() // 5. Attach request queue to url group - Done in Start() @@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys _urlGroup = new UrlGroup(_serverSession, Logger); - _requestQueue = new RequestQueue(_urlGroup, options.RequestQueueName, Logger); + _requestQueue = new RequestQueue(_urlGroup, options.RequestQueueName, options.RequestQueueMode, Logger); _disconnectListener = new DisconnectListener(_requestQueue, Logger); } @@ -147,20 +147,24 @@ namespace Microsoft.AspNetCore.Server.HttpSys return; } - Options.Apply(UrlGroup, RequestQueue); - - _requestQueue.AttachToUrlGroup(); - - // All resources are set up correctly. Now add all prefixes. - try + // If this instance created the queue then configure it. + if (_requestQueue.Created) { - Options.UrlPrefixes.RegisterAllPrefixes(UrlGroup); - } - catch (HttpSysException) - { - // If an error occurred while adding prefixes, free all resources allocated by previous steps. - _requestQueue.DetachFromUrlGroup(); - throw; + Options.Apply(UrlGroup, RequestQueue); + + _requestQueue.AttachToUrlGroup(); + + // All resources are set up correctly. Now add all prefixes. + try + { + Options.UrlPrefixes.RegisterAllPrefixes(UrlGroup); + } + catch (HttpSysException) + { + // If an error occurred while adding prefixes, free all resources allocated by previous steps. + _requestQueue.DetachFromUrlGroup(); + throw; + } } _state = State.Started; @@ -188,11 +192,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys return; } - Options.UrlPrefixes.UnregisterAllPrefixes(); + // If this instance created the queue then remove the URL prefixes before shutting down. + if (_requestQueue.Created) + { + Options.UrlPrefixes.UnregisterAllPrefixes(); + _requestQueue.DetachFromUrlGroup(); + } _state = State.Stopped; - _requestQueue.DetachFromUrlGroup(); } } catch (Exception exception) diff --git a/src/Servers/HttpSys/src/HttpSysOptions.cs b/src/Servers/HttpSys/src/HttpSysOptions.cs index 7a2f75d8fc..477a0bda28 100644 --- a/src/Servers/HttpSys/src/HttpSysOptions.cs +++ b/src/Servers/HttpSys/src/HttpSysOptions.cs @@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys get => _requestQueueName; set { - if (value.Length > MaximumRequestQueueNameLength) + if (value?.Length > MaximumRequestQueueNameLength) { throw new ArgumentOutOfRangeException(nameof(value), value, @@ -48,6 +48,12 @@ namespace Microsoft.AspNetCore.Server.HttpSys } } + /// + /// Indicates if this server instance is responsible for creating and configuring the request queue, + /// of if it should attach to an existing queue. The default is to create. + /// + public RequestQueueMode RequestQueueMode { get; set; } + /// /// The maximum number of concurrent accepts. /// @@ -63,6 +69,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys /// /// The url prefixes to register with Http.Sys. These may be modified at any time prior to disposing /// the listener. + /// When attached to an existing queue the prefixes are only used to compute PathBase for requests. /// public UrlPrefixCollection UrlPrefixes { get; } = new UrlPrefixCollection(); @@ -75,6 +82,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys /// /// Exposes the Http.Sys timeout configurations. These may also be configured in the registry. /// These may be modified at any time prior to disposing the listener. + /// These settings do not apply when attaching to an existing queue. /// public TimeoutManager Timeouts { get; } = new TimeoutManager(); @@ -87,6 +95,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys /// /// Gets or sets the maximum number of concurrent connections to accept, -1 for infinite, or null to /// use the machine wide setting from the registry. The default value is null. + /// This settings does not apply when attaching to an existing queue. /// public long? MaxConnections { @@ -109,6 +118,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys /// /// Gets or sets the maximum number of requests that will be queued up in Http.Sys. + /// This settings does not apply when attaching to an existing queue. /// public long RequestQueueLimit { @@ -164,6 +174,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys /// Gets or sets a value that controls how http.sys reacts when rejecting requests due to throttling conditions - like when the request /// queue limit is reached. The default in http.sys is "Basic" which means http.sys is just resetting the TCP connection. IIS uses Limited /// as its default behavior which will result in sending back a 503 - Service Unavailable back to the client. + /// This settings does not apply when attaching to an existing queue. /// public Http503VerbosityLevel Http503Verbosity { @@ -192,6 +203,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } } + // Not called when attaching to an existing queue. internal void Apply(UrlGroup urlGroup, RequestQueue requestQueue) { _urlGroup = urlGroup; diff --git a/src/Servers/HttpSys/src/MessagePump.cs b/src/Servers/HttpSys/src/MessagePump.cs index f829d6e92d..9917e69872 100644 --- a/src/Servers/HttpSys/src/MessagePump.cs +++ b/src/Servers/HttpSys/src/MessagePump.cs @@ -112,13 +112,14 @@ namespace Microsoft.AspNetCore.Server.HttpSys Listener.Options.UrlPrefixes.Add(value); } } - else + else if (Listener.RequestQueue.Created) { LogHelper.LogDebug(_logger, $"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default."); _serverAddresses.Addresses.Add(Constants.DefaultServerAddress); Listener.Options.UrlPrefixes.Add(Constants.DefaultServerAddress); } + // else // Attaching to an existing queue, don't add a default. // Can't call Start twice Contract.Assert(_application == null); diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index 1dc97ef20d..c4e23d3b3d 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)] internal static extern unsafe uint HttpCreateRequestQueue(HTTPAPI_VERSION version, string pName, - UnsafeNclNativeMethods.SECURITY_ATTRIBUTES pSecurityAttributes, uint flags, out HttpRequestQueueV2Handle pReqQueueHandle); + UnsafeNclNativeMethods.SECURITY_ATTRIBUTES pSecurityAttributes, HTTP_CREATE_REQUEST_QUEUE_FLAG flags, out HttpRequestQueueV2Handle pReqQueueHandle); [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)] internal static extern unsafe uint HttpCloseRequestQueue(IntPtr pReqQueueHandle); diff --git a/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs b/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs index 84f107e48d..00ecc78de9 100644 --- a/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs +++ b/src/Servers/HttpSys/src/NativeInterop/RequestQueue.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; using Microsoft.AspNetCore.HttpSys.Internal; @@ -14,20 +15,54 @@ namespace Microsoft.AspNetCore.Server.HttpSys private static readonly int BindingInfoSize = Marshal.SizeOf(); + private readonly RequestQueueMode _mode; private readonly UrlGroup _urlGroup; private readonly ILogger _logger; private bool _disposed; - internal RequestQueue(UrlGroup urlGroup, string requestQueueName, ILogger logger) + internal RequestQueue(UrlGroup urlGroup, string requestQueueName, RequestQueueMode mode, ILogger logger) { + _mode = mode; _urlGroup = urlGroup; _logger = logger; - HttpRequestQueueV2Handle requestQueueHandle = null; - var statusCode = HttpApi.HttpCreateRequestQueue( - HttpApi.Version, requestQueueName, null, 0, out requestQueueHandle); + 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 (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + var statusCode = HttpApi.HttpCreateRequestQueue( + HttpApi.Version, + requestQueueName, + null, + flags, + out var requestQueueHandle); + + if (_mode == RequestQueueMode.CreateOrAttach && statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_ALREADY_EXISTS) + { + // Tried to create, but it already exists so attach to it instead. + Created = false; + flags = HttpApiTypes.HTTP_CREATE_REQUEST_QUEUE_FLAG.OpenExisting; + statusCode = HttpApi.HttpCreateRequestQueue( + HttpApi.Version, + requestQueueName, + null, + flags, + out requestQueueHandle); + } + + if (flags == 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."); + } + else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_INVALID_NAME) + { + throw new HttpSysException((int)statusCode, $"The given request queue name '{requestQueueName}' is invalid."); + } + else if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) { throw new HttpSysException((int)statusCode); } @@ -39,18 +74,30 @@ namespace Microsoft.AspNetCore.Server.HttpSys UnsafeNclNativeMethods.FileCompletionNotificationModes.SkipCompletionPortOnSuccess | UnsafeNclNativeMethods.FileCompletionNotificationModes.SkipSetEventOnHandle)) { + requestQueueHandle.Dispose(); throw new HttpSysException(Marshal.GetLastWin32Error()); } Handle = requestQueueHandle; BoundHandle = ThreadPoolBoundHandle.BindHandle(Handle); + + if (!Created) + { + _logger.LogInformation("Attached to an existing request queue '{requestQueueName}', some options do not apply.", requestQueueName); + } } + /// + /// True if this instace created the queue instead of attaching to an existing one. + /// + internal bool Created { get; } + internal SafeHandle Handle { get; } internal ThreadPoolBoundHandle BoundHandle { get; } internal unsafe void AttachToUrlGroup() { + Debug.Assert(Created); CheckDisposed(); // Set the association between request queue and url group. After this, requests for registered urls will // get delivered to this request queue. @@ -67,6 +114,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal unsafe void DetachFromUrlGroup() { + Debug.Assert(Created); CheckDisposed(); // Break the association between request queue and url group. After this, requests for registered urls // will get 503s. @@ -87,6 +135,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys // The listener must be active for this to work. internal unsafe void SetLengthLimit(long length) { + Debug.Assert(Created); CheckDisposed(); var result = HttpApi.HttpSetRequestQueueProperty(Handle, @@ -102,6 +151,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys // The listener must be active for this to work. internal unsafe void SetRejectionVerbosity(Http503VerbosityLevel verbosity) { + Debug.Assert(Created); CheckDisposed(); var result = HttpApi.HttpSetRequestQueueProperty(Handle, diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 826cf629da..bf663da134 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -55,8 +55,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys var cookedUrl = nativeRequestContext.GetCookedUrl(); QueryString = cookedUrl.GetQueryString() ?? string.Empty; - var prefix = requestContext.Server.Options.UrlPrefixes.GetPrefix((int)nativeRequestContext.UrlContext); - var rawUrlInBytes = _nativeRequestContext.GetRawUrlInBytes(); var originalPath = RequestUriBuilder.DecodeAndUnescapePath(rawUrlInBytes); @@ -66,19 +64,37 @@ namespace Microsoft.AspNetCore.Server.HttpSys PathBase = string.Empty; Path = string.Empty; } - // These paths are both unescaped already. - else if (originalPath.Length == prefix.Path.Length - 1) + else if (requestContext.Server.RequestQueue.Created) { - // They matched exactly except for the trailing slash. - PathBase = originalPath; - Path = string.Empty; + 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 + { + // 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 { - // url: /base/path, prefix: /base/, base: /base, path: /path - // url: /, prefix: /, base: , path: / - PathBase = originalPath.Substring(0, prefix.Path.Length - 1); - Path = originalPath.Substring(prefix.Path.Length - 1); + // 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)) + { + PathBase = pathBase; + Path = path; + } + else + { + PathBase = string.Empty; + Path = originalPath; + } } ProtocolVersion = _nativeRequestContext.GetVersion(); diff --git a/src/Servers/HttpSys/src/RequestQueueMode.cs b/src/Servers/HttpSys/src/RequestQueueMode.cs new file mode 100644 index 0000000000..0f13a8e7b9 --- /dev/null +++ b/src/Servers/HttpSys/src/RequestQueueMode.cs @@ -0,0 +1,27 @@ +// 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 +{ + /// + /// Used to indicate if this server instance should create a new Http.Sys request queue + /// or attach to an existing one. + /// + public enum RequestQueueMode + { + /// + /// Create a new queue. This will fail if there's an existing queue with the same name. + /// + Create = 0, + /// + /// Attach to an existing queue with the name given. This will fail if the queue does not already exist. + /// Most configuration options do not apply when attaching to an existing queue. + /// + Attach, + /// + /// Create a queue with the given name if it does not already exist, otherwise attach to the existing queue. + /// Most configuration options do not apply when attaching to an existing queue. + /// + CreateOrAttach + } +} diff --git a/src/Servers/HttpSys/src/TimeoutManager.cs b/src/Servers/HttpSys/src/TimeoutManager.cs index e9bb62dfce..9f119618f1 100644 --- a/src/Servers/HttpSys/src/TimeoutManager.cs +++ b/src/Servers/HttpSys/src/TimeoutManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -12,6 +12,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys /// /// Exposes the Http.Sys timeout configurations. These may also be configured in the registry. + /// These settings do not apply when attaching to an existing queue. /// public sealed class TimeoutManager { diff --git a/src/Servers/HttpSys/src/UrlPrefix.cs b/src/Servers/HttpSys/src/UrlPrefix.cs index 4286870d71..c013b0e491 100644 --- a/src/Servers/HttpSys/src/UrlPrefix.cs +++ b/src/Servers/HttpSys/src/UrlPrefix.cs @@ -15,8 +15,10 @@ namespace Microsoft.AspNetCore.Server.HttpSys Scheme = scheme; Host = host; Port = port; + HostAndPort = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", Host, Port); PortValue = portValue; Path = path; + PathWithoutTrailingSlash = Path.Length > 1 ? Path[0..^1] : string.Empty; FullPrefix = string.Format(CultureInfo.InvariantCulture, "{0}://{1}:{2}{3}", Scheme, Host, Port, Path); } @@ -144,13 +146,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys return Create(scheme, host, port, path); } - public bool IsHttps { get; private set; } - public string Scheme { get; private set; } - public string Host { get; private set; } - public string Port { get; private set; } - public int PortValue { get; private set; } - public string Path { get; private set; } - public string FullPrefix { get; private set; } + public bool IsHttps { get; } + public string Scheme { get; } + public string Host { get; } + public string Port { get; } + internal string HostAndPort { get; } + public int PortValue { get; } + public string Path { get; } + internal string PathWithoutTrailingSlash { get; } + public string FullPrefix { get; } public override bool Equals(object obj) { diff --git a/src/Servers/HttpSys/src/UrlPrefixCollection.cs b/src/Servers/HttpSys/src/UrlPrefixCollection.cs index 92c50aea09..e38dd9a5d2 100644 --- a/src/Servers/HttpSys/src/UrlPrefixCollection.cs +++ b/src/Servers/HttpSys/src/UrlPrefixCollection.cs @@ -1,8 +1,10 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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 System.Collections; using System.Collections.Generic; +using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Server.HttpSys { @@ -57,10 +59,36 @@ namespace Microsoft.AspNetCore.Server.HttpSys { lock (_prefixes) { - return _prefixes[id]; + return _prefixes.TryGetValue(id, out var prefix) ? prefix : null; } } + internal bool TryMatchLongestPrefix(bool isHttps, string host, string originalPath, out string pathBase, out string remainingPath) + { + var originalPathString = new PathString(originalPath); + var found = false; + pathBase = null; + remainingPath = null; + lock (_prefixes) + { + foreach (var prefix in _prefixes.Values) + { + // The scheme, host, port, and start of path must match. + // Note this does not currently handle prefixes with wildcard subdomains. + if (isHttps == prefix.IsHttps + && string.Equals(host, prefix.HostAndPort, StringComparison.OrdinalIgnoreCase) + && originalPathString.StartsWithSegments(new PathString(prefix.PathWithoutTrailingSlash), StringComparison.OrdinalIgnoreCase, out var remainder) + && (!found || remainder.Value.Length < remainingPath.Length)) // Longest match + { + found = true; + pathBase = originalPath.Substring(0, prefix.PathWithoutTrailingSlash.Length); // Maintain the input casing + remainingPath = remainder.Value; + } + } + } + return found; + } + public void Clear() { lock (_prefixes) @@ -159,4 +187,4 @@ namespace Microsoft.AspNetCore.Server.HttpSys } } } -} \ No newline at end of file +} diff --git a/src/Servers/HttpSys/test/FunctionalTests/Listener/AuthenticationOnExistingQueueTests.cs b/src/Servers/HttpSys/test/FunctionalTests/Listener/AuthenticationOnExistingQueueTests.cs new file mode 100644 index 0000000000..151b2ca5dc --- /dev/null +++ b/src/Servers/HttpSys/test/FunctionalTests/Listener/AuthenticationOnExistingQueueTests.cs @@ -0,0 +1,171 @@ +// 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 System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.HttpSys.Listener +{ + public class AuthenticationOnExistingQueueTests + { + private static readonly bool AllowAnoymous = true; + private static readonly bool DenyAnoymous = false; + + [ConditionalTheory] + [InlineData(AuthenticationSchemes.None)] + [InlineData(AuthenticationSchemes.Negotiate)] + [InlineData(AuthenticationSchemes.NTLM)] + // [InlineData(AuthenticationSchemes.Digest)] + [InlineData(AuthenticationSchemes.Basic)] + [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationSchemes.Digest |*/ AuthenticationSchemes.Basic)] + public async Task AuthTypes_AllowAnonymous_NoChallenge(AuthenticationSchemes authType) + { + using var baseServer = Utilities.CreateHttpAuthServer(authType, AllowAnoymous, out var address); + using var server = Utilities.CreateServerOnExistingQueue(authType, AllowAnoymous, baseServer.Options.RequestQueueName); + + Task responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout); + Assert.NotNull(context.User); + Assert.False(context.User.Identity.IsAuthenticated); + Assert.Equal(authType, context.Response.AuthenticationChallenges); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers.WwwAuthenticate); + } + + [ConditionalTheory] + [InlineData(AuthenticationSchemes.Negotiate)] + [InlineData(AuthenticationSchemes.NTLM)] + // [InlineData(AuthenticationType.Digest)] // TODO: Not implemented + [InlineData(AuthenticationSchemes.Basic)] + public async Task AuthType_RequireAuth_ChallengesAdded(AuthenticationSchemes authType) + { + using var baseServer = Utilities.CreateHttpAuthServer(authType, DenyAnoymous, out var address); + using var server = Utilities.CreateServerOnExistingQueue(authType, DenyAnoymous, baseServer.Options.RequestQueueName); + + Task responseTask = SendRequestAsync(address); + + var contextTask = server.AcceptAsync(Utilities.DefaultTimeout); // Fails when the server shuts down, the challenge happens internally. + var response = await responseTask; + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal(authType.ToString(), response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase); + } + + [ConditionalTheory] + [InlineData(AuthenticationSchemes.Negotiate)] + [InlineData(AuthenticationSchemes.NTLM)] + // [InlineData(AuthenticationSchemes.Digest)] // TODO: Not implemented + [InlineData(AuthenticationSchemes.Basic)] + public async Task AuthType_AllowAnonymousButSpecify401_ChallengesAdded(AuthenticationSchemes authType) + { + using var baseServer = Utilities.CreateHttpAuthServer(authType, AllowAnoymous, out var address); + using var server = Utilities.CreateServerOnExistingQueue(authType, AllowAnoymous, baseServer.Options.RequestQueueName); + + Task responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout); + Assert.NotNull(context.User); + Assert.False(context.User.Identity.IsAuthenticated); + Assert.Equal(authType, context.Response.AuthenticationChallenges); + context.Response.StatusCode = 401; + context.Dispose(); + + var response = await responseTask; + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal(authType.ToString(), response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase); + } + + [ConditionalFact] + public async Task MultipleAuthTypes_AllowAnonymousButSpecify401_ChallengesAdded() + { + AuthenticationSchemes authType = + AuthenticationSchemes.Negotiate + | AuthenticationSchemes.NTLM + /* | AuthenticationSchemes.Digest TODO: Not implemented */ + | AuthenticationSchemes.Basic; + using var baseServer = Utilities.CreateHttpAuthServer(authType, AllowAnoymous, out var address); + using var server = Utilities.CreateServerOnExistingQueue(authType, AllowAnoymous, baseServer.Options.RequestQueueName); + + Task responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout); + Assert.NotNull(context.User); + Assert.False(context.User.Identity.IsAuthenticated); + Assert.Equal(authType, context.Response.AuthenticationChallenges); + context.Response.StatusCode = 401; + context.Dispose(); + + var response = await responseTask; + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal("Negotiate, NTLM, basic", response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase); + } + + [ConditionalTheory] + [InlineData(AuthenticationSchemes.Negotiate)] + [InlineData(AuthenticationSchemes.NTLM)] + // [InlineData(AuthenticationSchemes.Digest)] // TODO: Not implemented + // [InlineData(AuthenticationSchemes.Basic)] // Doesn't work with default creds + [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationType.Digest |*/ AuthenticationSchemes.Basic)] + public async Task AuthTypes_AllowAnonymousButSpecify401_Success(AuthenticationSchemes authType) + { + using var baseServer = Utilities.CreateHttpAuthServer(authType, AllowAnoymous, out var address); + using var server = Utilities.CreateServerOnExistingQueue(authType, AllowAnoymous, baseServer.Options.RequestQueueName); + + Task responseTask = SendRequestAsync(address, useDefaultCredentials: true); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout); + Assert.NotNull(context.User); + Assert.False(context.User.Identity.IsAuthenticated); + Assert.Equal(authType, context.Response.AuthenticationChallenges); + context.Response.StatusCode = 401; + context.Dispose(); + + context = await server.AcceptAsync(Utilities.DefaultTimeout); + Assert.NotNull(context.User); + Assert.True(context.User.Identity.IsAuthenticated); + Assert.Equal(authType, context.Response.AuthenticationChallenges); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [ConditionalTheory] + [InlineData(AuthenticationSchemes.Negotiate)] + [InlineData(AuthenticationSchemes.NTLM)] + // [InlineData(AuthenticationSchemes.Digest)] // TODO: Not implemented + // [InlineData(AuthenticationSchemes.Basic)] // Doesn't work with default creds + [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationType.Digest |*/ AuthenticationSchemes.Basic)] + public async Task AuthTypes_RequireAuth_Success(AuthenticationSchemes authType) + { + using var baseServer = Utilities.CreateHttpAuthServer(authType, DenyAnoymous, out var address); + using var server = Utilities.CreateServerOnExistingQueue(authType, DenyAnoymous, baseServer.Options.RequestQueueName); + + Task responseTask = SendRequestAsync(address, useDefaultCredentials: true); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout); + Assert.NotNull(context.User); + Assert.True(context.User.Identity.IsAuthenticated); + Assert.Equal(authType, context.Response.AuthenticationChallenges); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + private async Task SendRequestAsync(string uri, bool useDefaultCredentials = false) + { + HttpClientHandler handler = new HttpClientHandler(); + handler.UseDefaultCredentials = useDefaultCredentials; + using HttpClient client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(5) }; + return await client.GetAsync(uri); + } + } +} diff --git a/src/Servers/HttpSys/test/FunctionalTests/Listener/ServerOnExistingQueueTests.cs b/src/Servers/HttpSys/test/FunctionalTests/Listener/ServerOnExistingQueueTests.cs new file mode 100644 index 0000000000..eb3277dc9c --- /dev/null +++ b/src/Servers/HttpSys/test/FunctionalTests/Listener/ServerOnExistingQueueTests.cs @@ -0,0 +1,249 @@ +// 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 System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.HttpSys.Listener +{ + public class ServerOnExistingQueueTests + { + [ConditionalFact] + public async Task Server_200OK_Success() + { + using var baseServer = Utilities.CreateHttpServer(out var address); + using var server = Utilities.CreateServerOnExistingQueue(baseServer.Options.RequestQueueName); + + var responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(string.Empty, response); + } + + [ConditionalFact] + public async Task Server_SendHelloWorld_Success() + { + using var baseServer = Utilities.CreateHttpServer(out var address); + using var server = Utilities.CreateServerOnExistingQueue(baseServer.Options.RequestQueueName); + + Task responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout); + context.Response.ContentLength = 11; + await using (var writer = new StreamWriter(context.Response.Body)) + { + await writer.WriteAsync("Hello World"); + } + + string response = await responseTask; + Assert.Equal("Hello World", response); + } + + [ConditionalFact] + public async Task Server_EchoHelloWorld_Success() + { + using var baseServer = Utilities.CreateHttpServer(out var address); + using var server = Utilities.CreateServerOnExistingQueue(baseServer.Options.RequestQueueName); + + var responseTask = SendRequestAsync(address, "Hello World"); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout); + string input = await new StreamReader(context.Request.Body).ReadToEndAsync(); + Assert.Equal("Hello World", input); + context.Response.ContentLength = 11; + await using (var writer = new StreamWriter(context.Response.Body)) + { + await writer.WriteAsync("Hello World"); + } + + var response = await responseTask; + Assert.Equal("Hello World", response); + } + + [ConditionalFact] + // No-ops if you did not create the queue + public async Task Server_SetQueueLimit_Success() + { + using var baseServer = Utilities.CreateHttpServer(out var address); + using var server = Utilities.CreateServerOnExistingQueue(baseServer.Options.RequestQueueName); + server.Options.RequestQueueLimit = 1001; + var responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(string.Empty, response); + } + + [ConditionalFact] + public async Task Server_PathBase_Success() + { + using var baseServer = Utilities.CreateDynamicHttpServer("/PathBase", out var root, out var address); + using var server = Utilities.CreateServerOnExistingQueue(baseServer.Options.RequestQueueName); + server.Options.UrlPrefixes.Add(address); // Need to mirror the setting so we can parse out PathBase + + var responseTask = SendRequestAsync(root + "/pathBase/paTh"); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout).Before(responseTask); + Assert.Equal("/pathBase", context.Request.PathBase); + Assert.Equal("/paTh", context.Request.Path); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(string.Empty, response); + } + + [ConditionalFact] + public async Task Server_PathBaseMismatch_Success() + { + using var baseServer = Utilities.CreateDynamicHttpServer("/PathBase", out var root, out var address); + using var server = Utilities.CreateServerOnExistingQueue(baseServer.Options.RequestQueueName); + + var responseTask = SendRequestAsync(root + "/pathBase/paTh"); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout).Before(responseTask); + Assert.Equal(string.Empty, context.Request.PathBase); + Assert.Equal("/pathBase/paTh", context.Request.Path); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(string.Empty, response); + } + + [ConditionalTheory] + [InlineData("/", "/", "", "/")] + [InlineData("/basepath/", "/basepath", "/basepath", "")] + [InlineData("/basepath/", "/basepath/", "/basepath", "/")] + [InlineData("/basepath/", "/basepath/subpath", "/basepath", "/subpath")] + [InlineData("/base path/", "/base%20path/sub%20path", "/base path", "/sub path")] + [InlineData("/base葉path/", "/base%E8%91%89path/sub%E8%91%89path", "/base葉path", "/sub葉path")] + [InlineData("/basepath/", "/basepath/sub%2Fpath", "/basepath", "/sub%2Fpath")] + public async Task Server_PathSplitting(string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + using var baseServer = Utilities.CreateDynamicHttpServer(pathBase, out var root, out var baseAddress); + using var server = Utilities.CreateServerOnExistingQueue(baseServer.Options.RequestQueueName); + server.Options.UrlPrefixes.Add(baseAddress); // Keep them in sync + + var responseTask = SendRequestAsync(root + requestPath); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout).Before(responseTask); + Assert.Equal(expectedPathBase, context.Request.PathBase); + Assert.Equal(expectedPath, context.Request.Path); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(string.Empty, response); + } + + [ConditionalFact] + public async Task Server_LongestPathSplitting() + { + using var baseServer = Utilities.CreateDynamicHttpServer("/basepath", out var root, out var baseAddress); + baseServer.Options.UrlPrefixes.Add(baseAddress + "secondTier"); + using var server = Utilities.CreateServerOnExistingQueue(baseServer.Options.RequestQueueName); + server.Options.UrlPrefixes.Add(baseAddress); // Keep them in sync + server.Options.UrlPrefixes.Add(baseAddress + "secondTier"); + + var responseTask = SendRequestAsync(root + "/basepath/secondTier/path/thing"); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout).Before(responseTask); + Assert.Equal("/basepath/secondTier", context.Request.PathBase); + Assert.Equal("/path/thing", context.Request.Path); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(string.Empty, response); + } + + [ConditionalFact] + // Changes to the base server are reflected + public async Task Server_HotAddPrefix_Success() + { + using var baseServer = Utilities.CreateHttpServer(out var address); + using var server = Utilities.CreateServerOnExistingQueue(baseServer.Options.RequestQueueName); + server.Options.UrlPrefixes.Add(address); // Keep them in sync + + var responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout).Before(responseTask); + Assert.Equal(string.Empty, context.Request.PathBase); + Assert.Equal("/", context.Request.Path); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(string.Empty, response); + + address += "pathbase/"; + baseServer.Options.UrlPrefixes.Add(address); + server.Options.UrlPrefixes.Add(address); + + responseTask = SendRequestAsync(address); + + context = await server.AcceptAsync(Utilities.DefaultTimeout).Before(responseTask); + Assert.Equal("/pathbase", context.Request.PathBase); + Assert.Equal("/", context.Request.Path); + context.Dispose(); + + response = await responseTask; + Assert.Equal(string.Empty, response); + } + + [ConditionalFact] + // Changes to the base server are reflected + public async Task Server_HotRemovePrefix_Success() + { + using var baseServer = Utilities.CreateHttpServer(out var address); + using var server = Utilities.CreateServerOnExistingQueue(baseServer.Options.RequestQueueName); + server.Options.UrlPrefixes.Add(address); // Keep them in sync + + address += "pathbase/"; + baseServer.Options.UrlPrefixes.Add(address); + server.Options.UrlPrefixes.Add(address); + var responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(Utilities.DefaultTimeout).Before(responseTask); + Assert.Equal("/pathbase", context.Request.PathBase); + Assert.Equal("/", context.Request.Path); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(string.Empty, response); + + Assert.True(baseServer.Options.UrlPrefixes.Remove(address)); + Assert.True(server.Options.UrlPrefixes.Remove(address)); + + responseTask = SendRequestAsync(address); + + context = await server.AcceptAsync(Utilities.DefaultTimeout).Before(responseTask); + Assert.Equal(string.Empty, context.Request.PathBase); + Assert.Equal("/pathbase/", context.Request.Path); + context.Dispose(); + + response = await responseTask; + Assert.Equal(string.Empty, response); + } + + private async Task SendRequestAsync(string uri) + { + using HttpClient client = new HttpClient(); + return await client.GetStringAsync(uri); + } + + private async Task SendRequestAsync(string uri, string upload) + { + using HttpClient client = new HttpClient(); + HttpResponseMessage response = await client.PostAsync(uri, new StringContent(upload)); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + } +} diff --git a/src/Servers/HttpSys/test/FunctionalTests/Listener/Utilities.cs b/src/Servers/HttpSys/test/FunctionalTests/Listener/Utilities.cs index df96652463..2448b3427a 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Listener/Utilities.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Listener/Utilities.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.HttpSys.Listener @@ -21,6 +22,14 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15); + internal static HttpSysListener CreateHttpAuthServer(AuthenticationSchemes authType, bool allowAnonymous, out string baseAddress) + { + var listener = CreateHttpServer(out baseAddress); + listener.Options.Authentication.Schemes = authType; + listener.Options.Authentication.AllowAnonymous = allowAnonymous; + return listener; + } + internal static HttpSysListener CreateHttpServer(out string baseAddress) { string root; @@ -43,16 +52,24 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener var prefix = UrlPrefix.Create("http", "localhost", port, basePath); root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port; baseAddress = prefix.ToString(); - var listener = new HttpSysListener(new HttpSysOptions(), new LoggerFactory()); - listener.Options.UrlPrefixes.Add(prefix); + var options = new HttpSysOptions(); + options.UrlPrefixes.Add(prefix); + options.RequestQueueName = prefix.Port; // Convention for use with CreateServerOnExistingQueue + var listener = new HttpSysListener(options, new LoggerFactory()); try { listener.Start(); return listener; } - catch (HttpSysException) + catch (HttpSysException ex) { listener.Dispose(); + if (ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_ALREADY_EXISTS + && ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SHARING_VIOLATION + && ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_ACCESS_DENIED) + { + throw; + } } } NextPort = BasePort; @@ -73,6 +90,23 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener return listener; } + internal static HttpSysListener CreateServerOnExistingQueue(string requestQueueName) + { + return CreateServerOnExistingQueue(AuthenticationSchemes.None, true, requestQueueName); + } + + internal static HttpSysListener CreateServerOnExistingQueue(AuthenticationSchemes authScheme, bool allowAnonymos, string requestQueueName) + { + var options = new HttpSysOptions(); + options.RequestQueueMode = RequestQueueMode.Attach; + options.RequestQueueName = requestQueueName; + options.Authentication.Schemes = authScheme; + options.Authentication.AllowAnonymous = allowAnonymos; + var listener = new HttpSysListener(options, new LoggerFactory()); + listener.Start(); + return listener; + } + /// /// AcceptAsync extension with timeout. This extension should be used in all tests to prevent /// unexpected hangs when a request does not arrive. diff --git a/src/Servers/HttpSys/test/FunctionalTests/ServerTests.cs b/src/Servers/HttpSys/test/FunctionalTests/ServerTests.cs index 2bab8a1bf5..1816fdcc28 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/ServerTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/ServerTests.cs @@ -11,8 +11,11 @@ using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Server.HttpSys @@ -33,6 +36,44 @@ namespace Microsoft.AspNetCore.Server.HttpSys } } + [ConditionalFact] + public async Task Server_ConnectExistingQueueName_Success() + { + string address; + var queueName = Guid.NewGuid().ToString(); + + // First create the queue. + HttpRequestQueueV2Handle requestQueueHandle = null; + var statusCode = HttpApi.HttpCreateRequestQueue( + HttpApi.Version, + queueName, + null, + 0, + out requestQueueHandle); + + Assert.True(statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS); + + // Now attach to the existing one + using (Utilities.CreateHttpServer(out address, httpContext => + { + return Task.FromResult(0); + }, options => + { + options.RequestQueueName = queueName; + options.RequestQueueMode = RequestQueueMode.Attach; + })) + { + var psi = new ProcessStartInfo("netsh", "http show servicestate view=requestq") + { + RedirectStandardOutput = true + }; + using var process = Process.Start(psi); + process.Start(); + var netshOutput = await process.StandardOutput.ReadToEndAsync(); + Assert.Contains(queueName, netshOutput); + } + } + [ConditionalFact] public async Task Server_SetQueueName_Success() { @@ -585,6 +626,25 @@ namespace Microsoft.AspNetCore.Server.HttpSys } } + [ConditionalFact] + public async Task Server_AttachToExistingQueue_NoIServerAddresses_NoDefaultAdded() + { + var queueName = Guid.NewGuid().ToString(); + using var server = Utilities.CreateHttpServer(out var address, httpContext => Task.CompletedTask, options => + { + options.RequestQueueName = queueName; + }); + using var attachedServer = Utilities.CreatePump(options => + { + options.RequestQueueName = queueName; + options.RequestQueueMode = RequestQueueMode.Attach; + }); + await attachedServer.StartAsync(new DummyApplication(context => Task.CompletedTask), default); + var addressesFeature = attachedServer.Features.Get(); + Assert.Empty(addressesFeature.Addresses); + Assert.Empty(attachedServer.Listener.Options.UrlPrefixes); + } + private async Task SendRequestAsync(string uri) { using (HttpClient client = new HttpClient() { Timeout = Utilities.DefaultTimeout }) diff --git a/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs b/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs index 80a06607cb..aa5d884061 100644 --- a/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs +++ b/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs @@ -616,6 +616,16 @@ namespace Microsoft.AspNetCore.HttpSys.Internal HTTP_AUTH_ENABLE_KERBEROS = 0x00000010, } + [Flags] + internal enum HTTP_CREATE_REQUEST_QUEUE_FLAG : uint + { + None = 0, + // The HTTP_CREATE_REQUEST_QUEUE_FLAG_OPEN_EXISTING flag allows applications to open an existing request queue by name and retrieve the request queue handle. The pName parameter must contain a valid request queue name; it cannot be NULL. + OpenExisting = 1, + // The handle to the request queue created using this flag cannot be used to perform I/O operations. This flag can be set only when the request queue handle is created. + Controller = 2, + } + internal static class HTTP_RESPONSE_HEADER_ID { private static string[] _strings = diff --git a/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs b/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs index 4483cbc306..14c55cefcb 100644 --- a/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs +++ b/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs @@ -24,9 +24,13 @@ namespace Microsoft.AspNetCore.HttpSys.Internal internal static class ErrorCodes { internal const uint ERROR_SUCCESS = 0; + internal const uint ERROR_FILE_NOT_FOUND = 2; + internal const uint ERROR_ACCESS_DENIED = 5; + internal const uint ERROR_SHARING_VIOLATION = 32; internal const uint ERROR_HANDLE_EOF = 38; internal const uint ERROR_NOT_SUPPORTED = 50; internal const uint ERROR_INVALID_PARAMETER = 87; + internal const uint ERROR_INVALID_NAME = 123; internal const uint ERROR_ALREADY_EXISTS = 183; internal const uint ERROR_MORE_DATA = 234; internal const uint ERROR_OPERATION_ABORTED = 995;