Add MaxRequestBodySize limit (#478).
This commit is contained in:
parent
f918279873
commit
e53a87be9c
|
|
@ -78,6 +78,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
case RequestRejectionReason.RequestTimeout:
|
||||
ex = new BadHttpRequestException("Request timed out.", 408);
|
||||
break;
|
||||
case RequestRejectionReason.PayloadTooLarge:
|
||||
ex = new BadHttpRequestException("Payload too large.", 413);
|
||||
break;
|
||||
default:
|
||||
ex = new BadHttpRequestException("Bad request.", 400);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
private DateHeaderValueManager DateHeaderValueManager => ConnectionContext.ListenerContext.ServiceContext.DateHeaderValueManager;
|
||||
private ServerAddress ServerAddress => ConnectionContext.ListenerContext.ServerAddress;
|
||||
// Hold direct reference to ServerOptions since this is used very often in the request processing path
|
||||
private KestrelServerOptions ServerOptions { get; }
|
||||
public KestrelServerOptions ServerOptions { get; }
|
||||
private IPEndPoint LocalEndPoint => ConnectionContext.LocalEndPoint;
|
||||
private IPEndPoint RemoteEndPoint => ConnectionContext.RemoteEndPoint;
|
||||
protected string ConnectionId => ConnectionContext.ConnectionId;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||
{
|
||||
|
|
@ -272,6 +271,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
try
|
||||
{
|
||||
var contentLength = FrameHeaders.ParseContentLength(unparsedContentLength);
|
||||
|
||||
if (contentLength > context.ServerOptions.Limits.MaxRequestBodySize)
|
||||
{
|
||||
context.RejectRequest(RequestRejectionReason.PayloadTooLarge);
|
||||
}
|
||||
|
||||
return new ForContentLength(keepAlive, contentLength, context);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
|
|
@ -281,7 +286,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
}
|
||||
|
||||
// Avoid slowing down most common case
|
||||
if (!object.ReferenceEquals(context.Method, HttpMethods.Get))
|
||||
if (!ReferenceEquals(context.Method, HttpMethods.Get))
|
||||
{
|
||||
// If we got here, request contains no Content-Length or Transfer-Encoding header.
|
||||
// Reject with 411 Length Required.
|
||||
|
|
@ -394,6 +399,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
|
||||
private readonly SocketInput _input;
|
||||
private readonly FrameRequestHeaders _requestHeaders;
|
||||
private long _inputBytesRead;
|
||||
private int _inputLength;
|
||||
|
||||
private Mode _mode = Mode.Prefix;
|
||||
|
|
@ -414,6 +420,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
protected override void OnConsumedBytes(int count)
|
||||
{
|
||||
_inputLength -= count;
|
||||
_inputBytesRead += count;
|
||||
|
||||
if (_inputBytesRead > _context.ServerOptions.Limits.MaxRequestBodySize)
|
||||
{
|
||||
_context.RejectRequest(RequestRejectionReason.PayloadTooLarge);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ArraySegment<byte>> PeekStateMachineAsync()
|
||||
|
|
|
|||
|
|
@ -30,5 +30,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
FinalTransferCodingNotChunked,
|
||||
LengthRequired,
|
||||
LengthRequiredHttp10,
|
||||
PayloadTooLarge
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
// Matches the default LimitRequestFields in Apache httpd.
|
||||
private int _maxRequestHeaderCount = 100;
|
||||
|
||||
// Matches default HttpRuntimeSection.MaxRequestLength.
|
||||
// https://msdn.microsoft.com/en-us/library/system.web.configuration.httpruntimesection.maxrequestlength%28v=vs.100%29.aspx
|
||||
private long _maxRequestBodySize = 4 * 1024 * 1024;
|
||||
|
||||
// Matches the default http.sys connectionTimeout.
|
||||
private TimeSpan _keepAliveTimeout = TimeSpan.FromMinutes(2);
|
||||
|
||||
|
|
@ -144,6 +148,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum request body size, in bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Defaults to 4MB.
|
||||
/// </remarks>
|
||||
public long MaxRequestBodySize
|
||||
{
|
||||
get
|
||||
{
|
||||
return _maxRequestBodySize;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value), "Value must a positive integer.");
|
||||
}
|
||||
_maxRequestBodySize = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the keep-alive timeout.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -178,7 +178,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
AddServerHeader = false,
|
||||
Limits =
|
||||
{
|
||||
KeepAliveTimeout = KeepAliveTimeout
|
||||
KeepAliveTimeout = KeepAliveTimeout,
|
||||
// Prevent request rejection if ConnectionNotTimedOutWhileRequestBeingSent
|
||||
// sends more bytes than the default body size limit.
|
||||
MaxRequestBodySize = long.MaxValue
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
// 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.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||
{
|
||||
public class MaxRequestBodySizeTests
|
||||
{
|
||||
private const int MaxRequestBodySize = 128;
|
||||
|
||||
[Theory]
|
||||
[InlineData(MaxRequestBodySize - 1, 0)]
|
||||
[InlineData(MaxRequestBodySize, 0)]
|
||||
[InlineData(MaxRequestBodySize - 1, 1)]
|
||||
[InlineData(MaxRequestBodySize, 1)]
|
||||
[InlineData(MaxRequestBodySize - 1, 2)]
|
||||
[InlineData(MaxRequestBodySize, 2)]
|
||||
public async Task ServerAcceptsRequestBodyWithinLimit(int requestBodySize, int chunks)
|
||||
{
|
||||
using (var server = CreateServer(MaxRequestBodySize, async httpContext => await httpContext.Response.WriteAsync("hello, world")))
|
||||
{
|
||||
using (var connection = new TestConnection(server.Port))
|
||||
{
|
||||
await connection.SendAll(BuildRequest(connection, requestBodySize, chunks));
|
||||
|
||||
await connection.Receive(
|
||||
"HTTP/1.1 200 OK",
|
||||
$"Date: {server.Context.DateHeaderValue}",
|
||||
"Transfer-Encoding: chunked",
|
||||
"",
|
||||
"c",
|
||||
"hello, world",
|
||||
"0",
|
||||
"",
|
||||
"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(MaxRequestBodySize + 1, 0)]
|
||||
[InlineData(MaxRequestBodySize + 1, 1)]
|
||||
[InlineData(MaxRequestBodySize + 1, 2)]
|
||||
public async Task ServerRejectsRequestBodyExceedingLimit(int requestBodySize, int chunks)
|
||||
{
|
||||
using (var server = CreateServer(MaxRequestBodySize, async httpContext =>
|
||||
{
|
||||
var received = 0;
|
||||
while (received < requestBodySize)
|
||||
{
|
||||
received += await httpContext.Request.Body.ReadAsync(new byte[1024], 0, 1024);
|
||||
}
|
||||
|
||||
// Should never get here
|
||||
await httpContext.Response.WriteAsync("hello, world");
|
||||
}))
|
||||
{
|
||||
using (var connection = new TestConnection(server.Port))
|
||||
{
|
||||
await connection.SendAll(BuildRequest(connection, requestBodySize, chunks));
|
||||
|
||||
await connection.ReceiveForcedEnd(
|
||||
"HTTP/1.1 413 Payload Too Large",
|
||||
"Connection: close",
|
||||
$"Date: {server.Context.DateHeaderValue}",
|
||||
"Content-Length: 0",
|
||||
"",
|
||||
"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MaxRequestBodySizeNotEnforcedOnUpgradedConnection()
|
||||
{
|
||||
var sendBytes = MaxRequestBodySize + 1;
|
||||
|
||||
using (var server = CreateServer(MaxRequestBodySize, async httpContext =>
|
||||
{
|
||||
var stream = await httpContext.Features.Get<IHttpUpgradeFeature>().UpgradeAsync();
|
||||
|
||||
var received = 0;
|
||||
while (received < sendBytes)
|
||||
{
|
||||
received += await stream.ReadAsync(new byte[1024], 0, 1024);
|
||||
}
|
||||
|
||||
var response = Encoding.ASCII.GetBytes($"{received}");
|
||||
await stream.WriteAsync(response, 0, response.Length);
|
||||
}))
|
||||
{
|
||||
using (var connection = new TestConnection(server.Port))
|
||||
{
|
||||
await connection.Send(
|
||||
"GET / HTTP/1.1",
|
||||
"Connection: upgrade",
|
||||
"",
|
||||
new string('a', sendBytes));
|
||||
|
||||
await connection.Receive(
|
||||
"HTTP/1.1 101 Switching Protocols",
|
||||
"Connection: Upgrade",
|
||||
$"Date: {server.Context.DateHeaderValue}",
|
||||
"",
|
||||
$"{sendBytes}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildRequest(TestConnection connection, int requestBodySize, int chunks)
|
||||
{
|
||||
var request = new StringBuilder();
|
||||
|
||||
request.Append("POST / HTTP/1.1\r\n");
|
||||
|
||||
if (chunks == 0)
|
||||
{
|
||||
request.Append($"Content-Length: {requestBodySize}\r\n\r\n");
|
||||
request.Append(new string('a', requestBodySize));
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Append("Transfer-Encoding: chunked\r\n\r\n");
|
||||
|
||||
var bytesSent = 0;
|
||||
while (bytesSent < requestBodySize)
|
||||
{
|
||||
var chunkSize = Math.Min(requestBodySize / chunks, requestBodySize - bytesSent);
|
||||
|
||||
request.Append($"{chunkSize:X}\r\n");
|
||||
request.Append(new string('a', chunkSize));
|
||||
request.Append("\r\n");
|
||||
|
||||
bytesSent += chunkSize;
|
||||
}
|
||||
|
||||
// Make sure we sent the right amount of data
|
||||
Assert.Equal(requestBodySize, bytesSent);
|
||||
|
||||
request.Append("0\r\n\r\n");
|
||||
}
|
||||
|
||||
return request.ToString();
|
||||
}
|
||||
|
||||
private TestServer CreateServer(int maxRequestBodySize, RequestDelegate app)
|
||||
{
|
||||
return new TestServer(app, new TestServiceContext
|
||||
{
|
||||
ServerOptions = new KestrelServerOptions
|
||||
{
|
||||
AddServerHeader = false,
|
||||
Limits =
|
||||
{
|
||||
MaxRequestBodySize = maxRequestBodySize
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -169,6 +169,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
.UseKestrel(options =>
|
||||
{
|
||||
options.Limits.MaxRequestBufferSize = maxRequestBufferSize;
|
||||
options.Limits.MaxRequestBodySize = _dataLength;
|
||||
options.UseHttps(@"TestResources/testCert.pfx", "testPassword");
|
||||
|
||||
if (maxRequestBufferSize.HasValue &&
|
||||
|
|
|
|||
|
|
@ -43,7 +43,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
Assert.True(bufferLength % 256 == 0, $"{nameof(bufferLength)} must be evenly divisible by 256");
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
.UseKestrel(options =>
|
||||
{
|
||||
options.Limits.MaxRequestBodySize = contentLength;
|
||||
})
|
||||
.UseUrls("http://127.0.0.1:0/")
|
||||
.Configure(app =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -149,6 +149,34 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
Assert.Equal(value, o.MaxRequestHeaderCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxRequestBodySizeDefault()
|
||||
{
|
||||
Assert.Equal(4 * 1024 * 1024, (new KestrelServerLimits()).MaxRequestBodySize);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(long.MinValue)]
|
||||
[InlineData(-1)]
|
||||
[InlineData(0)]
|
||||
public void MaxRequestBodySizeInvalid(long value)
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
{
|
||||
(new KestrelServerLimits()).MaxRequestBodySize = value;
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(long.MaxValue)]
|
||||
public void MaxRequestBodySizeValid(long value)
|
||||
{
|
||||
var o = new KestrelServerLimits();
|
||||
o.MaxRequestBodySize = value;
|
||||
Assert.Equal(value, o.MaxRequestBodySize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeepAliveTimeoutDefault()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue