diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs index 196a6e941f..f5981b5607 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs @@ -102,6 +102,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel case RequestRejectionReason.TooManyHeaders: ex = new BadHttpRequestException("Request contains too many headers.", 431); break; + case RequestRejectionReason.RequestTimeout: + ex = new BadHttpRequestException("Request timed out.", 408); + break; default: ex = new BadHttpRequestException("Bad request.", 400); break; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs index 877e66fac0..2ba693ef73 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs @@ -41,6 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private long _lastTimestamp; private long _timeoutTimestamp = long.MaxValue; + private TimeoutAction _timeoutAction; public Connection(ListenerContext context, UvStreamHandle socket) : base(context) { @@ -170,8 +171,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http // Called on Libuv thread public void Tick(long timestamp) { - if (timestamp > _timeoutTimestamp) + if (timestamp > Interlocked.Read(ref _timeoutTimestamp)) { + ConnectionControl.CancelTimeout(); + + if (_timeoutAction == TimeoutAction.SendTimeoutResponse) + { + _frame.SetBadRequestState(RequestRejectionReason.RequestTimeout); + } + StopAsync(); } @@ -299,12 +307,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http } } - void IConnectionControl.SetTimeout(long milliseconds) + void IConnectionControl.SetTimeout(long milliseconds, TimeoutAction timeoutAction) { Debug.Assert(_timeoutTimestamp == long.MaxValue, "Concurrent timeouts are not supported"); - // Add KestrelThread.HeartbeatMilliseconds extra milliseconds since this can be called right before the next heartbeat. - Interlocked.Exchange(ref _timeoutTimestamp, _lastTimestamp + milliseconds + KestrelThread.HeartbeatMilliseconds); + AssignTimeout(milliseconds, timeoutAction); + } + + void IConnectionControl.ResetTimeout(long milliseconds, TimeoutAction timeoutAction) + { + AssignTimeout(milliseconds, timeoutAction); } void IConnectionControl.CancelTimeout() @@ -312,6 +324,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http Interlocked.Exchange(ref _timeoutTimestamp, long.MaxValue); } + private void AssignTimeout(long milliseconds, TimeoutAction timeoutAction) + { + _timeoutAction = timeoutAction; + + // Add KestrelThread.HeartbeatMilliseconds extra milliseconds since this can be called right before the next heartbeat. + Interlocked.Exchange(ref _timeoutTimestamp, _lastTimestamp + milliseconds + KestrelThread.HeartbeatMilliseconds); + } + private static unsafe string GenerateConnectionId(long id) { // The following routine is ~310% faster than calling long.ToString() on x64 diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index dde74b0f56..767130fd03 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -71,6 +71,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private int _requestHeadersParsed; protected readonly long _keepAliveMilliseconds; + private readonly long _requestHeadersTimeoutMilliseconds; public Frame(ConnectionContext context) { @@ -84,6 +85,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http FrameControl = this; _keepAliveMilliseconds = (long)ServerOptions.Limits.KeepAliveTimeout.TotalMilliseconds; + _requestHeadersTimeoutMilliseconds = (long)ServerOptions.Limits.RequestHeadersTimeout.TotalMilliseconds; } public ConnectionContext ConnectionContext { get; } @@ -372,6 +374,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { _requestProcessingStopping = true; } + return _requestProcessingTask ?? TaskCache.CompletedTask; } @@ -648,7 +651,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http protected Task TryProduceInvalidRequestResponse() { - if (_requestProcessingStatus == RequestProcessingStatus.RequestStarted && _requestRejected) + if (_requestRejected) { if (FrameRequestHeaders == null || FrameResponseHeaders == null) { @@ -833,7 +836,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http return RequestLineStatus.Empty; } - ConnectionControl.CancelTimeout(); + if (_requestProcessingStatus == RequestProcessingStatus.RequestPending) + { + ConnectionControl.ResetTimeout(_requestHeadersTimeoutMilliseconds, TimeoutAction.SendTimeoutResponse); + } _requestProcessingStatus = RequestProcessingStatus.RequestStarted; @@ -1102,6 +1108,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http } else if (ch == '\n') { + ConnectionControl.CancelTimeout(); consumed = end; return true; } @@ -1274,12 +1281,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http throw ex; } + public void SetBadRequestState(RequestRejectionReason reason) + { + SetBadRequestState(BadHttpRequestException.GetException(reason)); + } + public void SetBadRequestState(BadHttpRequestException ex) { - StatusCode = ex.StatusCode; + // Setting status code will throw if response has already started + if (!HasResponseStarted) + { + StatusCode = ex.StatusCode; + } + _keepAlive = false; _requestProcessingStopping = true; _requestRejected = true; + Log.ConnectionBadRequest(ConnectionId, ex); } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameOfT.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameOfT.cs index 99d56f1fbf..fd8306baf7 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameOfT.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameOfT.cs @@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { while (!_requestProcessingStopping) { - ConnectionControl.SetTimeout(_keepAliveMilliseconds); + ConnectionControl.SetTimeout(_keepAliveMilliseconds, TimeoutAction.CloseConnection); while (!_requestProcessingStopping && TakeStartLine(SocketInput) != RequestLineStatus.Done) { @@ -141,7 +141,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http } } - Reset(); + // Don't lose request rejection state + if (!_requestRejected) + { + Reset(); + } } } catch (BadHttpRequestException ex) diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IConnectionControl.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IConnectionControl.cs index f7de78dfcf..2905913040 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IConnectionControl.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IConnectionControl.cs @@ -8,7 +8,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http void Pause(); void Resume(); void End(ProduceEndType endType); - void SetTimeout(long milliseconds); + void SetTimeout(long milliseconds, TimeoutAction timeoutAction); + void ResetTimeout(long milliseconds, TimeoutAction timeoutAction); void CancelTimeout(); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs index a0d04ed0fc..3bd3aecaf5 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs @@ -34,5 +34,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http HeadersExceedMaxTotalSize, MissingCRInHeaderLine, TooManyHeaders, + RequestTimeout, } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/TimeoutAction.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/TimeoutAction.cs new file mode 100644 index 0000000000..33d832ce01 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/TimeoutAction.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http +{ + public enum TimeoutAction + { + CloseConnection, + SendTimeoutResponse + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs index 57aaa031b9..ac8a047542 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs @@ -23,8 +23,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel // Matches the default LimitRequestFields in Apache httpd. private int _maxRequestHeaderCount = 100; - // Matches the default http.sys connection timeout. - private TimeSpan _connectionTimeout = TimeSpan.FromMinutes(2); + // Matches the default http.sys connectionTimeout. + private TimeSpan _keepAliveTimeout = TimeSpan.FromMinutes(2); + + private TimeSpan _requestHeadersTimeout = TimeSpan.FromSeconds(30); /// /// Gets or sets the maximum size of the response buffer before write @@ -152,11 +154,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel { get { - return _connectionTimeout; + return _keepAliveTimeout; } set { - _connectionTimeout = value; + _keepAliveTimeout = value; + } + } + + /// + /// Gets or sets the maximum amount of time the server will spend receiving request headers. + /// + /// + /// Defaults to 30 seconds. + /// + public TimeSpan RequestHeadersTimeout + { + get + { + return _requestHeadersTimeout; + } + set + { + _requestHeadersTimeout = value; } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs index f5c202a4dd..ce2bcc631c 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs @@ -2,7 +2,6 @@ // 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.Text; using System.Threading; using System.Threading.Tasks; @@ -50,17 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "", ""); await ReceiveResponse(connection, server.Context); - - await Task.Delay(LongDelay); - - await Assert.ThrowsAsync(async () => - { - await connection.Send( - "GET / HTTP/1.1", - "", - ""); - await ReceiveResponse(connection, server.Context); - }); + await connection.WaitForConnectionClose().TimeoutAfter(LongDelay); } } @@ -143,13 +132,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var connection = new TestConnection(server.Port)) { await Task.Delay(LongDelay); - await Assert.ThrowsAsync(async () => - { - await connection.Send( - "GET / HTTP/1.1", - "", - ""); - }); + await connection.WaitForConnectionClose().TimeoutAfter(LongDelay); } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestHeadersTimeoutTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestHeadersTimeoutTests.cs new file mode 100644 index 0000000000..b80a637058 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestHeadersTimeoutTests.cs @@ -0,0 +1,135 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class RequestHeadersTimeoutTests + { + private static readonly TimeSpan RequestHeadersTimeout = TimeSpan.FromSeconds(10); + private static readonly TimeSpan LongDelay = TimeSpan.FromSeconds(30); + private static readonly TimeSpan ShortDelay = TimeSpan.FromSeconds(LongDelay.TotalSeconds / 10); + + [Fact] + public async Task TestRequestHeadersTimeout() + { + using (var server = CreateServer()) + { + var tasks = new[] + { + ConnectionAbortedWhenRequestHeadersNotReceivedInTime(server, ""), + ConnectionAbortedWhenRequestHeadersNotReceivedInTime(server, "Content-Length: 1\r\n"), + ConnectionAbortedWhenRequestHeadersNotReceivedInTime(server, "Content-Length: 1\r\n\r"), + RequestHeadersTimeoutCanceledAfterHeadersReceived(server), + ConnectionAbortedWhenRequestLineNotReceivedInTime(server, "P"), + ConnectionAbortedWhenRequestLineNotReceivedInTime(server, "POST / HTTP/1.1\r"), + TimeoutNotResetOnEachRequestLineCharacterReceived(server) + }; + + await Task.WhenAll(tasks); + } + } + + private async Task ConnectionAbortedWhenRequestHeadersNotReceivedInTime(TestServer server, string headers) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + headers); + await ReceiveTimeoutResponse(connection, server.Context); + } + } + + private async Task RequestHeadersTimeoutCanceledAfterHeadersReceived(TestServer server) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Content-Length: 1", + "", + ""); + await Task.Delay(RequestHeadersTimeout); + await connection.Send( + "a"); + await ReceiveResponse(connection, server.Context); + } + } + + private async Task ConnectionAbortedWhenRequestLineNotReceivedInTime(TestServer server, string requestLine) + { + using (var connection = server.CreateConnection()) + { + await connection.Send(requestLine); + await ReceiveTimeoutResponse(connection, server.Context); + } + } + + private async Task TimeoutNotResetOnEachRequestLineCharacterReceived(TestServer server) + { + using (var connection = server.CreateConnection()) + { + await Assert.ThrowsAsync(async () => + { + foreach (var ch in "POST / HTTP/1.1\r\n\r\n") + { + await connection.Send(ch.ToString()); + await Task.Delay(ShortDelay); + } + }); + } + } + + private TestServer CreateServer() + { + return new TestServer(async httpContext => + { + await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1); + await httpContext.Response.WriteAsync("hello, world"); + }, + new TestServiceContext + { + ServerOptions = new KestrelServerOptions + { + AddServerHeader = false, + Limits = + { + RequestHeadersTimeout = RequestHeadersTimeout + } + } + }); + } + + private async Task ReceiveResponse(TestConnection connection, TestServiceContext testServiceContext) + { + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {testServiceContext.DateHeaderValue}", + "Transfer-Encoding: chunked", + "", + "c", + "hello, world", + "0", + "", + ""); + } + + private async Task ReceiveTimeoutResponse(TestConnection connection, TestServiceContext testServiceContext) + { + await connection.ReceiveForcedEnd( + "HTTP/1.1 408 Request Timeout", + "Connection: close", + $"Date: {testServiceContext.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs index 736807ac62..4bdf8940f1 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Primitives; using Xunit; @@ -178,6 +179,40 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } + // https://github.com/aspnet/KestrelHttpServer/pull/1111/files#r80584475 explains the reason for this test. + [Fact] + public async Task SingleErrorResponseSentWhenAppSwallowsBadRequestException() + { + using (var server = new TestServer(async httpContext => + { + try + { + await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1); + } + catch (BadHttpRequestException) + { + } + }, new TestServiceContext())) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Transfer-Encoding: chunked", + "", + "g", + ""); + await connection.ReceiveForcedEnd( + "HTTP/1.1 400 Bad Request", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + public static TheoryData NullHeaderData { get diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs index 4854a2d369..c59e74160b 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs @@ -36,7 +36,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { ServerAddress = ServerAddress.FromUrl("http://localhost:5000") }; - var connectionContext = new ConnectionContext(listenerContext); + var connectionContext = new ConnectionContext(listenerContext) + { + ConnectionControl = Mock.Of() + }; var frame = new Frame(application: null, context: connectionContext); frame.Reset(); @@ -84,7 +87,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { ServerAddress = ServerAddress.FromUrl("http://localhost:5000") }; - var connectionContext = new ConnectionContext(listenerContext); + var connectionContext = new ConnectionContext(listenerContext) + { + ConnectionControl = Mock.Of() + }; var frame = new Frame(application: null, context: connectionContext); frame.Reset(); @@ -131,7 +137,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { ServerAddress = ServerAddress.FromUrl("http://localhost:5000") }; - var connectionContext = new ConnectionContext(listenerContext); + var connectionContext = new ConnectionContext(listenerContext) + { + ConnectionControl = Mock.Of() + }; var frame = new Frame(application: null, context: connectionContext); frame.Reset(); @@ -178,7 +187,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { ServerAddress = ServerAddress.FromUrl("http://localhost:5000") }; - var connectionContext = new ConnectionContext(listenerContext); + var connectionContext = new ConnectionContext(listenerContext) + { + ConnectionControl = Mock.Of() + }; var frame = new Frame(application: null, context: connectionContext); frame.Reset(); @@ -564,7 +576,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { ServerAddress = ServerAddress.FromUrl("http://localhost:5000") }; - var connectionContext = new ConnectionContext(listenerContext); + var connectionContext = new ConnectionContext(listenerContext) + { + ConnectionControl = Mock.Of() + }; var frame = new Frame(application: null, context: connectionContext); frame.Reset(); @@ -633,7 +648,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { ServerAddress = ServerAddress.FromUrl("http://localhost:5000") }; - var connectionContext = new ConnectionContext(listenerContext); + var connectionContext = new ConnectionContext(listenerContext) + { + ConnectionControl = Mock.Of() + }; var frame = new Frame(application: null, context: connectionContext); frame.Reset(); @@ -931,7 +949,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Fact] - public void TakeStartLineDisablesKeepAliveTimeoutOnFirstByteAvailable() + public void TakeStartLineStartsRequestHeadersTimeoutOnFirstByteAvailable() { var trace = new KestrelTrace(new TestKestrelTrace()); var ltp = new LoggingThreadPool(trace); @@ -960,12 +978,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests socketInput.IncomingData(requestLineBytes, 0, requestLineBytes.Length); frame.TakeStartLine(socketInput); - connectionControl.Verify(cc => cc.CancelTimeout()); + var expectedRequestHeadersTimeout = (long)serviceContext.ServerOptions.Limits.RequestHeadersTimeout.TotalMilliseconds; + connectionControl.Verify(cc => cc.ResetTimeout(expectedRequestHeadersTimeout, TimeoutAction.SendTimeoutResponse)); } } [Fact] - public void TakeStartLineDoesNotDisableKeepAliveTimeoutIfNoDataAvailable() + public void TakeStartLineDoesNotStartRequestHeadersTimeoutIfNoDataAvailable() { var trace = new KestrelTrace(new TestKestrelTrace()); var ltp = new LoggingThreadPool(trace); @@ -991,7 +1010,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests frame.Reset(); frame.TakeStartLine(socketInput); - connectionControl.Verify(cc => cc.CancelTimeout(), Times.Never); + connectionControl.Verify(cc => cc.ResetTimeout(It.IsAny(), It.IsAny()), Times.Never); } } @@ -1132,7 +1151,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { ServerAddress = ServerAddress.FromUrl("http://localhost:5000") }; - var connectionContext = new ConnectionContext(listenerContext); + var connectionContext = new ConnectionContext(listenerContext) + { + ConnectionControl = Mock.Of() + }; var frame = new Frame(application: null, context: connectionContext); frame.Reset(); frame.InitializeHeaders(); @@ -1228,7 +1250,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests frame.Reset(); var requestProcessingTask = frame.RequestProcessingAsync(); - connectionControl.Verify(cc => cc.SetTimeout((long)serviceContext.ServerOptions.Limits.KeepAliveTimeout.TotalMilliseconds)); + + var expectedKeepAliveTimeout = (long)serviceContext.ServerOptions.Limits.KeepAliveTimeout.TotalMilliseconds; + connectionControl.Verify(cc => cc.SetTimeout(expectedKeepAliveTimeout, TimeoutAction.CloseConnection)); frame.Stop(); socketInput.IncomingFin(); diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs index 8481fc258d..0b9363b58f 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs @@ -167,5 +167,25 @@ namespace Microsoft.AspNetCore.Server.KestrelTests o.KeepAliveTimeout = TimeSpan.FromSeconds(seconds); Assert.Equal(seconds, o.KeepAliveTimeout.TotalSeconds); } + + [Fact] + public void RequestHeadersTimeoutDefault() + { + Assert.Equal(TimeSpan.FromSeconds(30), new KestrelServerLimits().RequestHeadersTimeout); + } + + [Theory] + [InlineData(0)] + [InlineData(0.5)] + [InlineData(1.0)] + [InlineData(2.5)] + [InlineData(10)] + [InlineData(60)] + public void RequestHeadersTimeoutValid(double seconds) + { + var o = new KestrelServerLimits(); + o.KeepAliveTimeout = TimeSpan.FromSeconds(seconds); + Assert.Equal(seconds, o.KeepAliveTimeout.TotalSeconds); + } } } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs index 47fb05c169..3de1e92e07 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs @@ -71,7 +71,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { } - public void SetTimeout(long milliseconds) + public void SetTimeout(long milliseconds, TimeoutAction timeoutAction) + { + } + + public void ResetTimeout(long milliseconds, TimeoutAction timeoutAction) { } diff --git a/test/shared/TestConnection.cs b/test/shared/TestConnection.cs index a382cac84d..cdb7e40ec5 100644 --- a/test/shared/TestConnection.cs +++ b/test/shared/TestConnection.cs @@ -139,6 +139,31 @@ namespace Microsoft.AspNetCore.Testing } } + public Task WaitForConnectionClose() + { + var tcs = new TaskCompletionSource(); + var eventArgs = new SocketAsyncEventArgs(); + eventArgs.SetBuffer(new byte[1], 0, 1); + eventArgs.Completed += ReceiveAsyncCompleted; + eventArgs.UserToken = tcs; + + if (!_socket.ReceiveAsync(eventArgs)) + { + ReceiveAsyncCompleted(this, eventArgs); + } + + return tcs.Task; + } + + private void ReceiveAsyncCompleted(object sender, SocketAsyncEventArgs e) + { + if (e.BytesTransferred == 0) + { + var tcs = (TaskCompletionSource)e.UserToken; + tcs.SetResult(null); + } + } + public static Socket CreateConnectedLoopbackSocket(int port) { var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);