Add request headers timeout (#1110).

This commit is contained in:
Cesar Blum Silveira 2016-09-16 09:46:57 -07:00
parent 7b2f7b94ab
commit 0312da7df3
15 changed files with 350 additions and 46 deletions

View File

@ -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;

View File

@ -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

View File

@ -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);
}

View File

@ -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)

View File

@ -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();
}
}

View File

@ -34,5 +34,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
HeadersExceedMaxTotalSize,
MissingCRInHeaderLine,
TooManyHeaders,
RequestTimeout,
}
}

View File

@ -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
}
}

View File

@ -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);
/// <summary>
/// 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;
}
}
/// <summary>
/// Gets or sets the maximum amount of time the server will spend receiving request headers.
/// </summary>
/// <remarks>
/// Defaults to 30 seconds.
/// </remarks>
public TimeSpan RequestHeadersTimeout
{
get
{
return _requestHeadersTimeout;
}
set
{
_requestHeadersTimeout = value;
}
}
}

View File

@ -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<IOException>(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<IOException>(async () =>
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
});
await connection.WaitForConnectionClose().TimeoutAfter(LongDelay);
}
}

View File

@ -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<IOException>(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",
"",
"");
}
}
}

View File

@ -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<string, StringValues, string> NullHeaderData
{
get

View File

@ -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<IConnectionControl>()
};
var frame = new Frame<object>(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<IConnectionControl>()
};
var frame = new Frame<object>(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<IConnectionControl>()
};
var frame = new Frame<object>(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<IConnectionControl>()
};
var frame = new Frame<object>(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<IConnectionControl>()
};
var frame = new Frame<object>(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<IConnectionControl>()
};
var frame = new Frame<object>(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<long>(), It.IsAny<TimeoutAction>()), 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<IConnectionControl>()
};
var frame = new Frame<object>(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();

View File

@ -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);
}
}
}

View File

@ -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)
{
}

View File

@ -139,6 +139,31 @@ namespace Microsoft.AspNetCore.Testing
}
}
public Task WaitForConnectionClose()
{
var tcs = new TaskCompletionSource<object>();
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<object>)e.UserToken;
tcs.SetResult(null);
}
}
public static Socket CreateConnectedLoopbackSocket(int port)
{
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);