Support attaching to an existing request queue in HTTP.SYS (#14182)
This commit is contained in:
parent
5df258ae5b
commit
51ae61baca
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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<int, System.ReadOnlyMemory<byte>> RequestInfo { get; }
|
||||
}
|
||||
public enum RequestQueueMode
|
||||
{
|
||||
Create = 0,
|
||||
Attach = 1,
|
||||
CreateOrAttach = 2,
|
||||
}
|
||||
public sealed partial class TimeoutManager
|
||||
{
|
||||
internal TimeoutManager() { }
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Server.HttpSys" />
|
||||
<Reference Include="Microsoft.Extensions.Hosting" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Console" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -32,6 +32,9 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When attaching to an existing queue this setting must match the one used to create the queue.
|
||||
/// </summary>
|
||||
public AuthenticationSchemes Schemes
|
||||
{
|
||||
get { return _authSchemes; }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public RequestQueueMode RequestQueueMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of concurrent accepts.
|
||||
/// </summary>
|
||||
|
|
@ -63,6 +69,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public UrlPrefixCollection UrlPrefixes { get; } = new UrlPrefixCollection();
|
||||
|
||||
|
|
@ -75,6 +82,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public TimeoutManager Timeouts { get; } = new TimeoutManager();
|
||||
|
||||
|
|
@ -87,6 +95,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public long? MaxConnections
|
||||
{
|
||||
|
|
@ -109,6 +118,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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.
|
||||
/// </summary>
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<HttpApiTypes.HTTP_BINDING_INFO>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if this instace created the queue instead of attaching to an existing one.
|
||||
/// </summary>
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to indicate if this server instance should create a new Http.Sys request queue
|
||||
/// or attach to an existing one.
|
||||
/// </summary>
|
||||
public enum RequestQueueMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new queue. This will fail if there's an existing queue with the same name.
|
||||
/// </summary>
|
||||
Create = 0,
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Attach,
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
CreateOrAttach
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class TimeoutManager
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> 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<string> SendRequestAsync(string uri)
|
||||
{
|
||||
using HttpClient client = new HttpClient();
|
||||
return await client.GetStringAsync(uri);
|
||||
}
|
||||
|
||||
private async Task<string> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AcceptAsync extension with timeout. This extension should be used in all tests to prevent
|
||||
/// unexpected hangs when a request does not arrive.
|
||||
|
|
|
|||
|
|
@ -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<IServerAddressesFeature>();
|
||||
Assert.Empty(addressesFeature.Addresses);
|
||||
Assert.Empty(attachedServer.Listener.Options.UrlPrefixes);
|
||||
}
|
||||
|
||||
private async Task<string> SendRequestAsync(string uri)
|
||||
{
|
||||
using (HttpClient client = new HttpClient() { Timeout = Utilities.DefaultTimeout })
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue