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
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Connections.Abstractions", "..\Connections.Abstractions\src\Microsoft.AspNetCore.Connections.Abstractions.csproj", "{00A88B8D-D539-45DD-B071-1E955AF89A4A}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Connections.Abstractions", "..\Connections.Abstractions\src\Microsoft.AspNetCore.Connections.Abstractions.csproj", "{00A88B8D-D539-45DD-B071-1E955AF89A4A}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueueSharing", "samples\QueueSharing\QueueSharing.csproj", "{9B58DF76-DC6D-4728-86B7-40087BDDC897}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|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.ActiveCfg = Release|Any CPU
|
||||||
{00A88B8D-D539-45DD-B071-1E955AF89A4A}.Release|x86.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -329,6 +343,7 @@ Global
|
||||||
{19DC60DE-C413-43A2-985E-0D0F20AD2302} = {4DA3C456-5050-4AC0-A554-795F6DEC8660}
|
{19DC60DE-C413-43A2-985E-0D0F20AD2302} = {4DA3C456-5050-4AC0-A554-795F6DEC8660}
|
||||||
{D93575B3-BFA3-4523-B060-D268D6A0A66B} = {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}
|
{00A88B8D-D539-45DD-B071-1E955AF89A4A} = {4DA3C456-5050-4AC0-A554-795F6DEC8660}
|
||||||
|
{9B58DF76-DC6D-4728-86B7-40087BDDC897} = {3A1E31E3-2794-4CA3-B8E2-253E96BDE514}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {34B42B42-FA09-41AB-9216-14073990C504}
|
SolutionGuid = {34B42B42-FA09-41AB-9216-14073990C504}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
public long? MaxConnections { get { throw null; } set { } }
|
public long? MaxConnections { get { throw null; } set { } }
|
||||||
public long? MaxRequestBodySize { get { throw null; } set { } }
|
public long? MaxRequestBodySize { get { throw null; } set { } }
|
||||||
public long RequestQueueLimit { 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 string RequestQueueName { get { throw null; } set { } }
|
||||||
public bool ThrowWriteExceptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]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; } }
|
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; }
|
System.Collections.Generic.IReadOnlyDictionary<int, System.ReadOnlyMemory<byte>> RequestInfo { get; }
|
||||||
}
|
}
|
||||||
|
public enum RequestQueueMode
|
||||||
|
{
|
||||||
|
Create = 0,
|
||||||
|
Attach = 1,
|
||||||
|
CreateOrAttach = 2,
|
||||||
|
}
|
||||||
public sealed partial class TimeoutManager
|
public sealed partial class TimeoutManager
|
||||||
{
|
{
|
||||||
internal 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
|
public AuthenticationSchemes Schemes
|
||||||
{
|
{
|
||||||
get { return _authSchemes; }
|
get { return _authSchemes; }
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
// V2 initialization sequence:
|
// V2 initialization sequence:
|
||||||
// 1. Create server session
|
// 1. Create server session
|
||||||
// 2. Create url group
|
// 2. Create url group
|
||||||
// 3. Create request queue - Done in Start()
|
// 3. Create request queue
|
||||||
// 4. Add urls to url group - Done in Start()
|
// 4. Add urls to url group - Done in Start()
|
||||||
// 5. Attach request queue 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);
|
_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);
|
_disconnectListener = new DisconnectListener(_requestQueue, Logger);
|
||||||
}
|
}
|
||||||
|
|
@ -147,20 +147,24 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Options.Apply(UrlGroup, RequestQueue);
|
// If this instance created the queue then configure it.
|
||||||
|
if (_requestQueue.Created)
|
||||||
_requestQueue.AttachToUrlGroup();
|
|
||||||
|
|
||||||
// All resources are set up correctly. Now add all prefixes.
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
Options.UrlPrefixes.RegisterAllPrefixes(UrlGroup);
|
Options.Apply(UrlGroup, RequestQueue);
|
||||||
}
|
|
||||||
catch (HttpSysException)
|
_requestQueue.AttachToUrlGroup();
|
||||||
{
|
|
||||||
// If an error occurred while adding prefixes, free all resources allocated by previous steps.
|
// All resources are set up correctly. Now add all prefixes.
|
||||||
_requestQueue.DetachFromUrlGroup();
|
try
|
||||||
throw;
|
{
|
||||||
|
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;
|
_state = State.Started;
|
||||||
|
|
@ -188,11 +192,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
return;
|
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;
|
_state = State.Stopped;
|
||||||
|
|
||||||
_requestQueue.DetachFromUrlGroup();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
get => _requestQueueName;
|
get => _requestQueueName;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (value.Length > MaximumRequestQueueNameLength)
|
if (value?.Length > MaximumRequestQueueNameLength)
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(nameof(value),
|
throw new ArgumentOutOfRangeException(nameof(value),
|
||||||
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>
|
/// <summary>
|
||||||
/// The maximum number of concurrent accepts.
|
/// The maximum number of concurrent accepts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -63,6 +69,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The url prefixes to register with Http.Sys. These may be modified at any time prior to disposing
|
/// The url prefixes to register with Http.Sys. These may be modified at any time prior to disposing
|
||||||
/// the listener.
|
/// the listener.
|
||||||
|
/// When attached to an existing queue the prefixes are only used to compute PathBase for requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UrlPrefixCollection UrlPrefixes { get; } = new UrlPrefixCollection();
|
public UrlPrefixCollection UrlPrefixes { get; } = new UrlPrefixCollection();
|
||||||
|
|
||||||
|
|
@ -75,6 +82,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exposes the Http.Sys timeout configurations. These may also be configured in the registry.
|
/// 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 may be modified at any time prior to disposing the listener.
|
||||||
|
/// These settings do not apply when attaching to an existing queue.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeoutManager Timeouts { get; } = new TimeoutManager();
|
public TimeoutManager Timeouts { get; } = new TimeoutManager();
|
||||||
|
|
||||||
|
|
@ -87,6 +95,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the maximum number of concurrent connections to accept, -1 for infinite, or null to
|
/// 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.
|
/// 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>
|
/// </summary>
|
||||||
public long? MaxConnections
|
public long? MaxConnections
|
||||||
{
|
{
|
||||||
|
|
@ -109,6 +118,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the maximum number of requests that will be queued up in Http.Sys.
|
/// 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>
|
/// </summary>
|
||||||
public long RequestQueueLimit
|
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
|
/// 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
|
/// 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.
|
/// 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>
|
/// </summary>
|
||||||
public Http503VerbosityLevel Http503Verbosity
|
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)
|
internal void Apply(UrlGroup urlGroup, RequestQueue requestQueue)
|
||||||
{
|
{
|
||||||
_urlGroup = urlGroup;
|
_urlGroup = urlGroup;
|
||||||
|
|
|
||||||
|
|
@ -112,13 +112,14 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
Listener.Options.UrlPrefixes.Add(value);
|
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.");
|
LogHelper.LogDebug(_logger, $"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default.");
|
||||||
|
|
||||||
_serverAddresses.Addresses.Add(Constants.DefaultServerAddress);
|
_serverAddresses.Addresses.Add(Constants.DefaultServerAddress);
|
||||||
Listener.Options.UrlPrefixes.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
|
// Can't call Start twice
|
||||||
Contract.Assert(_application == null);
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
|
|
||||||
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
|
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
|
||||||
internal static extern unsafe uint HttpCreateRequestQueue(HTTPAPI_VERSION version, string pName,
|
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)]
|
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
|
||||||
internal static extern unsafe uint HttpCloseRequestQueue(IntPtr pReqQueueHandle);
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.HttpSys.Internal;
|
using Microsoft.AspNetCore.HttpSys.Internal;
|
||||||
|
|
@ -14,20 +15,54 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
private static readonly int BindingInfoSize =
|
private static readonly int BindingInfoSize =
|
||||||
Marshal.SizeOf<HttpApiTypes.HTTP_BINDING_INFO>();
|
Marshal.SizeOf<HttpApiTypes.HTTP_BINDING_INFO>();
|
||||||
|
|
||||||
|
private readonly RequestQueueMode _mode;
|
||||||
private readonly UrlGroup _urlGroup;
|
private readonly UrlGroup _urlGroup;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private bool _disposed;
|
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;
|
_urlGroup = urlGroup;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
HttpRequestQueueV2Handle requestQueueHandle = null;
|
var flags = HttpApiTypes.HTTP_CREATE_REQUEST_QUEUE_FLAG.None;
|
||||||
var statusCode = HttpApi.HttpCreateRequestQueue(
|
Created = true;
|
||||||
HttpApi.Version, requestQueueName, null, 0, out requestQueueHandle);
|
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);
|
throw new HttpSysException((int)statusCode);
|
||||||
}
|
}
|
||||||
|
|
@ -39,18 +74,30 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
UnsafeNclNativeMethods.FileCompletionNotificationModes.SkipCompletionPortOnSuccess |
|
UnsafeNclNativeMethods.FileCompletionNotificationModes.SkipCompletionPortOnSuccess |
|
||||||
UnsafeNclNativeMethods.FileCompletionNotificationModes.SkipSetEventOnHandle))
|
UnsafeNclNativeMethods.FileCompletionNotificationModes.SkipSetEventOnHandle))
|
||||||
{
|
{
|
||||||
|
requestQueueHandle.Dispose();
|
||||||
throw new HttpSysException(Marshal.GetLastWin32Error());
|
throw new HttpSysException(Marshal.GetLastWin32Error());
|
||||||
}
|
}
|
||||||
|
|
||||||
Handle = requestQueueHandle;
|
Handle = requestQueueHandle;
|
||||||
BoundHandle = ThreadPoolBoundHandle.BindHandle(Handle);
|
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 SafeHandle Handle { get; }
|
||||||
internal ThreadPoolBoundHandle BoundHandle { get; }
|
internal ThreadPoolBoundHandle BoundHandle { get; }
|
||||||
|
|
||||||
internal unsafe void AttachToUrlGroup()
|
internal unsafe void AttachToUrlGroup()
|
||||||
{
|
{
|
||||||
|
Debug.Assert(Created);
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
// Set the association between request queue and url group. After this, requests for registered urls will
|
// Set the association between request queue and url group. After this, requests for registered urls will
|
||||||
// get delivered to this request queue.
|
// get delivered to this request queue.
|
||||||
|
|
@ -67,6 +114,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
|
|
||||||
internal unsafe void DetachFromUrlGroup()
|
internal unsafe void DetachFromUrlGroup()
|
||||||
{
|
{
|
||||||
|
Debug.Assert(Created);
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
// Break the association between request queue and url group. After this, requests for registered urls
|
// Break the association between request queue and url group. After this, requests for registered urls
|
||||||
// will get 503s.
|
// will get 503s.
|
||||||
|
|
@ -87,6 +135,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
// The listener must be active for this to work.
|
// The listener must be active for this to work.
|
||||||
internal unsafe void SetLengthLimit(long length)
|
internal unsafe void SetLengthLimit(long length)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(Created);
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
var result = HttpApi.HttpSetRequestQueueProperty(Handle,
|
var result = HttpApi.HttpSetRequestQueueProperty(Handle,
|
||||||
|
|
@ -102,6 +151,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
// The listener must be active for this to work.
|
// The listener must be active for this to work.
|
||||||
internal unsafe void SetRejectionVerbosity(Http503VerbosityLevel verbosity)
|
internal unsafe void SetRejectionVerbosity(Http503VerbosityLevel verbosity)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(Created);
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
var result = HttpApi.HttpSetRequestQueueProperty(Handle,
|
var result = HttpApi.HttpSetRequestQueueProperty(Handle,
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
var cookedUrl = nativeRequestContext.GetCookedUrl();
|
var cookedUrl = nativeRequestContext.GetCookedUrl();
|
||||||
QueryString = cookedUrl.GetQueryString() ?? string.Empty;
|
QueryString = cookedUrl.GetQueryString() ?? string.Empty;
|
||||||
|
|
||||||
var prefix = requestContext.Server.Options.UrlPrefixes.GetPrefix((int)nativeRequestContext.UrlContext);
|
|
||||||
|
|
||||||
var rawUrlInBytes = _nativeRequestContext.GetRawUrlInBytes();
|
var rawUrlInBytes = _nativeRequestContext.GetRawUrlInBytes();
|
||||||
var originalPath = RequestUriBuilder.DecodeAndUnescapePath(rawUrlInBytes);
|
var originalPath = RequestUriBuilder.DecodeAndUnescapePath(rawUrlInBytes);
|
||||||
|
|
||||||
|
|
@ -66,19 +64,37 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
PathBase = string.Empty;
|
PathBase = string.Empty;
|
||||||
Path = string.Empty;
|
Path = string.Empty;
|
||||||
}
|
}
|
||||||
// These paths are both unescaped already.
|
else if (requestContext.Server.RequestQueue.Created)
|
||||||
else if (originalPath.Length == prefix.Path.Length - 1)
|
|
||||||
{
|
{
|
||||||
// They matched exactly except for the trailing slash.
|
var prefix = requestContext.Server.Options.UrlPrefixes.GetPrefix((int)nativeRequestContext.UrlContext);
|
||||||
PathBase = originalPath;
|
|
||||||
Path = string.Empty;
|
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
|
else
|
||||||
{
|
{
|
||||||
// url: /base/path, prefix: /base/, base: /base, path: /path
|
// When attaching to an existing queue, the UrlContext hint may not match our configuration. Search manualy.
|
||||||
// url: /, prefix: /, base: , path: /
|
if (requestContext.Server.Options.UrlPrefixes.TryMatchLongestPrefix(IsHttps, cookedUrl.GetHost(), originalPath, out var pathBase, out var path))
|
||||||
PathBase = originalPath.Substring(0, prefix.Path.Length - 1);
|
{
|
||||||
Path = originalPath.Substring(prefix.Path.Length - 1);
|
PathBase = pathBase;
|
||||||
|
Path = path;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PathBase = string.Empty;
|
||||||
|
Path = originalPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ProtocolVersion = _nativeRequestContext.GetVersion();
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -12,6 +12,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exposes the Http.Sys timeout configurations. These may also be configured in the registry.
|
/// 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>
|
/// </summary>
|
||||||
public sealed class TimeoutManager
|
public sealed class TimeoutManager
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,10 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
Scheme = scheme;
|
Scheme = scheme;
|
||||||
Host = host;
|
Host = host;
|
||||||
Port = port;
|
Port = port;
|
||||||
|
HostAndPort = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", Host, Port);
|
||||||
PortValue = portValue;
|
PortValue = portValue;
|
||||||
Path = path;
|
Path = path;
|
||||||
|
PathWithoutTrailingSlash = Path.Length > 1 ? Path[0..^1] : string.Empty;
|
||||||
FullPrefix = string.Format(CultureInfo.InvariantCulture, "{0}://{1}:{2}{3}", Scheme, Host, Port, Path);
|
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);
|
return Create(scheme, host, port, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsHttps { get; private set; }
|
public bool IsHttps { get; }
|
||||||
public string Scheme { get; private set; }
|
public string Scheme { get; }
|
||||||
public string Host { get; private set; }
|
public string Host { get; }
|
||||||
public string Port { get; private set; }
|
public string Port { get; }
|
||||||
public int PortValue { get; private set; }
|
internal string HostAndPort { get; }
|
||||||
public string Path { get; private set; }
|
public int PortValue { get; }
|
||||||
public string FullPrefix { get; private set; }
|
public string Path { get; }
|
||||||
|
internal string PathWithoutTrailingSlash { get; }
|
||||||
|
public string FullPrefix { get; }
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
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.
|
// 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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.HttpSys
|
namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
{
|
{
|
||||||
|
|
@ -57,10 +59,36 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
{
|
{
|
||||||
lock (_prefixes)
|
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()
|
public void Clear()
|
||||||
{
|
{
|
||||||
lock (_prefixes)
|
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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.HttpSys.Internal;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.HttpSys.Listener
|
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 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)
|
internal static HttpSysListener CreateHttpServer(out string baseAddress)
|
||||||
{
|
{
|
||||||
string root;
|
string root;
|
||||||
|
|
@ -43,16 +52,24 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener
|
||||||
var prefix = UrlPrefix.Create("http", "localhost", port, basePath);
|
var prefix = UrlPrefix.Create("http", "localhost", port, basePath);
|
||||||
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
|
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
|
||||||
baseAddress = prefix.ToString();
|
baseAddress = prefix.ToString();
|
||||||
var listener = new HttpSysListener(new HttpSysOptions(), new LoggerFactory());
|
var options = new HttpSysOptions();
|
||||||
listener.Options.UrlPrefixes.Add(prefix);
|
options.UrlPrefixes.Add(prefix);
|
||||||
|
options.RequestQueueName = prefix.Port; // Convention for use with CreateServerOnExistingQueue
|
||||||
|
var listener = new HttpSysListener(options, new LoggerFactory());
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
listener.Start();
|
listener.Start();
|
||||||
return listener;
|
return listener;
|
||||||
}
|
}
|
||||||
catch (HttpSysException)
|
catch (HttpSysException ex)
|
||||||
{
|
{
|
||||||
listener.Dispose();
|
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;
|
NextPort = BasePort;
|
||||||
|
|
@ -73,6 +90,23 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener
|
||||||
return 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>
|
/// <summary>
|
||||||
/// AcceptAsync extension with timeout. This extension should be used in all tests to prevent
|
/// AcceptAsync extension with timeout. This extension should be used in all tests to prevent
|
||||||
/// unexpected hangs when a request does not arrive.
|
/// unexpected hangs when a request does not arrive.
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,11 @@ using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.HttpSys.Internal;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.HttpSys
|
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]
|
[ConditionalFact]
|
||||||
public async Task Server_SetQueueName_Success()
|
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)
|
private async Task<string> SendRequestAsync(string uri)
|
||||||
{
|
{
|
||||||
using (HttpClient client = new HttpClient() { Timeout = Utilities.DefaultTimeout })
|
using (HttpClient client = new HttpClient() { Timeout = Utilities.DefaultTimeout })
|
||||||
|
|
|
||||||
|
|
@ -616,6 +616,16 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
|
||||||
HTTP_AUTH_ENABLE_KERBEROS = 0x00000010,
|
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
|
internal static class HTTP_RESPONSE_HEADER_ID
|
||||||
{
|
{
|
||||||
private static string[] _strings =
|
private static string[] _strings =
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,13 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
|
||||||
internal static class ErrorCodes
|
internal static class ErrorCodes
|
||||||
{
|
{
|
||||||
internal const uint ERROR_SUCCESS = 0;
|
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_HANDLE_EOF = 38;
|
||||||
internal const uint ERROR_NOT_SUPPORTED = 50;
|
internal const uint ERROR_NOT_SUPPORTED = 50;
|
||||||
internal const uint ERROR_INVALID_PARAMETER = 87;
|
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_ALREADY_EXISTS = 183;
|
||||||
internal const uint ERROR_MORE_DATA = 234;
|
internal const uint ERROR_MORE_DATA = 234;
|
||||||
internal const uint ERROR_OPERATION_ABORTED = 995;
|
internal const uint ERROR_OPERATION_ABORTED = 995;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue