Improve timeout logic

This commit is contained in:
Stephen Halter 2017-08-30 17:25:16 -07:00
parent 66d8e8198d
commit 1e465e9643
22 changed files with 452 additions and 221 deletions

View File

@ -7,8 +7,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks
{
public class MockTimeoutControl : ITimeoutControl
{
public bool TimedOut { get; }
public void CancelTimeout()
{
}

View File

@ -345,4 +345,10 @@
<data name="PositiveNumberOrNullMinDataRateRequired" xml:space="preserve">
<value>Value must be a positive number. To disable a minimum data rate, use null where a MinDataRate instance is expected.</value>
</data>
<data name="ConcurrentTimeoutsNotSupported" xml:space="preserve">
<value>Concurrent timeouts are not supported.</value>
</data>
<data name="PositiveFiniteTimeSpanRequired" xml:space="preserve">
<value>Timespan must be positive and finite.</value>
</data>
</root>

View File

@ -0,0 +1,31 @@
// 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;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
{
/// <summary>
/// Feature for efficiently handling connection timeouts.
/// </summary>
public interface IConnectionTimeoutFeature
{
/// <summary>
/// Close the connection after the specified positive finite <see cref="TimeSpan"/>
/// unless the timeout is canceled or reset. This will fail if there is an ongoing timeout.
/// </summary>
void SetTimeout(TimeSpan timeSpan);
/// <summary>
/// Close the connection after the specified positive finite <see cref="TimeSpan"/>
/// unless the timeout is canceled or reset. This will cancel any ongoing timeouts.
/// </summary>
void ResetTimeout(TimeSpan timeSpan);
/// <summary>
/// Prevent the connection from closing after a timeout specified by <see cref="SetTimeout(TimeSpan)"/>
/// or <see cref="ResetTimeout(TimeSpan)"/>.
/// </summary>
void CancelTimeout();
}
}

View File

@ -10,7 +10,6 @@ using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
@ -20,7 +19,7 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
public class FrameConnection : ITimeoutControl
public class FrameConnection : ITimeoutControl, IConnectionTimeoutFeature
{
private const int Http2ConnectionNotStarted = 0;
private const int Http2ConnectionStarted = 1;
@ -58,7 +57,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
internal Frame Frame => _frame;
internal IDebugger Debugger { get; set; } = DebuggerWrapper.Singleton;
public bool TimedOut { get; private set; }
// For testing
internal bool RequestTimedOut { get; private set; }
public string ConnectionId => _context.ConnectionId;
public IPEndPoint LocalEndPoint => _context.LocalEndPoint;
@ -106,8 +106,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
adaptedPipeline = new AdaptedPipeline(transport,
application,
PipeFactory.Create(AdaptedInputPipeOptions),
PipeFactory.Create(AdaptedOutputPipeOptions));
PipeFactory.Create(AdaptedInputPipeOptions),
PipeFactory.Create(AdaptedOutputPipeOptions));
transport = adaptedPipeline;
}
@ -134,6 +134,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_context.ServiceContext.ConnectionManager.AddConnection(_context.FrameConnectionId, this);
_lastTimestamp = _context.ServiceContext.SystemClock.UtcNow.Ticks;
_frame.ConnectionFeatures.Set<IConnectionTimeoutFeature>(this);
if (adaptedPipeline != null)
{
// Stream can be null here and run async will close the connection in that case
@ -195,7 +197,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_socketClosedTcs.TrySetResult(null);
}
public Task StopAsync()
public Task StopProcessingNextRequestAsync()
{
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
@ -205,7 +207,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
}
else
{
_frame.Stop();
_frame.StopProcessingNextRequest();
}
return _lifetimeTask;
@ -233,19 +235,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
return _lifetimeTask;
}
public void SetTimeoutResponse()
public void SendTimeoutResponse()
{
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
_frame.SetBadRequestState(RequestRejectionReason.RequestTimeout);
RequestTimedOut = true;
_frame.SendTimeoutResponse();
}
public void Timeout()
public void StopProcessingNextRequest()
{
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
TimedOut = true;
_frame.Stop();
_frame.StopProcessingNextRequest();
}
private async Task<Stream> ApplyConnectionAdaptersAsync()
@ -303,11 +305,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private void CheckForTimeout(long timestamp)
{
if (TimedOut)
{
return;
}
// TODO: Use PlatformApis.VolatileRead equivalent again
if (timestamp > Interlocked.Read(ref _timeoutTimestamp))
{
@ -315,12 +312,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
CancelTimeout();
if (_timeoutAction == TimeoutAction.SendTimeoutResponse)
switch (_timeoutAction)
{
SetTimeoutResponse();
case TimeoutAction.StopProcessingNextRequest:
StopProcessingNextRequest();
break;
case TimeoutAction.SendTimeoutResponse:
SendTimeoutResponse();
break;
case TimeoutAction.AbortConnection:
Abort(new TimeoutException());
break;
}
Timeout();
}
}
}
@ -330,7 +333,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
// The only time when both a timeout is set and the read data rate could be enforced is
// when draining the request body. Since there's already a (short) timeout set for draining,
// it's safe to not check the data rate at this point.
if (TimedOut || Interlocked.Read(ref _timeoutTimestamp) != long.MaxValue)
if (Interlocked.Read(ref _timeoutTimestamp) != long.MaxValue)
{
return;
}
@ -352,7 +355,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
if (rate < minRequestBodyDataRate.BytesPerSecond && !Debugger.IsAttached)
{
Log.RequestBodyMininumDataRateNotSatisfied(_context.ConnectionId, _frame.TraceIdentifier, minRequestBodyDataRate.BytesPerSecond);
Timeout();
SendTimeoutResponse();
}
}
@ -370,16 +373,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private void CheckForWriteDataRateTimeout(long timestamp)
{
if (TimedOut)
{
return;
}
lock (_writeTimingLock)
{
if (_writeTimingWrites > 0 && timestamp > _writeTimingTimeoutTimestamp && !Debugger.IsAttached)
{
TimedOut = true;
RequestTimedOut = true;
Log.ResponseMininumDataRateNotSatisfied(_frame.ConnectionIdFeature, _frame.TraceIdentifier);
Abort(new TimeoutException());
}
@ -484,5 +482,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_writeTimingWrites--;
}
}
void IConnectionTimeoutFeature.SetTimeout(TimeSpan timeSpan)
{
if (timeSpan < TimeSpan.Zero)
{
throw new ArgumentException(CoreStrings.PositiveFiniteTimeSpanRequired, nameof(timeSpan));
}
if (_timeoutTimestamp != long.MaxValue)
{
throw new InvalidOperationException(CoreStrings.ConcurrentTimeoutsNotSupported);
}
SetTimeout(timeSpan.Ticks, TimeoutAction.AbortConnection);
}
void IConnectionTimeoutFeature.ResetTimeout(TimeSpan timeSpan)
{
if (timeSpan < TimeSpan.Zero)
{
throw new ArgumentException(CoreStrings.PositiveFiniteTimeSpanRequired, nameof(timeSpan));
}
ResetTimeout(timeSpan.Ticks, TimeoutAction.AbortConnection);
}
}
}

View File

@ -15,7 +15,6 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Protocols;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
@ -52,13 +51,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected Stack<KeyValuePair<Func<object, Task>, object>> _onStarting;
protected Stack<KeyValuePair<Func<object, Task>, object>> _onCompleted;
protected volatile bool _requestProcessingStopping; // volatile, see: https://msdn.microsoft.com/en-us/library/x13ttww7.aspx
protected int _requestAborted;
protected volatile int _requestAborted;
private volatile bool _requestTimedOut;
private CancellationTokenSource _abortedCts;
private CancellationToken? _manuallySetRequestAbortToken;
protected RequestProcessingStatus _requestProcessingStatus;
protected bool _keepAlive;
protected volatile bool _keepAlive = true; // volatile, see: https://msdn.microsoft.com/en-us/library/x13ttww7.aspx
protected bool _upgradeAvailable;
private volatile bool _wasUpgraded;
private bool _canHaveBody;
@ -111,6 +110,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public IPipeReader Input => _frameContext.Transport.Input;
public OutputProducer Output { get; }
public ITimeoutControl TimeoutControl => _frameContext.TimeoutControl;
public bool RequestTimedOut => _requestTimedOut;
protected IKestrelTrace Log => ServiceContext.Log;
private DateHeaderValueManager DateHeaderValueManager => ServiceContext.DateHeaderValueManager;
@ -260,7 +260,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
var cts = _abortedCts;
return
cts != null ? cts.Token :
(Volatile.Read(ref _requestAborted) == 1) ? new CancellationToken(true) :
(_requestAborted == 1) ? new CancellationToken(true) :
RequestAbortedSource.Token;
}
set
@ -285,7 +285,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
var cts = LazyInitializer.EnsureInitialized(ref _abortedCts, () => new CancellationTokenSource())
?? new CancellationTokenSource();
if (Volatile.Read(ref _requestAborted) == 1)
if (_requestAborted == 1)
{
cts.Cancel();
}
@ -329,9 +329,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_onCompleted = null;
_requestProcessingStatus = RequestProcessingStatus.RequestPending;
_keepAlive = false;
_autoChunk = false;
_applicationException = null;
_requestTimedOut = false;
_requestRejectedException = null;
ResetFeatureCollection();
@ -389,9 +390,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
/// Called on all active connections when the server wants to initiate a shutdown
/// and after a keep-alive timeout.
/// </summary>
public void Stop()
public void StopProcessingNextRequest()
{
_requestProcessingStopping = true;
_keepAlive = false;
Input.CancelPendingRead();
}
public void SendTimeoutResponse()
{
_requestTimedOut = true;
Input.CancelPendingRead();
}
@ -415,7 +422,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
if (Interlocked.Exchange(ref _requestAborted, 1) == 0)
{
_requestProcessingStopping = true;
_keepAlive = false;
_frameStreams?.Abort(error);
@ -789,7 +796,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected Task TryProduceInvalidRequestResponse()
{
if (_requestRejectedException != null)
// If _requestAborted is set, the connection has already been closed.
if (_requestRejectedException != null && _requestAborted == 0)
{
return ProduceEnd();
}
@ -804,7 +812,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
if (HasResponseStarted)
{
// We can no longer change the response, so we simply close the connection.
_requestProcessingStopping = true;
_keepAlive = false;
return Task.CompletedTask;
}
@ -883,9 +891,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
var hasTransferEncoding = responseHeaders.HasTransferEncoding;
var transferCoding = FrameHeaders.GetFinalTransferCoding(responseHeaders.HeaderTransferEncoding);
if (_keepAlive && hasConnection)
if (_keepAlive && hasConnection && (connectionOptions & ConnectionOptions.KeepAlive) != ConnectionOptions.KeepAlive)
{
_keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive;
_keepAlive = false;
}
// https://tools.ietf.org/html/rfc7230#section-3.3.1
@ -1171,7 +1179,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
_keepAlive = false;
_requestProcessingStopping = true;
_requestRejectedException = ex;
}

View File

@ -3,7 +3,6 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Protocols;
@ -32,15 +31,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
try
{
while (!_requestProcessingStopping)
while (_keepAlive)
{
TimeoutControl.SetTimeout(_keepAliveTicks, TimeoutAction.CloseConnection);
Reset();
TimeoutControl.SetTimeout(_keepAliveTicks, TimeoutAction.StopProcessingNextRequest);
while (!_requestProcessingStopping)
while (_requestProcessingStatus != RequestProcessingStatus.AppStarted)
{
var result = await Input.ReadAsync();
var examined = result.Buffer.End;
var consumed = result.Buffer.End;
@ -62,11 +61,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
Input.Advance(consumed, examined);
}
if (_requestProcessingStatus == RequestProcessingStatus.AppStarted)
{
break;
}
if (result.IsCompleted)
{
switch (_requestProcessingStatus)
@ -81,132 +75,140 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
RequestRejectionReason.MalformedRequestInvalidHeaders);
}
}
else if (!_keepAlive && _requestProcessingStatus == RequestProcessingStatus.RequestPending)
{
// Stop the request processing loop if the server is shutting down or there was a keep-alive timeout
// and there is no ongoing request.
return;
}
else if (RequestTimedOut)
{
// In this case, there is an ongoing request but the start line/header parsing has timed out, so send
// a 408 response.
throw BadHttpRequestException.GetException(RequestRejectionReason.RequestTimeout);
}
}
if (!_requestProcessingStopping)
EnsureHostHeaderExists();
var messageBody = MessageBody.For(_httpVersion, FrameRequestHeaders, this);
if (!messageBody.RequestKeepAlive)
{
EnsureHostHeaderExists();
_keepAlive = false;
}
var messageBody = MessageBody.For(_httpVersion, FrameRequestHeaders, this);
_keepAlive = messageBody.RequestKeepAlive;
_upgradeAvailable = messageBody.RequestUpgrade;
_upgradeAvailable = messageBody.RequestUpgrade;
InitializeStreams(messageBody);
InitializeStreams(messageBody);
var context = _application.CreateContext(this);
var context = _application.CreateContext(this);
try
{
try
{
try
KestrelEventSource.Log.RequestStart(this);
await _application.ProcessRequestAsync(context);
if (_requestAborted == 0)
{
KestrelEventSource.Log.RequestStart(this);
await _application.ProcessRequestAsync(context);
if (Volatile.Read(ref _requestAborted) == 0)
{
VerifyResponseContentLength();
}
}
catch (Exception ex)
{
ReportApplicationError(ex);
if (ex is BadHttpRequestException)
{
throw;
}
}
finally
{
KestrelEventSource.Log.RequestStop(this);
// Trigger OnStarting if it hasn't been called yet and the app hasn't
// already failed. If an OnStarting callback throws we can go through
// our normal error handling in ProduceEnd.
// https://github.com/aspnet/KestrelHttpServer/issues/43
if (!HasResponseStarted && _applicationException == null && _onStarting != null)
{
await FireOnStarting();
}
PauseStreams();
if (_onCompleted != null)
{
await FireOnCompleted();
}
}
// If _requestAbort is set, the connection has already been closed.
if (Volatile.Read(ref _requestAborted) == 0)
{
if (HasResponseStarted)
{
// If the response has already started, call ProduceEnd() before
// consuming the rest of the request body to prevent
// delaying clients waiting for the chunk terminator:
//
// https://github.com/dotnet/corefx/issues/17330#issuecomment-288248663
//
// ProduceEnd() must be called before _application.DisposeContext(), to ensure
// HttpContext.Response.StatusCode is correctly set when
// IHttpContextFactory.Dispose(HttpContext) is called.
await ProduceEnd();
}
// ForZeroContentLength does not complete the reader nor the writer
if (!messageBody.IsEmpty && _keepAlive)
{
// Finish reading the request body in case the app did not.
TimeoutControl.SetTimeout(Constants.RequestBodyDrainTimeout.Ticks, TimeoutAction.SendTimeoutResponse);
await messageBody.ConsumeAsync();
TimeoutControl.CancelTimeout();
}
if (!HasResponseStarted)
{
await ProduceEnd();
}
}
else if (!HasResponseStarted)
{
// If the request was aborted and no response was sent, there's no
// meaningful status code to log.
StatusCode = 0;
VerifyResponseContentLength();
}
}
catch (BadHttpRequestException ex)
catch (Exception ex)
{
// Handle BadHttpRequestException thrown during app execution or remaining message body consumption.
// This has to be caught here so StatusCode is set properly before disposing the HttpContext
// (DisposeContext logs StatusCode).
SetBadRequestState(ex);
ReportApplicationError(ex);
if (ex is BadHttpRequestException)
{
throw;
}
}
finally
{
_application.DisposeContext(context, _applicationException);
KestrelEventSource.Log.RequestStop(this);
// StopStreams should be called before the end of the "if (!_requestProcessingStopping)" block
// to ensure InitializeStreams has been called.
StopStreams();
if (HasStartedConsumingRequestBody)
// Trigger OnStarting if it hasn't been called yet and the app hasn't
// already failed. If an OnStarting callback throws we can go through
// our normal error handling in ProduceEnd.
// https://github.com/aspnet/KestrelHttpServer/issues/43
if (!HasResponseStarted && _applicationException == null && _onStarting != null)
{
RequestBodyPipe.Reader.Complete();
await FireOnStarting();
}
// Wait for MessageBody.PumpAsync() to call RequestBodyPipe.Writer.Complete().
await messageBody.StopAsync();
PauseStreams();
// At this point both the request body pipe reader and writer should be completed.
RequestBodyPipe.Reset();
if (_onCompleted != null)
{
await FireOnCompleted();
}
}
}
if (!_keepAlive)
// If _requestAbort is set, the connection has already been closed.
if (_requestAborted == 0)
{
if (HasResponseStarted)
{
// If the response has already started, call ProduceEnd() before
// consuming the rest of the request body to prevent
// delaying clients waiting for the chunk terminator:
//
// https://github.com/dotnet/corefx/issues/17330#issuecomment-288248663
//
// ProduceEnd() must be called before _application.DisposeContext(), to ensure
// HttpContext.Response.StatusCode is correctly set when
// IHttpContextFactory.Dispose(HttpContext) is called.
await ProduceEnd();
}
// ForZeroContentLength does not complete the reader nor the writer
if (!messageBody.IsEmpty && _keepAlive)
{
// Finish reading the request body in case the app did not.
TimeoutControl.SetTimeout(Constants.RequestBodyDrainTimeout.Ticks, TimeoutAction.SendTimeoutResponse);
await messageBody.ConsumeAsync();
TimeoutControl.CancelTimeout();
}
if (!HasResponseStarted)
{
await ProduceEnd();
}
}
else if (!HasResponseStarted)
{
// If the request was aborted and no response was sent, there's no
// meaningful status code to log.
StatusCode = 0;
}
}
catch (BadHttpRequestException ex)
{
// End the connection for non keep alive as data incoming may have been thrown off
return;
// Handle BadHttpRequestException thrown during app execution or remaining message body consumption.
// This has to be caught here so StatusCode is set properly before disposing the HttpContext
// (DisposeContext logs StatusCode).
SetBadRequestState(ex);
}
finally
{
_application.DisposeContext(context, _applicationException);
// StopStreams should be called before the end of the "if (!_requestProcessingStopping)" block
// to ensure InitializeStreams has been called.
StopStreams();
if (HasStartedConsumingRequestBody)
{
RequestBodyPipe.Reader.Complete();
// Wait for MessageBody.PumpAsync() to call RequestBodyPipe.Writer.Complete().
await messageBody.StopAsync();
// At this point both the request body pipe reader and writer should be completed.
RequestBodyPipe.Reset();
}
}
}
}
@ -237,13 +239,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
try
{
Input.Complete();
// If _requestAborted is set, the connection has already been closed.
if (Volatile.Read(ref _requestAborted) == 0)
{
await TryProduceInvalidRequestResponse();
Output.Dispose();
}
await TryProduceInvalidRequestResponse();
Output.Dispose();
}
catch (Exception ex)
{

View File

@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
var result = await awaitable;
if (_context.TimeoutControl.TimedOut)
if (_context.RequestTimedOut)
{
_context.ThrowRequestRejected(RequestRejectionReason.RequestTimeout);
}

View File

@ -50,8 +50,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public IKestrelTrace Log => _context.ServiceContext.Log;
bool ITimeoutControl.TimedOut => throw new NotImplementedException();
public void Abort(Exception ex)
{
_stopping = true;

View File

@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
connectionManager.Walk(connection =>
{
closeTasks.Add(connection.StopAsync());
closeTasks.Add(connection.StopProcessingNextRequestAsync());
});
var allClosedTask = Task.WhenAll(closeTasks.ToArray());

View File

@ -5,8 +5,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
public interface ITimeoutControl
{
bool TimedOut { get; }
void SetTimeout(long ticks, TimeoutAction timeoutAction);
void ResetTimeout(long ticks, TimeoutAction timeoutAction);
void CancelTimeout();

View File

@ -5,7 +5,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
public enum TimeoutAction
{
CloseConnection,
SendTimeoutResponse
StopProcessingNextRequest,
SendTimeoutResponse,
AbortConnection,
}
}

View File

@ -1074,6 +1074,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatPositiveNumberOrNullMinDataRateRequired()
=> GetString("PositiveNumberOrNullMinDataRateRequired");
/// <summary>
/// Concurrent timeouts are not supported.
/// </summary>
internal static string ConcurrentTimeoutsNotSupported
{
get => GetString("ConcurrentTimeoutsNotSupported");
}
/// <summary>
/// Concurrent timeouts are not supported.
/// </summary>
internal static string FormatConcurrentTimeoutsNotSupported()
=> GetString("ConcurrentTimeoutsNotSupported");
/// <summary>
/// Timespan must be positive and finite.
/// </summary>
internal static string PositiveFiniteTimeSpanRequired
{
get => GetString("PositiveFiniteTimeSpanRequired");
}
/// <summary>
/// Timespan must be positive and finite.
/// </summary>
internal static string FormatPositiveFiniteTimeSpanRequired()
=> GetString("PositiveFiniteTimeSpanRequired");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -5,6 +5,7 @@ using System;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
namespace Microsoft.AspNetCore.Server.Kestrel.Https
{
@ -13,6 +14,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
/// </summary>
public class HttpsConnectionAdapterOptions
{
private TimeSpan _handshakeTimeout;
/// <summary>
/// Initializes a new instance of <see cref="HttpsConnectionAdapterOptions"/>.
/// </summary>
@ -20,6 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
{
ClientCertificateMode = ClientCertificateMode.NoCertificate;
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11;
HandshakeTimeout = TimeSpan.FromSeconds(10);
}
/// <summary>
@ -51,5 +55,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
/// Specifies whether the certificate revocation list is checked during authentication.
/// </summary>
public bool CheckCertificateRevocation { get; set; }
/// <summary>
/// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite.
/// </summary>
public TimeSpan HandshakeTimeout
{
get => _handshakeTimeout;
set
{
if (value <= TimeSpan.Zero && value != Timeout.InfiniteTimeSpan)
{
throw new ArgumentOutOfRangeException(nameof(value), HttpsStrings.PositiveTimeSpanRequired);
}
_handshakeTimeout = value != Timeout.InfiniteTimeSpan ? value : TimeSpan.MaxValue;
}
}
}
}

View File

@ -120,9 +120,15 @@
<data name="AuthenticationFailed" xml:space="preserve">
<value>Failed to authenticate HTTPS connection.</value>
</data>
<data name="AuthenticationTimedOut" xml:space="preserve">
<value>Authentication of the HTTPS connection timed out.</value>
</data>
<data name="InvalidServerCertificateEku" xml:space="preserve">
<value>Certificate {thumbprint} cannot be used as an SSL server certificate. It has an Extended Key Usage extension but the usages do not include Server Authentication (OID 1.3.6.1.5.5.7.3.1).</value>
</data>
<data name="PositiveTimeSpanRequired" xml:space="preserve">
<value>Value must be a positive TimeSpan.</value>
</data>
<data name="ServiceCertificateRequired" xml:space="preserve">
<value>The server certificate parameter is required.</value>
</data>

View File

@ -9,6 +9,7 @@ using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
@ -108,17 +109,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
certificateRequired = true;
}
var timeoutFeature = context.Features.Get<IConnectionTimeoutFeature>();
timeoutFeature.SetTimeout(_options.HandshakeTimeout);
try
{
await sslStream.AuthenticateAsServerAsync(_serverCertificate, certificateRequired,
_options.SslProtocols, _options.CheckCertificateRevocation);
}
catch (OperationCanceledException)
{
_logger?.LogInformation(2, HttpsStrings.AuthenticationTimedOut);
sslStream.Dispose();
return _closedAdaptedConnection;
}
catch (IOException ex)
{
_logger?.LogInformation(1, ex, HttpsStrings.AuthenticationFailed);
sslStream.Dispose();
return _closedAdaptedConnection;
}
finally
{
timeoutFeature.CancelTimeout();
}
// Always set the feature even though the cert might be null
context.Features.Set<ITlsConnectionFeature>(new TlsConnectionFeature

View File

@ -24,6 +24,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
internal static string FormatAuthenticationFailed()
=> GetString("AuthenticationFailed");
/// <summary>
/// Authentication of the HTTPS connection timed out.
/// </summary>
internal static string AuthenticationTimedOut
{
get => GetString("AuthenticationTimedOut");
}
/// <summary>
/// Authentication of the HTTPS connection timed out.
/// </summary>
internal static string FormatAuthenticationTimedOut()
=> GetString("AuthenticationTimedOut");
/// <summary>
/// Certificate {thumbprint} cannot be used as an SSL server certificate. It has an Extended Key Usage extension but the usages do not include Server Authentication (OID 1.3.6.1.5.5.7.3.1).
/// </summary>
@ -38,6 +52,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
internal static string FormatInvalidServerCertificateEku(object thumbprint)
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidServerCertificateEku", "thumbprint"), thumbprint);
/// <summary>
/// Value must be a positive TimeSpan.
/// </summary>
internal static string PositiveTimeSpanRequired
{
get => GetString("PositiveTimeSpanRequired");
}
/// <summary>
/// Value must be a positive TimeSpan.
/// </summary>
internal static string FormatPositiveTimeSpanRequired()
=> GetString("PositiveTimeSpanRequired");
/// <summary>
/// The server certificate parameter is required.
/// </summary>

View File

@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.SetTimeout(1, TimeoutAction.SendTimeoutResponse);
_frameConnection.Tick(now.AddTicks(2).Add(Heartbeat.Interval));
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
}
[Fact]
@ -77,7 +77,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
TickBodyWithMinimumDataRate(mockLogger.Object, bytesPerSecond);
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
}
[Fact]
@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
TickBodyWithMinimumDataRate(mockLogger.Object, bytesPerSecond);
// Timed out
Assert.True(_frameConnection.TimedOut);
Assert.True(_frameConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Once);
}
@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(now);
// Not timed out
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Never);
@ -154,7 +154,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(now);
// Timed out
Assert.True(_frameConnection.TimedOut);
Assert.True(_frameConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Once);
}
@ -191,7 +191,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(now);
// Not timed out
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Never);
@ -201,7 +201,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(now);
// Not timed out
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Never);
@ -211,7 +211,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(now);
// Not timed out
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Never);
@ -221,7 +221,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(now);
// Not timed out
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Never);
@ -231,7 +231,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(now);
// Timed out
Assert.True(_frameConnection.TimedOut);
Assert.True(_frameConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Once);
}
@ -274,7 +274,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Never);
@ -288,7 +288,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Never);
@ -298,7 +298,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(systemClock.UtcNow);
// Timed out
Assert.True(_frameConnection.TimedOut);
Assert.True(_frameConnection.RequestTimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Once);
@ -330,7 +330,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Never);
@ -349,7 +349,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Never);
@ -359,7 +359,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(systemClock.UtcNow);
// Timed out
Assert.True(_frameConnection.TimedOut);
Assert.True(_frameConnection.RequestTimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Once);
@ -388,7 +388,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.StartTimingReads();
_frameConnection.SetTimeout(timeout.Ticks, TimeoutAction.CloseConnection);
_frameConnection.SetTimeout(timeout.Ticks, TimeoutAction.StopProcessingNextRequest);
// Tick beyond grace period with low data rate
systemClock.UtcNow += TimeSpan.FromSeconds(3);
@ -396,14 +396,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
// Tick just past timeout period, adjusted by Heartbeat.Interval
systemClock.UtcNow = startTime + timeout + Heartbeat.Interval + TimeSpan.FromTicks(1);
_frameConnection.Tick(systemClock.UtcNow);
// Timed out
Assert.True(_frameConnection.TimedOut);
Assert.True(_frameConnection.RequestTimedOut);
}
[Fact]
@ -436,7 +436,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
systemClock.UtcNow += TimeSpan.FromSeconds(4) + Heartbeat.Interval + TimeSpan.FromTicks(1);
_frameConnection.Tick(systemClock.UtcNow);
Assert.True(_frameConnection.TimedOut);
Assert.True(_frameConnection.RequestTimedOut);
Assert.True(aborted.Wait(TimeSpan.FromSeconds(10)));
}
@ -472,13 +472,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(systemClock.UtcNow);
// Still within grace period, not timed out
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
// Tick just past grace period (adjusted by Heartbeat.Interval)
systemClock.UtcNow = startTime + minResponseDataRate.GracePeriod + Heartbeat.Interval + TimeSpan.FromTicks(1);
_frameConnection.Tick(systemClock.UtcNow);
Assert.True(_frameConnection.TimedOut);
Assert.True(_frameConnection.RequestTimedOut);
Assert.True(aborted.Wait(TimeSpan.FromSeconds(10)));
}
@ -516,7 +516,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frameConnection.Tick(systemClock.UtcNow);
// Not timed out because the timeout was pushed by the second write
Assert.False(_frameConnection.TimedOut);
Assert.False(_frameConnection.RequestTimedOut);
// Complete the first write, this should have no effect on the timeout
_frameConnection.StopTimingWrite();
@ -525,7 +525,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
systemClock.UtcNow += TimeSpan.FromSeconds(3) + TimeSpan.FromTicks(1);
_frameConnection.Tick(systemClock.UtcNow);
Assert.True(_frameConnection.TimedOut);
Assert.True(_frameConnection.RequestTimedOut);
Assert.True(aborted.Wait(TimeSpan.FromSeconds(10)));
}
}

View File

@ -511,9 +511,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var requestProcessingTask = _frame.ProcessRequestsAsync();
var expectedKeepAliveTimeout = _serviceContext.ServerOptions.Limits.KeepAliveTimeout.Ticks;
_timeoutControl.Verify(cc => cc.SetTimeout(expectedKeepAliveTimeout, TimeoutAction.CloseConnection));
_timeoutControl.Verify(cc => cc.SetTimeout(expectedKeepAliveTimeout, TimeoutAction.StopProcessingNextRequest));
_frame.Stop();
_frame.StopProcessingNextRequest();
_application.Output.Complete();
requestProcessingTask.Wait();
@ -598,7 +598,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var data = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n");
await _application.Output.WriteAsync(data);
_frame.Stop();
_frame.StopProcessingNextRequest();
Assert.IsNotType<Task<Task>>(requestProcessingTask);
await requestProcessingTask.TimeoutAfter(TimeSpan.FromSeconds(10));

View File

@ -314,11 +314,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
TimeSpan.MaxValue,
};
public static TheoryData<TimeSpan> TimeoutInfiniteData => new TheoryData<TimeSpan>
{
Timeout.InfiniteTimeSpan,
};
public static TheoryData<TimeSpan> TimeoutInvalidData => new TheoryData<TimeSpan>
{
TimeSpan.MinValue,

View File

@ -566,11 +566,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal(1, await body.ReadAsync(new ArraySegment<byte>(new byte[1])));
// Time out on the next read
mockTimeoutControl
.Setup(timeoutControl => timeoutControl.TimedOut)
.Returns(true);
input.Cancel();
input.Frame.SendTimeoutResponse();
var exception = await Assert.ThrowsAsync<BadHttpRequestException>(() => body.ReadAsync(new ArraySegment<byte>(new byte[1])));
Assert.Equal(StatusCodes.Status408RequestTimeout, exception.StatusCode);
@ -595,11 +591,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal(1, await body.ReadAsync(new ArraySegment<byte>(new byte[1])));
// Time out on the next read
mockTimeoutControl
.Setup(timeoutControl => timeoutControl.TimedOut)
.Returns(true);
input.Cancel();
input.Frame.SendTimeoutResponse();
var exception = await Assert.ThrowsAsync<BadHttpRequestException>(() => body.ConsumeAsync());
Assert.Equal(StatusCodes.Status408RequestTimeout, exception.StatusCode);
@ -624,11 +616,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal(1, await body.ReadAsync(new ArraySegment<byte>(new byte[1])));
// Time out on the next read
mockTimeoutControl
.Setup(timeoutControl => timeoutControl.TimedOut)
.Returns(true);
input.Cancel();
input.Frame.SendTimeoutResponse();
using (var ms = new MemoryStream())
{

View File

@ -0,0 +1,55 @@
// 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;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
public class HttpsConnectionAdapterOptionsTests
{
[Fact]
public void HandshakeTimeoutDefault()
{
Assert.Equal(TimeSpan.FromSeconds(10), new HttpsConnectionAdapterOptions().HandshakeTimeout);
}
[Theory]
[MemberData(nameof(TimeoutValidData))]
public void HandshakeTimeoutValid(TimeSpan value)
{
Assert.Equal(value, new HttpsConnectionAdapterOptions { HandshakeTimeout = value }.HandshakeTimeout);
}
[Fact]
public void HandshakeTimeoutCanBeSetToInfinite()
{
Assert.Equal(TimeSpan.MaxValue, new HttpsConnectionAdapterOptions { HandshakeTimeout = Timeout.InfiniteTimeSpan }.HandshakeTimeout);
}
[Theory]
[MemberData(nameof(TimeoutInvalidData))]
public void HandshakeTimeoutInvalid(TimeSpan value)
{
var exception = Assert.Throws<ArgumentOutOfRangeException>(() => new HttpsConnectionAdapterOptions { HandshakeTimeout = value });
Assert.Equal("value", exception.ParamName);
Assert.StartsWith(HttpsStrings.PositiveTimeSpanRequired, exception.Message);
}
public static TheoryData<TimeSpan> TimeoutValidData => new TheoryData<TimeSpan>
{
TimeSpan.FromTicks(1),
TimeSpan.MaxValue,
};
public static TheoryData<TimeSpan> TimeoutInvalidData => new TheoryData<TimeSpan>
{
TimeSpan.MinValue,
TimeSpan.FromTicks(-1),
TimeSpan.Zero
};
}
}

View File

@ -3,15 +3,18 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
@ -258,6 +261,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
}
}
[Fact]
public async Task HandshakeTimesOutAndIsLoggedAsInformation()
{
var loggerProvider = new HandshakeErrorLoggerProvider();
var hostBuilder = new WebHostBuilder()
.UseKestrel(options =>
{
options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions =>
{
listenOptions.UseHttps(new HttpsConnectionAdapterOptions
{
ServerCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"),
HandshakeTimeout = TimeSpan.FromSeconds(1)
});
});
})
.ConfigureLogging(builder => builder.AddProvider(loggerProvider))
.Configure(app => app.Run(httpContext => Task.CompletedTask));
using (var host = hostBuilder.Build())
{
host.Start();
using (var socket = await HttpClientSlim.GetSocket(new Uri($"https://127.0.0.1:{host.GetPort()}/")))
using (var stream = new NetworkStream(socket, ownsSocket: false))
{
// No data should be sent and the connection should be closed in well under 30 seconds.
Assert.Equal(0, await stream.ReadAsync(new byte[1], 0, 1).TimeoutAfter(TimeSpan.FromSeconds(30)));
}
}
await loggerProvider.FilterLogger.LogTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
Assert.Equal(2, loggerProvider.FilterLogger.LastEventId);
Assert.Equal(LogLevel.Information, loggerProvider.FilterLogger.LastLogLevel);
}
private class HandshakeErrorLoggerProvider : ILoggerProvider
{
public HttpsConnectionFilterLogger FilterLogger { get; } = new HttpsConnectionFilterLogger();