Implement max connection limits

- Added new options to allow configuring the maximum number of concurrent connections and upgraded connections.
- `KestrelServerLimits.MaxConcurrentConnections` defaults unlimited.
- `KestrelServerLimits.MaxConcurrentUpgradedConnections` defaults to unlimited.
- Calls to IHttpUpgradeFeature.UpgradeAsync() will throw when the MaxConcurrentUpgradedConnections limit has been reached.
- Kestrel will close new connections without response when MaxConcurrentConnections is reached.
This commit is contained in:
Nate McMaster 2017-05-26 12:26:05 -07:00
parent 009759c7f6
commit c343628926
33 changed files with 888 additions and 122 deletions

8
.vscode/launch.json vendored
View File

@ -7,6 +7,12 @@
"request": "attach", "request": "attach",
"processId": "${command:pickProcess}" "processId": "${command:pickProcess}"
}, },
{
"name": "Attach: .NET Framework",
"type": "clr",
"request": "attach",
"processId": "${command:pickProcess}"
},
{ {
"name": "Debug: SampleApp", "name": "Debug: SampleApp",
"type": "coreclr", "type": "coreclr",
@ -62,7 +68,7 @@
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "Compile: CodeGenerator", "preLaunchTask": "Compile: CodeGenerator",
"program": "${workspaceRoot}/tools/CodeGenerator/bin/Debug/netcoreapp1.1/CodeGenerator.dll", "program": "${workspaceRoot}/tools/CodeGenerator/bin/Debug/netcoreapp2.0/CodeGenerator.dll",
"args": [], "args": [],
"cwd": "${workspaceRoot}", "cwd": "${workspaceRoot}",
"console": "internalConsole", "console": "internalConsole",

17
.vscode/tasks.json vendored
View File

@ -76,6 +76,23 @@
"cwd": "${workspaceRoot}/tools/CodeGenerator/" "cwd": "${workspaceRoot}/tools/CodeGenerator/"
} }
}, },
{
"taskName": "Run: resx generation",
"suppressTaskName": true,
"command": "build.cmd",
"args": [
"/t:resx"
],
"options": {
"cwd": "${workspaceRoot}"
},
"osx": {
"command": "./build.sh"
},
"linux": {
"command": "./build.sh"
}
},
{ {
"taskName": "Run: Benchmarks", "taskName": "Run: Benchmarks",
"args": [ "args": [

View File

@ -228,14 +228,17 @@
<data name="InvalidContentLength_InvalidNumber" xml:space="preserve"> <data name="InvalidContentLength_InvalidNumber" xml:space="preserve">
<value>Invalid Content-Length: "{value}". Value must be a positive integral number.</value> <value>Invalid Content-Length: "{value}". Value must be a positive integral number.</value>
</data> </data>
<data name="NonNegativeNullableIntRequired" xml:space="preserve"> <data name="NonNegativeNumberOrNullRequired" xml:space="preserve">
<value>Value must be null or a non-negative integer.</value> <value>Value must be null or a non-negative number.</value>
</data> </data>
<data name="PositiveIntRequired" xml:space="preserve"> <data name="NonNegativeNumberRequired" xml:space="preserve">
<value>Value must be a positive integer.</value> <value>Value must be a non-negative number.</value>
</data> </data>
<data name="PositiveNullableIntRequired" xml:space="preserve"> <data name="PositiveNumberRequired" xml:space="preserve">
<value>Value must be null or a positive integer.</value> <value>Value must be a positive number.</value>
</data>
<data name="PositiveNumberOrNullRequired" xml:space="preserve">
<value>Value must be null or a positive number.</value>
</data> </data>
<data name="UnixSocketPathMustBeAbsolute" xml:space="preserve"> <data name="UnixSocketPathMustBeAbsolute" xml:space="preserve">
<value>Unix socket path must be absolute.</value> <value>Unix socket path must be absolute.</value>
@ -309,4 +312,10 @@
<data name="CannotUpgradeNonUpgradableRequest" xml:space="preserve"> <data name="CannotUpgradeNonUpgradableRequest" xml:space="preserve">
<value>Cannot upgrade a non-upgradable request. Check IHttpUpgradeFeature.IsUpgradableRequest to determine if a request can be upgraded.</value> <value>Cannot upgrade a non-upgradable request. Check IHttpUpgradeFeature.IsUpgradableRequest to determine if a request can be upgraded.</value>
</data> </data>
<data name="UpgradedConnectionLimitReached" xml:space="preserve">
<value>Request cannot be upgraded because the server has already opened the maximum number of upgraded connections.</value>
</data>
<data name="UpgradeCannotBeCalledMultipleTimes" xml:space="preserve">
<value>IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection.</value>
</data>
</root> </root>

View File

@ -33,6 +33,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
var connectionId = CorrelationIdGenerator.GetNextId(); var connectionId = CorrelationIdGenerator.GetNextId();
var frameConnectionId = Interlocked.Increment(ref _lastFrameConnectionId); var frameConnectionId = Interlocked.Increment(ref _lastFrameConnectionId);
if (!_serviceContext.ConnectionManager.NormalConnectionCount.TryLockOne())
{
var goAway = new RejectionConnection(inputPipe, outputPipe, connectionId, _serviceContext);
goAway.Reject();
return goAway;
}
var connection = new FrameConnection(new FrameConnectionContext var connection = new FrameConnection(new FrameConnectionContext
{ {
ConnectionId = connectionId, ConnectionId = connectionId,

View File

@ -126,6 +126,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_context.ServiceContext.ConnectionManager.RemoveConnection(_context.FrameConnectionId); _context.ServiceContext.ConnectionManager.RemoveConnection(_context.FrameConnectionId);
DisposeAdaptedConnections(); DisposeAdaptedConnections();
if (_frame.WasUpgraded)
{
_context.ServiceContext.ConnectionManager.UpgradedConnectionCount.ReleaseOne();
}
else
{
_context.ServiceContext.ConnectionManager.NormalConnectionCount.ReleaseOne();
}
Log.ConnectionStop(ConnectionId); Log.ConnectionStop(ConnectionId);
KestrelEventSource.Log.ConnectionStop(this); KestrelEventSource.Log.ConnectionStop(this);
} }

View File

@ -160,7 +160,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
bool IHttpResponseFeature.HasStarted => HasResponseStarted; bool IHttpResponseFeature.HasStarted => HasResponseStarted;
bool IHttpUpgradeFeature.IsUpgradableRequest => _upgrade; bool IHttpUpgradeFeature.IsUpgradableRequest => _upgradeAvailable;
bool IFeatureCollection.IsReadOnly => false; bool IFeatureCollection.IsReadOnly => false;
@ -235,6 +235,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
throw new InvalidOperationException(CoreStrings.CannotUpgradeNonUpgradableRequest); throw new InvalidOperationException(CoreStrings.CannotUpgradeNonUpgradableRequest);
} }
if (_wasUpgraded)
{
throw new InvalidOperationException(CoreStrings.UpgradeCannotBeCalledMultipleTimes);
}
if (!ServiceContext.ConnectionManager.UpgradedConnectionCount.TryLockOne())
{
throw new InvalidOperationException(CoreStrings.UpgradedConnectionLimitReached);
}
_wasUpgraded = true;
ServiceContext.ConnectionManager.NormalConnectionCount.ReleaseOne();
StatusCode = StatusCodes.Status101SwitchingProtocols; StatusCode = StatusCodes.Status101SwitchingProtocols;
ReasonPhrase = "Switching Protocols"; ReasonPhrase = "Switching Protocols";
ResponseHeaders["Connection"] = "Upgrade"; ResponseHeaders["Connection"] = "Upgrade";

View File

@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly byte[] _bytesTransferEncodingChunked = Encoding.ASCII.GetBytes("\r\nTransfer-Encoding: chunked"); private static readonly byte[] _bytesTransferEncodingChunked = Encoding.ASCII.GetBytes("\r\nTransfer-Encoding: chunked");
private static readonly byte[] _bytesHttpVersion11 = Encoding.ASCII.GetBytes("HTTP/1.1 "); private static readonly byte[] _bytesHttpVersion11 = Encoding.ASCII.GetBytes("HTTP/1.1 ");
private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n"); private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n");
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: Kestrel"); private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: " + Constants.ServerName);
private const string EmptyPath = "/"; private const string EmptyPath = "/";
private const string Asterisk = "*"; private const string Asterisk = "*";
@ -61,7 +61,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected RequestProcessingStatus _requestProcessingStatus; protected RequestProcessingStatus _requestProcessingStatus;
protected bool _keepAlive; protected bool _keepAlive;
protected bool _upgrade; protected bool _upgradeAvailable;
private volatile bool _wasUpgraded;
private bool _canHaveBody; private bool _canHaveBody;
private bool _autoChunk; private bool _autoChunk;
protected Exception _applicationException; protected Exception _applicationException;
@ -138,6 +139,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
} }
} }
public bool WasUpgraded => _wasUpgraded;
public IPAddress RemoteIpAddress { get; set; } public IPAddress RemoteIpAddress { get; set; }
public int RemotePort { get; set; } public int RemotePort { get; set; }
public IPAddress LocalIpAddress { get; set; } public IPAddress LocalIpAddress { get; set; }
@ -208,10 +210,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private int _statusCode; private int _statusCode;
public int StatusCode public int StatusCode
{ {
get get => _statusCode;
{
return _statusCode;
}
set set
{ {
if (HasResponseStarted) if (HasResponseStarted)
@ -227,10 +226,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public string ReasonPhrase public string ReasonPhrase
{ {
get get => _reasonPhrase;
{
return _reasonPhrase;
}
set set
{ {
if (HasResponseStarted) if (HasResponseStarted)
@ -1038,6 +1035,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues(); var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues();
responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes); responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
responseHeaders.ContentLength = 0; responseHeaders.ContentLength = 0;
if (ServerOptions.AddServerHeader) if (ServerOptions.AddServerHeader)

View File

@ -89,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
var messageBody = MessageBody.For(_httpVersion, FrameRequestHeaders, this); var messageBody = MessageBody.For(_httpVersion, FrameRequestHeaders, this);
_keepAlive = messageBody.RequestKeepAlive; _keepAlive = messageBody.RequestKeepAlive;
_upgrade = messageBody.RequestUpgrade; _upgradeAvailable = messageBody.RequestUpgrade;
InitializeStreams(messageBody); InitializeStreams(messageBody);
@ -165,7 +165,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// An upgraded request has no defined request body length. // An upgraded request has no defined request body length.
// Cancel any pending read so the read loop ends. // Cancel any pending read so the read loop ends.
if (_upgrade) if (_upgradeAvailable)
{ {
Input.CancelPendingRead(); Input.CancelPendingRead();
} }

View File

@ -130,7 +130,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
} }
if (bufferSize <= 0) if (bufferSize <= 0)
{ {
throw new ArgumentException(CoreStrings.PositiveIntRequired, nameof(bufferSize)); throw new ArgumentException(CoreStrings.PositiveNumberRequired, nameof(bufferSize));
} }
var task = ValidateState(cancellationToken); var task = ValidateState(cancellationToken);

View File

@ -11,11 +11,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
private readonly ConcurrentDictionary<long, FrameConnectionReference> _connectionReferences = new ConcurrentDictionary<long, FrameConnectionReference>(); private readonly ConcurrentDictionary<long, FrameConnectionReference> _connectionReferences = new ConcurrentDictionary<long, FrameConnectionReference>();
private readonly IKestrelTrace _trace; private readonly IKestrelTrace _trace;
public FrameConnectionManager(IKestrelTrace trace) public FrameConnectionManager(IKestrelTrace trace, long? normalConnectionLimit, long? upgradedConnectionLimit)
: this(trace, GetCounter(normalConnectionLimit), GetCounter(upgradedConnectionLimit))
{ {
}
public FrameConnectionManager(IKestrelTrace trace, ResourceCounter normalConnections, ResourceCounter upgradedConnections)
{
NormalConnectionCount = normalConnections;
UpgradedConnectionCount = upgradedConnections;
_trace = trace; _trace = trace;
} }
/// <summary>
/// TCP connections processed by Kestrel.
/// </summary>
public ResourceCounter NormalConnectionCount { get; }
/// <summary>
/// Connections that have been switched to a different protocol.
/// </summary>
public ResourceCounter UpgradedConnectionCount { get; }
public void AddConnection(long id, FrameConnection connection) public void AddConnection(long id, FrameConnection connection)
{ {
if (!_connectionReferences.TryAdd(id, new FrameConnectionReference(connection))) if (!_connectionReferences.TryAdd(id, new FrameConnectionReference(connection)))
@ -52,5 +69,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
// If both conditions are false, the connection was removed during the heartbeat. // If both conditions are false, the connection was removed during the heartbeat.
} }
} }
private static ResourceCounter GetCounter(long? number)
=> number.HasValue
? ResourceCounter.Quota(number.Value)
: ResourceCounter.Unlimited;
} }
} }

View File

@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
void ConnectionResume(string connectionId); void ConnectionResume(string connectionId);
void ConnectionRejected(string connectionId);
void ConnectionKeepAlive(string connectionId); void ConnectionKeepAlive(string connectionId);
void ConnectionDisconnect(string connectionId); void ConnectionDisconnect(string connectionId);

View File

@ -68,6 +68,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
WriteEvent(2, connectionId); WriteEvent(2, connectionId);
} }
[MethodImpl(MethodImplOptions.NoInlining)]
[Event(5, Level = EventLevel.Verbose)]
public void ConnectionRejected(string connectionId)
{
if (IsEnabled())
{
WriteEvent(5, connectionId);
}
}
[NonEvent] [NonEvent]
public void RequestStart(Frame frame) public void RequestStart(Frame frame)
{ {

View File

@ -54,6 +54,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private static readonly Action<ILogger, string, Exception> _applicationNeverCompleted = private static readonly Action<ILogger, string, Exception> _applicationNeverCompleted =
LoggerMessage.Define<string>(LogLevel.Critical, 23, @"Connection id ""{ConnectionId}"" application never completed"); LoggerMessage.Define<string>(LogLevel.Critical, 23, @"Connection id ""{ConnectionId}"" application never completed");
private static readonly Action<ILogger, string, Exception> _connectionRejected =
LoggerMessage.Define<string>(LogLevel.Warning, 24, @"Connection id ""{ConnectionId}"" rejected because the maximum number of concurrent connections has been reached.");
protected readonly ILogger _logger; protected readonly ILogger _logger;
public KestrelTrace(ILogger logger) public KestrelTrace(ILogger logger)
@ -86,6 +89,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_connectionKeepAlive(_logger, connectionId, null); _connectionKeepAlive(_logger, connectionId, null);
} }
public void ConnectionRejected(string connectionId)
{
_connectionRejected(_logger, connectionId, null);
}
public virtual void ConnectionDisconnect(string connectionId) public virtual void ConnectionDisconnect(string connectionId)
{ {
_connectionDisconnect(_logger, connectionId, null); _connectionDisconnect(_logger, connectionId, null);

View File

@ -0,0 +1,77 @@
// 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.Diagnostics;
using System.Threading;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
public abstract class ResourceCounter
{
public abstract bool TryLockOne();
public abstract void ReleaseOne();
public static ResourceCounter Unlimited { get; } = new UnlimitedCounter();
public static ResourceCounter Quota(long amount) => new FiniteCounter(amount);
private class UnlimitedCounter : ResourceCounter
{
public override bool TryLockOne() => true;
public override void ReleaseOne()
{
}
}
internal class FiniteCounter : ResourceCounter
{
private readonly long _max;
private long _count;
public FiniteCounter(long max)
{
if (max < 0)
{
throw new ArgumentOutOfRangeException(CoreStrings.NonNegativeNumberRequired);
}
_max = max;
}
public override bool TryLockOne()
{
var count = _count;
// Exit if count == MaxValue as incrementing would overflow.
while (count < _max && count != long.MaxValue)
{
var prev = Interlocked.CompareExchange(ref _count, count + 1, count);
if (prev == count)
{
return true;
}
// Another thread changed the count before us. Try again with the new counter value.
count = prev;
}
return false;
}
public override void ReleaseOne()
{
Interlocked.Decrement(ref _count);
Debug.Assert(_count >= 0, "Resource count is negative. More resources were released than were locked.");
}
// for testing
internal long Count
{
get => _count;
set => _count = value;
}
}
}
}

View File

@ -0,0 +1,46 @@
// 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 Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
public class RejectionConnection : IConnectionContext
{
private readonly IKestrelTrace _log;
private readonly IPipe _input;
private readonly IPipe _output;
public RejectionConnection(IPipe input, IPipe output, string connectionId, ServiceContext serviceContext)
{
ConnectionId = connectionId;
_log = serviceContext.Log;
_input = input;
_output = output;
}
public string ConnectionId { get; }
public IPipeWriter Input => _input.Writer;
public IPipeReader Output => _output.Reader;
public void Reject()
{
KestrelEventSource.Log.ConnectionRejected(ConnectionId);
_log.ConnectionRejected(ConnectionId);
_input.Reader.Complete();
_output.Writer.Complete();
}
// TODO: Remove these (https://github.com/aspnet/KestrelHttpServer/issues/1772)
void IConnectionContext.OnConnectionClosed(Exception ex)
{
}
void IConnectionContext.Abort(Exception ex)
{
}
}
}

View File

@ -67,7 +67,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
var serverOptions = options.Value ?? new KestrelServerOptions(); var serverOptions = options.Value ?? new KestrelServerOptions();
var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel"); var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel");
var trace = new KestrelTrace(logger); var trace = new KestrelTrace(logger);
var connectionManager = new FrameConnectionManager(trace); var connectionManager = new FrameConnectionManager(
trace,
serverOptions.Limits.MaxConcurrentConnections,
serverOptions.Limits.MaxConcurrentUpgradedConnections);
var systemClock = new SystemClock(); var systemClock = new SystemClock();
var dateHeaderValueManager = new DateHeaderValueManager(systemClock); var dateHeaderValueManager = new DateHeaderValueManager(systemClock);

View File

@ -28,6 +28,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
private TimeSpan _requestHeadersTimeout = TimeSpan.FromSeconds(30); private TimeSpan _requestHeadersTimeout = TimeSpan.FromSeconds(30);
// default to unlimited
private long? _maxConcurrentConnections = null;
private long? _maxConcurrentUpgradedConnections = null;
/// <summary> /// <summary>
/// Gets or sets the maximum size of the response buffer before write /// Gets or sets the maximum size of the response buffer before write
/// calls begin to block or return tasks that don't complete until the /// calls begin to block or return tasks that don't complete until the
@ -41,15 +45,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks> /// </remarks>
public long? MaxResponseBufferSize public long? MaxResponseBufferSize
{ {
get get => _maxResponseBufferSize;
{
return _maxResponseBufferSize;
}
set set
{ {
if (value.HasValue && value.Value < 0) if (value.HasValue && value.Value < 0)
{ {
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.NonNegativeNullableIntRequired); throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.NonNegativeNumberOrNullRequired);
} }
_maxResponseBufferSize = value; _maxResponseBufferSize = value;
} }
@ -64,15 +65,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks> /// </remarks>
public long? MaxRequestBufferSize public long? MaxRequestBufferSize
{ {
get get => _maxRequestBufferSize;
{
return _maxRequestBufferSize;
}
set set
{ {
if (value.HasValue && value.Value <= 0) if (value.HasValue && value.Value <= 0)
{ {
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveNullableIntRequired); throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveNumberOrNullRequired);
} }
_maxRequestBufferSize = value; _maxRequestBufferSize = value;
} }
@ -86,15 +84,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks> /// </remarks>
public int MaxRequestLineSize public int MaxRequestLineSize
{ {
get get => _maxRequestLineSize;
{
return _maxRequestLineSize;
}
set set
{ {
if (value <= 0) if (value <= 0)
{ {
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveIntRequired); throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveNumberRequired);
} }
_maxRequestLineSize = value; _maxRequestLineSize = value;
} }
@ -108,15 +103,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks> /// </remarks>
public int MaxRequestHeadersTotalSize public int MaxRequestHeadersTotalSize
{ {
get get => _maxRequestHeadersTotalSize;
{
return _maxRequestHeadersTotalSize;
}
set set
{ {
if (value <= 0) if (value <= 0)
{ {
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveIntRequired); throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveNumberRequired);
} }
_maxRequestHeadersTotalSize = value; _maxRequestHeadersTotalSize = value;
} }
@ -130,15 +122,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks> /// </remarks>
public int MaxRequestHeaderCount public int MaxRequestHeaderCount
{ {
get get => _maxRequestHeaderCount;
{
return _maxRequestHeaderCount;
}
set set
{ {
if (value <= 0) if (value <= 0)
{ {
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveIntRequired); throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveNumberRequired);
} }
_maxRequestHeaderCount = value; _maxRequestHeaderCount = value;
} }
@ -152,14 +141,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks> /// </remarks>
public TimeSpan KeepAliveTimeout public TimeSpan KeepAliveTimeout
{ {
get get => _keepAliveTimeout;
{ set => _keepAliveTimeout = value;
return _keepAliveTimeout;
}
set
{
_keepAliveTimeout = value;
}
} }
/// <summary> /// <summary>
@ -170,13 +153,58 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </remarks> /// </remarks>
public TimeSpan RequestHeadersTimeout public TimeSpan RequestHeadersTimeout
{ {
get get => _requestHeadersTimeout;
{ set => _requestHeadersTimeout = value;
return _requestHeadersTimeout; }
}
/// <summary>
/// Gets or sets the maximum number of open HTTP/S connections. When set to null, the number of connections is unlimited.
/// </summary>
/// <remarks>
/// <para>
/// Defaults to null.
/// </para>
/// <para>
/// When a connection is upgraded to another protocol, such as WebSockets, its connection is counted against the
/// <see cref="MaxConcurrentUpgradedConnections" /> limit instead of <see cref="MaxConcurrentConnections" />.
/// </para>
/// </remarks>
public long? MaxConcurrentConnections
{
get => _maxConcurrentConnections;
set set
{ {
_requestHeadersTimeout = value; if (value.HasValue && value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveNumberOrNullRequired);
}
_maxConcurrentConnections = value;
}
}
/// <summary>
/// Gets or sets the maximum number of open, upgraded connections. When set to null, the number of upgraded connections is unlimited.
/// An upgraded connection is one that has been switched from HTTP to another protocol, such as WebSockets.
/// </summary>
/// <remarks>
/// <para>
/// Defaults to null.
/// </para>
/// <para>
/// When a connection is upgraded to another protocol, such as WebSockets, its connection is counted against the
/// <see cref="MaxConcurrentUpgradedConnections" /> limit instead of <see cref="MaxConcurrentConnections" />.
/// </para>
/// </remarks>
public long? MaxConcurrentUpgradedConnections
{
get => _maxConcurrentUpgradedConnections;
set
{
if (value.HasValue && value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.NonNegativeNumberOrNullRequired);
}
_maxConcurrentUpgradedConnections = value;
} }
} }
} }

View File

@ -529,46 +529,60 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidContentLength_InvalidNumber", "value"), value); => string.Format(CultureInfo.CurrentCulture, GetString("InvalidContentLength_InvalidNumber", "value"), value);
/// <summary> /// <summary>
/// Value must be null or a non-negative integer. /// Value must be null or a non-negative number.
/// </summary> /// </summary>
internal static string NonNegativeNullableIntRequired internal static string NonNegativeNumberOrNullRequired
{ {
get => GetString("NonNegativeNullableIntRequired"); get => GetString("NonNegativeNumberOrNullRequired");
} }
/// <summary> /// <summary>
/// Value must be null or a non-negative integer. /// Value must be null or a non-negative number.
/// </summary> /// </summary>
internal static string FormatNonNegativeNullableIntRequired() internal static string FormatNonNegativeNumberOrNullRequired()
=> GetString("NonNegativeNullableIntRequired"); => GetString("NonNegativeNumberOrNullRequired");
/// <summary> /// <summary>
/// Value must be a positive integer. /// Value must be a non-negative number.
/// </summary> /// </summary>
internal static string PositiveIntRequired internal static string NonNegativeNumberRequired
{ {
get => GetString("PositiveIntRequired"); get => GetString("NonNegativeNumberRequired");
} }
/// <summary> /// <summary>
/// Value must be a positive integer. /// Value must be a non-negative number.
/// </summary> /// </summary>
internal static string FormatPositiveIntRequired() internal static string FormatNonNegativeNumberRequired()
=> GetString("PositiveIntRequired"); => GetString("NonNegativeNumberRequired");
/// <summary> /// <summary>
/// Value must be null or a positive integer. /// Value must be a positive number.
/// </summary> /// </summary>
internal static string PositiveNullableIntRequired internal static string PositiveNumberRequired
{ {
get => GetString("PositiveNullableIntRequired"); get => GetString("PositiveNumberRequired");
} }
/// <summary> /// <summary>
/// Value must be null or a positive integer. /// Value must be a positive number.
/// </summary> /// </summary>
internal static string FormatPositiveNullableIntRequired() internal static string FormatPositiveNumberRequired()
=> GetString("PositiveNullableIntRequired"); => GetString("PositiveNumberRequired");
/// <summary>
/// Value must be null or a positive number.
/// </summary>
internal static string PositiveNumberOrNullRequired
{
get => GetString("PositiveNumberOrNullRequired");
}
/// <summary>
/// Value must be null or a positive number.
/// </summary>
internal static string FormatPositiveNumberOrNullRequired()
=> GetString("PositiveNumberOrNullRequired");
/// <summary> /// <summary>
/// Unix socket path must be absolute. /// Unix socket path must be absolute.
@ -906,6 +920,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatCannotUpgradeNonUpgradableRequest() internal static string FormatCannotUpgradeNonUpgradableRequest()
=> GetString("CannotUpgradeNonUpgradableRequest"); => GetString("CannotUpgradeNonUpgradableRequest");
/// <summary>
/// Request cannot be upgraded because the server has already opened the maximum number of upgraded connections.
/// </summary>
internal static string UpgradedConnectionLimitReached
{
get => GetString("UpgradedConnectionLimitReached");
}
/// <summary>
/// Request cannot be upgraded because the server has already opened the maximum number of upgraded connections.
/// </summary>
internal static string FormatUpgradedConnectionLimitReached()
=> GetString("UpgradedConnectionLimitReached");
/// <summary>
/// IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection.
/// </summary>
internal static string UpgradeCannotBeCalledMultipleTimes
{
get => GetString("UpgradeCannotBeCalledMultipleTimes");
}
/// <summary>
/// IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection.
/// </summary>
internal static string FormatUpgradeCannotBeCalledMultipleTimes()
=> GetString("UpgradeCannotBeCalledMultipleTimes");
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
var connectionId = "0"; var connectionId = "0";
var trace = new Mock<IKestrelTrace>(); var trace = new Mock<IKestrelTrace>();
var frameConnectionManager = new FrameConnectionManager(trace.Object); var frameConnectionManager = new FrameConnectionManager(trace.Object, ResourceCounter.Unlimited, ResourceCounter.Unlimited);
// Create FrameConnection in inner scope so it doesn't get rooted by the current frame. // Create FrameConnection in inner scope so it doesn't get rooted by the current frame.
UnrootedConnectionsGetRemovedFromHeartbeatInnerScope(connectionId, frameConnectionManager, trace); UnrootedConnectionsGetRemovedFromHeartbeatInnerScope(connectionId, frameConnectionManager, trace);

View File

@ -105,10 +105,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[InlineData(0)] [InlineData(0)]
public void MaxRequestHeadersTotalSizeInvalid(int value) public void MaxRequestHeadersTotalSizeInvalid(int value)
{ {
Assert.Throws<ArgumentOutOfRangeException>(() => var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new KestrelServerLimits().MaxRequestHeadersTotalSize = value);
{ Assert.StartsWith(CoreStrings.PositiveNumberRequired, ex.Message);
(new KestrelServerLimits()).MaxRequestHeadersTotalSize = value;
});
} }
[Theory] [Theory]
@ -187,5 +185,61 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
o.RequestHeadersTimeout = TimeSpan.FromSeconds(seconds); o.RequestHeadersTimeout = TimeSpan.FromSeconds(seconds);
Assert.Equal(seconds, o.RequestHeadersTimeout.TotalSeconds); Assert.Equal(seconds, o.RequestHeadersTimeout.TotalSeconds);
} }
[Fact]
public void MaxConnectionsDefault()
{
Assert.Null(new KestrelServerLimits().MaxConcurrentConnections);
Assert.Null(new KestrelServerLimits().MaxConcurrentUpgradedConnections);
}
[Theory]
[InlineData(null)]
[InlineData(1u)]
[InlineData(long.MaxValue)]
public void MaxConnectionsValid(long? value)
{
var limits = new KestrelServerLimits
{
MaxConcurrentConnections = value
};
Assert.Equal(value, limits.MaxConcurrentConnections);
}
[Theory]
[InlineData(long.MinValue)]
[InlineData(-1)]
[InlineData(0)]
public void MaxConnectionsInvalid(long value)
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new KestrelServerLimits().MaxConcurrentConnections = value);
Assert.StartsWith(CoreStrings.PositiveNumberOrNullRequired, ex.Message);
}
[Theory]
[InlineData(null)]
[InlineData(0)]
[InlineData(1)]
[InlineData(long.MaxValue)]
public void MaxUpgradedConnectionsValid(long? value)
{
var limits = new KestrelServerLimits
{
MaxConcurrentUpgradedConnections = value
};
Assert.Equal(value, limits.MaxConcurrentUpgradedConnections);
}
[Theory]
[InlineData(long.MinValue)]
[InlineData(-1)]
public void MaxUpgradedConnectionsInvalid(long value)
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new KestrelServerLimits().MaxConcurrentUpgradedConnections = value);
Assert.StartsWith(CoreStrings.NonNegativeNumberOrNullRequired, ex.Message);
}
} }
} }

View File

@ -0,0 +1,56 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class ResourceCounterTests
{
[Theory]
[InlineData(-1)]
[InlineData(long.MinValue)]
public void QuotaInvalid(long max)
{
Assert.Throws<ArgumentOutOfRangeException>(() => ResourceCounter.Quota(max));
}
[Fact]
public void QuotaAcceptsUpToButNotMoreThanMax()
{
var counter = ResourceCounter.Quota(1);
Assert.True(counter.TryLockOne());
Assert.False(counter.TryLockOne());
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(10)]
[InlineData(100)]
public void QuotaValid(long max)
{
var counter = ResourceCounter.Quota(max);
Parallel.For(0, max, i =>
{
Assert.True(counter.TryLockOne());
});
Parallel.For(0, 10, i =>
{
Assert.False(counter.TryLockOne());
});
}
[Fact]
public void QuotaDoesNotWrapAround()
{
var counter = new ResourceCounter.FiniteCounter(long.MaxValue);
counter.Count = long.MaxValue;
Assert.False(counter.TryLockOne());
}
}
}

View File

@ -0,0 +1,198 @@
// 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.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Tests;
using Microsoft.AspNetCore.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
public class ConnectionLimitTests
{
[Fact]
public async Task ResetsCountWhenConnectionClosed()
{
var requestTcs = new TaskCompletionSource<object>();
var releasedTcs = new TaskCompletionSource<object>();
var lockedTcs = new TaskCompletionSource<bool>();
var (serviceContext, counter) = SetupMaxConnections(max: 1);
counter.OnLock += (s, e) => lockedTcs.TrySetResult(e);
counter.OnRelease += (s, e) => releasedTcs.TrySetResult(null);
using (var server = new TestServer(async context =>
{
await context.Response.WriteAsync("Hello");
await requestTcs.Task;
}, serviceContext))
using (var connection = server.CreateConnection())
{
await connection.SendEmptyGetAsKeepAlive(); ;
await connection.Receive("HTTP/1.1 200 OK");
Assert.True(await lockedTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10)));
requestTcs.TrySetResult(null);
}
await releasedTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
}
[Fact]
public async Task UpgradedConnectionsCountsAgainstDifferentLimit()
{
var (serviceContext, _) = SetupMaxConnections(max: 1);
using (var server = new TestServer(async context =>
{
var feature = context.Features.Get<IHttpUpgradeFeature>();
if (feature.IsUpgradableRequest)
{
var stream = await feature.UpgradeAsync();
// keep it running until aborted
while (!context.RequestAborted.IsCancellationRequested)
{
await Task.Delay(100);
}
}
}, serviceContext))
using (var disposables = new DisposableStack<TestConnection>())
{
var upgraded = server.CreateConnection();
disposables.Push(upgraded);
await upgraded.SendEmptyGetWithUpgrade();
await upgraded.Receive("HTTP/1.1 101");
// once upgraded, normal connection limit is decreased to allow room for more "normal" connections
var connection = server.CreateConnection();
disposables.Push(connection);
await connection.SendEmptyGetAsKeepAlive();
await connection.Receive("HTTP/1.1 200 OK");
using (var rejected = server.CreateConnection())
{
try
{
// this may throw IOException, depending on how fast Kestrel closes the socket
await rejected.SendEmptyGetAsKeepAlive();
} catch { }
// connection should close without sending any data
await rejected.WaitForConnectionClose().TimeoutAfter(TimeSpan.FromSeconds(15));
}
}
}
[Fact]
public async Task RejectsConnectionsWhenLimitReached()
{
const int max = 10;
var (serviceContext, _) = SetupMaxConnections(max);
var requestTcs = new TaskCompletionSource<object>();
using (var server = new TestServer(async context =>
{
await context.Response.WriteAsync("Hello");
await requestTcs.Task;
}, serviceContext))
using (var disposables = new DisposableStack<TestConnection>())
{
for (var i = 0; i < max; i++)
{
var connection = server.CreateConnection();
disposables.Push(connection);
await connection.SendEmptyGetAsKeepAlive();
await connection.Receive("HTTP/1.1 200 OK");
}
// limit has been reached
for (var i = 0; i < 10; i++)
{
using (var connection = server.CreateConnection())
{
try
{
// this may throw IOException, depending on how fast Kestrel closes the socket
await connection.SendEmptyGetAsKeepAlive();
} catch { }
// connection should close without sending any data
await connection.WaitForConnectionClose().TimeoutAfter(TimeSpan.FromSeconds(15));
}
}
requestTcs.TrySetResult(null);
}
}
[Fact]
public async Task ConnectionCountingReturnsToZero()
{
const int count = 500;
var opened = 0;
var closed = 0;
var openedTcs = new TaskCompletionSource<object>();
var closedTcs = new TaskCompletionSource<object>();
var (serviceContext, counter) = SetupMaxConnections(uint.MaxValue);
counter.OnLock += (o, e) =>
{
if (e && Interlocked.Increment(ref opened) >= count)
{
openedTcs.TrySetResult(null);
}
};
counter.OnRelease += (o, e) =>
{
if (Interlocked.Increment(ref closed) >= count)
{
closedTcs.TrySetResult(null);
}
};
using (var server = new TestServer(_ => Task.CompletedTask, serviceContext))
{
// open a bunch of connections in parallel
Parallel.For(0, count, async i =>
{
try
{
using (var connection = server.CreateConnection())
{
await connection.SendEmptyGetAsKeepAlive();
await connection.Receive("HTTP/1.1 200");
}
}
catch (Exception ex)
{
closedTcs.TrySetException(ex);
}
});
// wait until resource counter has called lock for each connection
await openedTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(60));
// wait until resource counter has released all normal connections
await closedTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(60));
Assert.Equal(count, opened);
Assert.Equal(count, closed);
}
}
private (TestServiceContext serviceContext, EventRaisingResourceCounter counter) SetupMaxConnections(long max)
{
var counter = new EventRaisingResourceCounter(ResourceCounter.Quota(max));
var serviceContext = new TestServiceContext();
serviceContext.ConnectionManager = new FrameConnectionManager(serviceContext.Log, counter, ResourceCounter.Unlimited);
return (serviceContext, counter);
}
}
}

View File

@ -46,10 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
await connection.Send("GET / HTTP/1.1", await connection.SendEmptyGet();
"Host:",
"",
"");
Assert.True(await appStartedWh.WaitAsync(TimeSpan.FromSeconds(10))); Assert.True(await appStartedWh.WaitAsync(TimeSpan.FromSeconds(10)));

View File

@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
Tuple.Create((long?)_dataLength - 1, false), Tuple.Create((long?)_dataLength - 1, false),
// Buffer is exactly the same size as data. Exposed race condition where // Buffer is exactly the same size as data. Exposed race condition where
// IConnectionControl.Resume() was called after socket was disconnected. // the connection was resumed after socket was disconnected.
Tuple.Create((long?)_dataLength, false), Tuple.Create((long?)_dataLength, false),
// Largest possible buffer, should never trigger backpressure. // Largest possible buffer, should never trigger backpressure.

View File

@ -662,10 +662,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
var buffer = new char[identifierLength]; var buffer = new char[identifierLength];
for (var i = 0; i < iterations; i++) for (var i = 0; i < iterations; i++)
{ {
await connection.Send("GET / HTTP/1.1", await connection.SendEmptyGet();
"Host:",
"",
"");
await connection.Receive($"HTTP/1.1 200 OK", await connection.Receive($"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}", $"Date: {server.Context.DateHeaderValue}",

View File

@ -6,14 +6,24 @@ using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Tests;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{ {
public class UpgradeTests public class UpgradeTests
{ {
private readonly ITestOutputHelper _output;
public UpgradeTests(ITestOutputHelper output)
{
_output = output;
}
[Fact] [Fact]
public async Task ResponseThrowsAfterUpgrade() public async Task ResponseThrowsAfterUpgrade()
{ {
@ -36,11 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
await connection.Send("GET / HTTP/1.1", await connection.SendEmptyGetWithUpgrade();
"Host:",
"Connection: Upgrade",
"",
"");
await connection.Receive("HTTP/1.1 101 Switching Protocols", await connection.Receive("HTTP/1.1 101 Switching Protocols",
"Connection: Upgrade", "Connection: Upgrade",
$"Date: {server.Context.DateHeaderValue}", $"Date: {server.Context.DateHeaderValue}",
@ -90,11 +96,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
await connection.Send("GET / HTTP/1.1", await connection.SendEmptyGetWithUpgrade();
"Host:",
"Connection: Upgrade",
"",
"");
await connection.Receive("HTTP/1.1 101 Switching Protocols", await connection.Receive("HTTP/1.1 101 Switching Protocols",
"Connection: Upgrade", "Connection: Upgrade",
@ -110,6 +112,45 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
} }
} }
[Fact]
public async Task UpgradeCannotBeCalledMultipleTimes()
{
var upgradeTcs = new TaskCompletionSource<object>();
using (var server = new TestServer(async context =>
{
var feature = context.Features.Get<IHttpUpgradeFeature>();
await feature.UpgradeAsync();
try
{
await feature.UpgradeAsync();
}
catch (Exception e)
{
upgradeTcs.TrySetException(e);
throw;
}
while (!context.RequestAborted.IsCancellationRequested)
{
await Task.Delay(100);
}
}))
using (var connection = server.CreateConnection())
{
await connection.SendEmptyGetWithUpgrade();
await connection.Receive("HTTP/1.1 101 Switching Protocols",
"Connection: Upgrade",
$"Date: {server.Context.DateHeaderValue}",
"",
"");
await connection.WaitForConnectionClose().TimeoutAfter(TimeSpan.FromSeconds(15));
}
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await upgradeTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(15)));
Assert.Equal(CoreStrings.UpgradeCannotBeCalledMultipleTimes, ex.Message);
}
[Fact] [Fact]
public async Task RejectsRequestWithContentLengthAndUpgrade() public async Task RejectsRequestWithContentLengthAndUpgrade()
{ {
@ -151,11 +192,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
await connection.Send("GET / HTTP/1.1", await connection.SendEmptyGetWithUpgrade();
"Host:",
"Connection: Upgrade",
"",
"");
await connection.Receive("HTTP/1.1 200 OK"); await connection.Receive("HTTP/1.1 200 OK");
} }
} }
@ -207,10 +244,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
await connection.Send("GET / HTTP/1.1", await connection.SendEmptyGet();
"Host:",
"",
"");
await connection.Receive("HTTP/1.1 200 OK"); await connection.Receive("HTTP/1.1 200 OK");
} }
} }
@ -218,5 +252,56 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await upgradeTcs.Task).TimeoutAfter(TimeSpan.FromSeconds(15)); var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await upgradeTcs.Task).TimeoutAfter(TimeSpan.FromSeconds(15));
Assert.Equal(CoreStrings.CannotUpgradeNonUpgradableRequest, ex.Message); Assert.Equal(CoreStrings.CannotUpgradeNonUpgradableRequest, ex.Message);
} }
[Fact]
public async Task RejectsUpgradeWhenLimitReached()
{
const int limit = 10;
var upgradeTcs = new TaskCompletionSource<object>();
var serviceContext = new TestServiceContext();
serviceContext.ConnectionManager = new FrameConnectionManager(serviceContext.Log, ResourceCounter.Unlimited, ResourceCounter.Quota(limit));
using (var server = new TestServer(async context =>
{
var feature = context.Features.Get<IHttpUpgradeFeature>();
if (feature.IsUpgradableRequest)
{
try
{
var stream = await feature.UpgradeAsync();
while (!context.RequestAborted.IsCancellationRequested)
{
await Task.Delay(100);
}
}
catch (InvalidOperationException ex)
{
upgradeTcs.TrySetException(ex);
}
}
}, serviceContext))
{
using (var disposables = new DisposableStack<TestConnection>())
{
for (var i = 0; i < limit; i++)
{
var connection = server.CreateConnection();
disposables.Push(connection);
await connection.SendEmptyGetWithUpgradeAndKeepAlive();
await connection.Receive("HTTP/1.1 101");
}
using (var connection = server.CreateConnection())
{
await connection.SendEmptyGetWithUpgradeAndKeepAlive();
await connection.Receive("HTTP/1.1 200");
}
}
}
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await upgradeTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(60)));
Assert.Equal(CoreStrings.UpgradedConnectionLimitReached, exception.Message);
}
} }
} }

View File

@ -1,6 +1,6 @@
using System; // Copyright (c) .NET Foundation. All rights reserved.
using System.Collections.Generic; // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks namespace Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
public void ConnectionReadFin(string connectionId) { } public void ConnectionReadFin(string connectionId) { }
public void ConnectionReset(string connectionId) { } public void ConnectionReset(string connectionId) { }
public void ConnectionResume(string connectionId) { } public void ConnectionResume(string connectionId) { }
public void ConnectionRejected(string connectionId) { }
public void ConnectionStart(string connectionId) { } public void ConnectionStart(string connectionId) { }
public void ConnectionStop(string connectionId) { } public void ConnectionStop(string connectionId) { }
public void ConnectionWrite(string connectionId, int count) { } public void ConnectionWrite(string connectionId, int count) { }

View File

@ -0,0 +1,20 @@
// 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.Generic;
namespace Microsoft.AspNetCore.Server.Kestrel.Tests
{
public class DisposableStack<T> : Stack<T>, IDisposable
where T : IDisposable
{
public void Dispose()
{
while (Count > 0)
{
Pop()?.Dispose();
}
}
}
}

View File

@ -0,0 +1,34 @@
// 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 Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Tests
{
public class EventRaisingResourceCounter : ResourceCounter
{
private readonly ResourceCounter _wrapped;
public EventRaisingResourceCounter(ResourceCounter wrapped)
{
_wrapped = wrapped;
}
public event EventHandler OnRelease;
public event EventHandler<bool> OnLock;
public override void ReleaseOne()
{
_wrapped.ReleaseOne();
OnRelease?.Invoke(this, EventArgs.Empty);
}
public override bool TryLockOne()
{
var retVal = _wrapped.TryLockOne();
OnLock?.Invoke(this, retVal);
return retVal;
}
}
}

View File

@ -61,6 +61,32 @@ namespace Microsoft.AspNetCore.Testing
} }
} }
public Task SendEmptyGet()
{
return Send("GET / HTTP/1.1",
"Host:",
"",
"");
}
public Task SendEmptyGetWithUpgradeAndKeepAlive()
=> SendEmptyGetWithConnection("Upgrade, keep-alive");
public Task SendEmptyGetWithUpgrade()
=> SendEmptyGetWithConnection("Upgrade");
public Task SendEmptyGetAsKeepAlive()
=> SendEmptyGetWithConnection("keep-alive");
private Task SendEmptyGetWithConnection(string connection)
{
return Send("GET / HTTP/1.1",
"Host:",
"Connection: " + connection,
"",
"");
}
public async Task SendAll(params string[] lines) public async Task SendAll(params string[] lines)
{ {
var text = string.Join("\r\n", lines); var text = string.Join("\r\n", lines);

View File

@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Testing
ThreadPool = new LoggingThreadPool(Log); ThreadPool = new LoggingThreadPool(Log);
SystemClock = new MockSystemClock(); SystemClock = new MockSystemClock();
DateHeaderValueManager = new DateHeaderValueManager(SystemClock); DateHeaderValueManager = new DateHeaderValueManager(SystemClock);
ConnectionManager = new FrameConnectionManager(Log); ConnectionManager = new FrameConnectionManager(Log, ResourceCounter.Unlimited, ResourceCounter.Unlimited);
DateHeaderValue = DateHeaderValueManager.GetDateHeaderValues().String; DateHeaderValue = DateHeaderValueManager.GetDateHeaderValues().String;
HttpParserFactory = frameAdapter => new HttpParser<FrameAdapter>(frameAdapter.Frame.ServiceContext.Log.IsEnabled(LogLevel.Information)); HttpParserFactory = frameAdapter => new HttpParser<FrameAdapter>(frameAdapter.Frame.ServiceContext.Log.IsEnabled(LogLevel.Information));
ServerOptions = new KestrelServerOptions ServerOptions = new KestrelServerOptions