Support attaching to an existing request queue in HTTP.SYS (#14182)

This commit is contained in:
bkatms 2019-10-01 16:05:52 -07:00 committed by Andrew Stanton-Nurse
parent 5df258ae5b
commit 51ae61baca
21 changed files with 839 additions and 51 deletions

View File

@ -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}

View File

@ -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() { }

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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; }

View File

@ -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)

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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,

View File

@ -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();

View File

@ -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
}
}

View File

@ -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
{

View File

@ -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)
{

View File

@ -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
}
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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.

View File

@ -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 })

View File

@ -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 =

View File

@ -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;