diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysListener.cs b/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysListener.cs
index c2bea54052..56d1e4356d 100644
--- a/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysListener.cs
+++ b/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysListener.cs
@@ -146,9 +146,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
return;
}
- Options.Authentication.SetUrlGroupSecurity(UrlGroup);
- Options.Timeouts.SetUrlGroupTimeouts(UrlGroup);
- Options.SetRequestQueueLimit(RequestQueue);
+ Options.Apply(UrlGroup, RequestQueue);
_requestQueue.AttachToUrlGroup();
diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs b/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs
index 85c8dcc2e1..7fc7089a15 100644
--- a/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs
+++ b/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs
@@ -12,7 +12,9 @@ namespace Microsoft.AspNetCore.Server.HttpSys
// The native request queue
private long _requestQueueLength = DefaultRequestQueueLength;
+ private long? _maxConnections;
private RequestQueue _requestQueue;
+ private UrlGroup _urlGroup;
public HttpSysOptions()
{
@@ -54,6 +56,29 @@ namespace Microsoft.AspNetCore.Server.HttpSys
///
public bool ThrowWriteExceptions { get; set; }
+ ///
+ /// 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.
+ ///
+ public long? MaxConnections
+ {
+ get => _maxConnections;
+ set
+ {
+ if (value.HasValue && value < -1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), value, string.Empty);
+ }
+
+ if (value.HasValue && _urlGroup != null)
+ {
+ _urlGroup.SetMaxConnections(value.Value);
+ }
+
+ _maxConnections = value;
+ }
+ }
+
///
/// Gets or sets the maximum number of requests that will be queued up in Http.Sys.
///
@@ -79,13 +104,23 @@ namespace Microsoft.AspNetCore.Server.HttpSys
}
}
- internal void SetRequestQueueLimit(RequestQueue requestQueue)
+ internal void Apply(UrlGroup urlGroup, RequestQueue requestQueue)
{
+ _urlGroup = urlGroup;
_requestQueue = requestQueue;
+
+ if (_maxConnections.HasValue)
+ {
+ _urlGroup.SetMaxConnections(_maxConnections.Value);
+ }
+
if (_requestQueueLength != DefaultRequestQueueLength)
{
_requestQueue.SetLengthLimit(_requestQueueLength);
}
+
+ Authentication.SetUrlGroupSecurity(urlGroup);
+ Timeouts.SetUrlGroupTimeouts(urlGroup);
}
}
}
diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpApi.cs b/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpApi.cs
index fbea3b2fbc..94bc558af5 100644
--- a/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpApi.cs
+++ b/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpApi.cs
@@ -544,6 +544,13 @@ namespace Microsoft.AspNetCore.Server.HttpSys
HttpRequestAuthTypeKerberos
}
+ internal enum HTTP_QOS_SETTING_TYPE
+ {
+ HttpQosSettingTypeBandwidth,
+ HttpQosSettingTypeConnectionLimit,
+ HttpQosSettingTypeFlowRate
+ }
+
[StructLayout(LayoutKind.Sequential)]
internal struct HTTP_SERVER_AUTHENTICATION_INFO
{
@@ -604,6 +611,20 @@ namespace Microsoft.AspNetCore.Server.HttpSys
internal IntPtr RequestQueueHandle;
}
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_CONNECTION_LIMIT_INFO
+ {
+ internal HTTP_FLAGS Flags;
+ internal uint MaxConnections;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_QOS_SETTING_INFO
+ {
+ internal HTTP_QOS_SETTING_TYPE QosType;
+ internal IntPtr QosSetting;
+ }
+
// see http.w for definitions
[Flags]
internal enum HTTP_FLAGS : uint
diff --git a/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/UrlGroup.cs b/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/UrlGroup.cs
index 2e69c0bcb4..cdeaae7800 100644
--- a/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/UrlGroup.cs
+++ b/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/UrlGroup.cs
@@ -3,12 +3,16 @@
using System;
using System.Diagnostics;
+using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.HttpSys
{
internal class UrlGroup : IDisposable
{
+ private static readonly int QosInfoSize =
+ Marshal.SizeOf();
+
private ServerSession _serverSession;
private ILogger _logger;
private bool _disposed;
@@ -33,6 +37,19 @@ namespace Microsoft.AspNetCore.Server.HttpSys
internal ulong Id { get; private set; }
+ internal unsafe void SetMaxConnections(long maxConnections)
+ {
+ var connectionLimit = new HttpApi.HTTP_CONNECTION_LIMIT_INFO();
+ connectionLimit.Flags = HttpApi.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT;
+ connectionLimit.MaxConnections = (uint)maxConnections;
+
+ var qosSettings = new HttpApi.HTTP_QOS_SETTING_INFO();
+ qosSettings.QosType = HttpApi.HTTP_QOS_SETTING_TYPE.HttpQosSettingTypeConnectionLimit;
+ qosSettings.QosSetting = new IntPtr(&connectionLimit);
+
+ SetProperty(HttpApi.HTTP_SERVER_PROPERTY.HttpServerQosProperty, new IntPtr(&qosSettings), (uint)QosInfoSize);
+ }
+
internal void SetProperty(HttpApi.HTTP_SERVER_PROPERTY property, IntPtr info, uint infosize, bool throwOnError = true)
{
Debug.Assert(info != IntPtr.Zero, "SetUrlGroupProperty called with invalid pointer");
diff --git a/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs b/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs
index 28ec77a4fd..4d8129d7f4 100644
--- a/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs
+++ b/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs
@@ -300,6 +300,112 @@ namespace Microsoft.AspNetCore.Server.HttpSys
}
}
+ [ConditionalFact]
+ public void Server_SetConnectionLimitArgumentValidation_Success()
+ {
+ var server = Utilities.CreatePump();
+
+ Assert.Null(server.Listener.Options.MaxConnections);
+ Assert.Throws(() => server.Listener.Options.MaxConnections = -2);
+ Assert.Null(server.Listener.Options.MaxConnections);
+ server.Listener.Options.MaxConnections = null;
+ server.Listener.Options.MaxConnections = 3;
+ }
+
+ [ConditionalFact]
+ public async Task Server_SetConnectionLimit_Success()
+ {
+ // This is just to get a dynamic port
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext => Task.FromResult(0))) { }
+
+ var server = Utilities.CreatePump();
+ server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(address));
+ Assert.Null(server.Listener.Options.MaxConnections);
+ server.Listener.Options.MaxConnections = 3;
+
+ using (server)
+ {
+ await server.StartAsync(new DummyApplication(), CancellationToken.None);
+
+ using (var client1 = await SendHungRequestAsync("GET", address))
+ using (var client2 = await SendHungRequestAsync("GET", address))
+ {
+ using (var client3 = await SendHungRequestAsync("GET", address))
+ {
+ // Maxed out, refuses connection and throws
+ await Assert.ThrowsAsync(() => SendRequestAsync(address));
+ }
+
+ // A connection has been closed, try again.
+ string responseText = await SendRequestAsync(address);
+ Assert.Equal(string.Empty, responseText);
+ }
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_SetConnectionLimitChangeAfterStarted_Success()
+ {
+ // This is just to get a dynamic port
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext => Task.FromResult(0))) { }
+
+ var server = Utilities.CreatePump();
+ server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(address));
+ Assert.Null(server.Listener.Options.MaxConnections);
+ server.Listener.Options.MaxConnections = 3;
+
+ using (server)
+ {
+ await server.StartAsync(new DummyApplication(), CancellationToken.None);
+
+ using (var client1 = await SendHungRequestAsync("GET", address))
+ using (var client2 = await SendHungRequestAsync("GET", address))
+ using (var client3 = await SendHungRequestAsync("GET", address))
+ {
+ // Maxed out, refuses connection and throws
+ await Assert.ThrowsAsync(() => SendRequestAsync(address));
+
+ server.Listener.Options.MaxConnections = 4;
+
+ string responseText = await SendRequestAsync(address);
+ Assert.Equal(string.Empty, responseText);
+
+ server.Listener.Options.MaxConnections = 2;
+
+ // Maxed out, refuses connection and throws
+ await Assert.ThrowsAsync(() => SendRequestAsync(address));
+ }
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_SetConnectionLimitInfinite_Success()
+ {
+ // This is just to get a dynamic port
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext => Task.FromResult(0))) { }
+
+ var server = Utilities.CreatePump();
+ server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(address));
+ server.Listener.Options.MaxConnections = -1; // infinite
+
+ using (server)
+ {
+ await server.StartAsync(new DummyApplication(), CancellationToken.None);
+
+ using (var client1 = await SendHungRequestAsync("GET", address))
+ using (var client2 = await SendHungRequestAsync("GET", address))
+ using (var client3 = await SendHungRequestAsync("GET", address))
+ {
+ // Doesn't max out
+ string responseText = await SendRequestAsync(address);
+ Assert.Equal(string.Empty, responseText);
+ }
+ }
+ }
+
private async Task SendRequestAsync(string uri)
{
using (HttpClient client = new HttpClient())