From 8836eec7d886fcbcb918c43fa20b66bcc8199874 Mon Sep 17 00:00:00 2001 From: Cesar Blum Silveira Date: Tue, 26 Jul 2016 20:42:49 -0700 Subject: [PATCH] Limit request line length (#784). --- .../BadHttpRequestException.cs | 14 +- .../Internal/Http/Connection.cs | 4 +- .../Internal/Http/Frame.cs | 47 ++- .../Internal/Http/RequestRejectionReason.cs | 6 +- .../Infrastructure/MemoryPoolIterator.cs | 225 ++++++++++- .../KestrelServer.cs | 12 + .../KestrelServerLimits.cs | 62 +++ .../KestrelServerOptions.cs | 31 +- .../MaxRequestBufferSizeTests.cs | 12 +- .../MaxRequestLineSizeTests.cs | 99 +++++ .../BadHttpRequestTests.cs | 92 ++--- .../KestrelServerLimitsTests.cs | 67 +++ .../KestrelServerOptionsTests.cs | 43 +- .../MemoryPoolIteratorTests.cs | 382 +++++++++++++++++- test/shared/TestConnection.cs | 2 +- 15 files changed, 968 insertions(+), 130 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs create mode 100644 test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestLineSizeTests.cs create mode 100644 test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs index 95ecedb930..9a435392ff 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs @@ -79,6 +79,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel case RequestRejectionReason.NonAsciiOrNullCharactersInInputString: ex = new BadHttpRequestException("The input string contains non-ASCII or null characters."); break; + case RequestRejectionReason.RequestLineTooLong: + ex = new BadHttpRequestException("Request line too long."); + break; + case RequestRejectionReason.MissingSpaceAfterMethod: + ex = new BadHttpRequestException("No space character found after method in request line."); + break; + case RequestRejectionReason.MissingSpaceAfterTarget: + ex = new BadHttpRequestException("No space character found after target in request line."); + break; + case RequestRejectionReason.MissingCrAfterVersion: + ex = new BadHttpRequestException("Missing CR in request line."); + break; default: ex = new BadHttpRequestException("Bad request."); break; @@ -92,7 +104,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel switch (reason) { case RequestRejectionReason.MalformedRequestLineStatus: - ex = new BadHttpRequestException($"Malformed request: {value}"); + ex = new BadHttpRequestException($"Invalid request line: {value}"); break; case RequestRejectionReason.InvalidContentLength: ex = new BadHttpRequestException($"Invalid content length: {value}"); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs index 0c73c35c7c..09a8c73d15 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs @@ -47,9 +47,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http ConnectionId = GenerateConnectionId(Interlocked.Increment(ref _lastConnectionId)); - if (ServerOptions.MaxRequestBufferSize.HasValue) + if (ServerOptions.Limits.MaxRequestBufferSize.HasValue) { - _bufferSizeControl = new BufferSizeControl(ServerOptions.MaxRequestBufferSize.Value, this, Thread); + _bufferSizeControl = new BufferSizeControl(ServerOptions.Limits.MaxRequestBufferSize.Value, this, Thread); } SocketInput = new SocketInput(Thread.Memory, ThreadPool, _bufferSizeControl); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index 9d700f0e82..ad46367135 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: Kestrel"); private static Vector _vectorCRs = new Vector((byte)'\r'); + private static Vector _vectorLFs = new Vector((byte)'\n'); private static Vector _vectorColons = new Vector((byte)':'); private static Vector _vectorSpaces = new Vector((byte)' '); private static Vector _vectorTabs = new Vector((byte)'\t'); @@ -801,13 +802,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http _requestProcessingStatus = RequestProcessingStatus.RequestStarted; + var end = scan; + int bytesScanned; + if (end.Seek(ref _vectorLFs, out bytesScanned, ServerOptions.Limits.MaxRequestLineSize) == -1) + { + if (bytesScanned >= ServerOptions.Limits.MaxRequestLineSize) + { + RejectRequest(RequestRejectionReason.RequestLineTooLong); + } + else + { + return RequestLineStatus.Incomplete; + } + } + string method; var begin = scan; if (!begin.GetKnownMethod(out method)) { - if (scan.Seek(ref _vectorSpaces) == -1) + if (scan.Seek(ref _vectorSpaces, ref end) == -1) { - return RequestLineStatus.MethodIncomplete; + RejectRequest(RequestRejectionReason.MissingSpaceAfterMethod); } method = begin.GetAsciiString(scan); @@ -835,18 +850,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http scan.Take(); begin = scan; var needDecode = false; - var chFound = scan.Seek(ref _vectorSpaces, ref _vectorQuestionMarks, ref _vectorPercentages); + var chFound = scan.Seek(ref _vectorSpaces, ref _vectorQuestionMarks, ref _vectorPercentages, ref end); if (chFound == -1) { - return RequestLineStatus.TargetIncomplete; + RejectRequest(RequestRejectionReason.MissingSpaceAfterTarget); } else if (chFound == '%') { needDecode = true; - chFound = scan.Seek(ref _vectorSpaces, ref _vectorQuestionMarks); + chFound = scan.Seek(ref _vectorSpaces, ref _vectorQuestionMarks, ref end); if (chFound == -1) { - return RequestLineStatus.TargetIncomplete; + RejectRequest(RequestRejectionReason.MissingSpaceAfterTarget); } } @@ -857,9 +872,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http if (chFound == '?') { begin = scan; - if (scan.Seek(ref _vectorSpaces) == -1) + if (scan.Seek(ref _vectorSpaces, ref end) == -1) { - return RequestLineStatus.TargetIncomplete; + RejectRequest(RequestRejectionReason.MissingSpaceAfterTarget); } queryString = begin.GetAsciiString(scan); } @@ -873,9 +888,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http scan.Take(); begin = scan; - if (scan.Seek(ref _vectorCRs) == -1) + if (scan.Seek(ref _vectorCRs, ref end) == -1) { - return RequestLineStatus.VersionIncomplete; + RejectRequest(RequestRejectionReason.MissingCrAfterVersion); } string httpVersion; @@ -898,16 +913,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http } } - scan.Take(); - var next = scan.Take(); - if (next == -1) - { - return RequestLineStatus.Incomplete; - } - else if (next != '\n') + scan.Take(); // consume CR + if (scan.Block != end.Block || scan.Index != end.Index) { RejectRequest(RequestRejectionReason.MissingLFInRequestLine); } + scan.Take(); // consume LF // URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11 // Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8; @@ -1155,7 +1166,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http // Trim trailing whitespace from header value by repeatedly advancing to next // whitespace or CR. - // + // // - If CR is found, this is the end of the header value. // - If whitespace is found, this is the _tentative_ end of the header value. // If non-whitespace is found after it and it's not CR, seek again to the next diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs index 83ea024a5d..d71ff5b414 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs @@ -26,6 +26,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http ChunkedRequestIncomplete, PathContainsNullCharacters, InvalidCharactersInHeaderName, - NonAsciiOrNullCharactersInInputString + NonAsciiOrNullCharactersInInputString, + RequestLineTooLong, + MissingSpaceAfterMethod, + MissingSpaceAfterTarget, + MissingCrAfterVersion, } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/MemoryPoolIterator.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/MemoryPoolIterator.cs index 68397406bb..78efd5adca 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/MemoryPoolIterator.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/MemoryPoolIterator.cs @@ -216,6 +216,123 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure } public unsafe int Seek(ref Vector byte0Vector) + { + int bytesScanned; + return Seek(ref byte0Vector, out bytesScanned); + } + + public unsafe int Seek( + ref Vector byte0Vector, + out int bytesScanned, + int limit = int.MaxValue) + { + bytesScanned = 0; + + if (IsDefault || limit <= 0) + { + return -1; + } + + var block = _block; + var index = _index; + var wasLastBlock = block.Next == null; + var following = block.End - index; + byte[] array; + var byte0 = byte0Vector[0]; + + while (true) + { + while (following == 0) + { + if (bytesScanned >= limit || wasLastBlock) + { + _block = block; + _index = index; + return -1; + } + + block = block.Next; + index = block.Start; + wasLastBlock = block.Next == null; + following = block.End - index; + } + array = block.Array; + while (following > 0) + { + // Need unit tests to test Vector path +#if !DEBUG + // Check will be Jitted away https://github.com/dotnet/coreclr/issues/1079 + if (Vector.IsHardwareAccelerated) + { +#endif + if (following >= _vectorSpan) + { + var byte0Equals = Vector.Equals(new Vector(array, index), byte0Vector); + + if (byte0Equals.Equals(Vector.Zero)) + { + if (bytesScanned + _vectorSpan >= limit) + { + _block = block; + // Ensure iterator is left at limit position + _index = index + (limit - bytesScanned); + bytesScanned = limit; + return -1; + } + + bytesScanned += _vectorSpan; + following -= _vectorSpan; + index += _vectorSpan; + continue; + } + + _block = block; + + var firstEqualByteIndex = FindFirstEqualByte(ref byte0Equals); + var vectorBytesScanned = firstEqualByteIndex + 1; + + if (bytesScanned + vectorBytesScanned > limit) + { + // Ensure iterator is left at limit position + _index = index + (limit - bytesScanned); + bytesScanned = limit; + return -1; + } + + _index = index + firstEqualByteIndex; + bytesScanned += vectorBytesScanned; + + return byte0; + } + // Need unit tests to test Vector path +#if !DEBUG + } +#endif + + var pCurrent = (block.DataFixedPtr + index); + var pEnd = pCurrent + Math.Min(following, limit - bytesScanned); + do + { + bytesScanned++; + if (*pCurrent == byte0) + { + _block = block; + _index = index; + return byte0; + } + pCurrent++; + index++; + } while (pCurrent < pEnd); + + following = 0; + break; + } + } + } + + public unsafe int Seek( + ref Vector byte0Vector, + ref MemoryPoolIterator limit) { if (IsDefault) { @@ -233,12 +350,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure { while (following == 0) { - if (wasLastBlock) + if ((block == limit.Block && index > limit.Index) || + wasLastBlock) { _block = block; - _index = index; + // Ensure iterator is left at limit position + _index = limit.Index; return -1; } + block = block.Next; index = block.Start; wasLastBlock = block.Next == null; @@ -259,13 +379,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure if (byte0Equals.Equals(Vector.Zero)) { + if (block == limit.Block && index + _vectorSpan > limit.Index) + { + _block = block; + // Ensure iterator is left at limit position + _index = limit.Index; + return -1; + } + following -= _vectorSpan; index += _vectorSpan; continue; } _block = block; - _index = index + FindFirstEqualByte(ref byte0Equals); + + var firstEqualByteIndex = FindFirstEqualByte(ref byte0Equals); + var vectorBytesScanned = firstEqualByteIndex + 1; + + if (_block == limit.Block && index + firstEqualByteIndex > limit.Index) + { + // Ensure iterator is left at limit position + _index = limit.Index; + return -1; + } + + _index = index + firstEqualByteIndex; + return byte0; } // Need unit tests to test Vector path @@ -274,7 +414,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure #endif var pCurrent = (block.DataFixedPtr + index); - var pEnd = pCurrent + following; + var pEnd = block == limit.Block ? block.DataFixedPtr + limit.Index + 1 : pCurrent + following; do { if (*pCurrent == byte0) @@ -294,6 +434,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure } public unsafe int Seek(ref Vector byte0Vector, ref Vector byte1Vector) + { + var limit = new MemoryPoolIterator(); + return Seek(ref byte0Vector, ref byte1Vector, ref limit); + } + + public unsafe int Seek( + ref Vector byte0Vector, + ref Vector byte1Vector, + ref MemoryPoolIterator limit) { if (IsDefault) { @@ -314,10 +463,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure { while (following == 0) { - if (wasLastBlock) + if ((block == limit.Block && index > limit.Index) || + wasLastBlock) { _block = block; - _index = index; + // Ensure iterator is left at limit position + _index = limit.Index; return -1; } block = block.Next; @@ -354,6 +505,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure { following -= _vectorSpan; index += _vectorSpan; + + if (block == limit.Block && index > limit.Index) + { + _block = block; + // Ensure iterator is left at limit position + _index = limit.Index; + return -1; + } + continue; } @@ -362,10 +522,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure if (byte0Index < byte1Index) { _index = index + byte0Index; + + if (block == limit.Block && _index > limit.Index) + { + // Ensure iterator is left at limit position + _index = limit.Index; + return -1; + } + return byte0; } _index = index + byte1Index; + + if (block == limit.Block && _index > limit.Index) + { + // Ensure iterator is left at limit position + _index = limit.Index; + return -1; + } + return byte1; } // Need unit tests to test Vector path @@ -373,7 +549,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure } #endif var pCurrent = (block.DataFixedPtr + index); - var pEnd = pCurrent + following; + var pEnd = block == limit.Block ? block.DataFixedPtr + limit.Index + 1 : pCurrent + following; do { if (*pCurrent == byte0) @@ -399,6 +575,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure } public unsafe int Seek(ref Vector byte0Vector, ref Vector byte1Vector, ref Vector byte2Vector) + { + var limit = new MemoryPoolIterator(); + return Seek(ref byte0Vector, ref byte1Vector, ref byte2Vector, ref limit); + } + + public unsafe int Seek( + ref Vector byte0Vector, + ref Vector byte1Vector, + ref Vector byte2Vector, + ref MemoryPoolIterator limit) { if (IsDefault) { @@ -421,10 +607,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure { while (following == 0) { - if (wasLastBlock) + if ((block == limit.Block && index > limit.Index) || + wasLastBlock) { _block = block; - _index = index; + // Ensure iterator is left at limit position + _index = limit.Index; return -1; } block = block.Next; @@ -465,6 +653,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure { following -= _vectorSpan; index += _vectorSpan; + + if (block == limit.Block && index > limit.Index) + { + _block = block; + // Ensure iterator is left at limit position + _index = limit.Index; + return -1; + } + continue; } @@ -499,6 +696,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure } _index = index + toMove; + + if (block == limit.Block && _index > limit.Index) + { + // Ensure iterator is left at limit position + _index = limit.Index; + return -1; + } + return toReturn; } // Need unit tests to test Vector path @@ -506,7 +711,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure } #endif var pCurrent = (block.DataFixedPtr + index); - var pEnd = pCurrent + following; + var pEnd = block == limit.Block ? block.DataFixedPtr + limit.Index + 1 : pCurrent + following; do { if (*pCurrent == byte0) diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs index 034d41e29e..09ddb1a6b5 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServer.cs @@ -56,6 +56,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel public void Start(IHttpApplication application) { + ValidateOptions(); + if (_disposables != null) { // The server has already started and/or has not been cleaned up yet @@ -196,5 +198,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel _disposables = null; } } + + private void ValidateOptions() + { + if (Options.Limits.MaxRequestBufferSize.HasValue && + Options.Limits.MaxRequestBufferSize < Options.Limits.MaxRequestLineSize) + { + throw new InvalidOperationException( + $"Maximum request buffer size ({Options.Limits.MaxRequestBufferSize.Value}) must be greater than or equal to maximum request line size ({Options.Limits.MaxRequestLineSize})."); + } + } } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs new file mode 100644 index 0000000000..02afceca2b --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs @@ -0,0 +1,62 @@ +// 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 +{ + public class KestrelServerLimits + { + // Matches the default client_max_body_size in nginx. Also large enough that most requests + // should be under the limit. + private long? _maxRequestBufferSize = 1024 * 1024; + + // Matches the default large_client_header_buffers in nginx. + private int _maxRequestLineSize = 8 * 1024; + + /// + /// Gets or sets the maximum size of the request buffer. + /// + /// + /// When set to null, the size of the request buffer is unlimited. + /// Defaults to 1,048,576 bytes (1 MB). + /// + public long? MaxRequestBufferSize + { + get + { + return _maxRequestBufferSize; + } + set + { + if (value.HasValue && value.Value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "Value must be null or a positive integer."); + } + _maxRequestBufferSize = value; + } + } + + /// + /// Gets or sets the maximum allowed size for the HTTP request line. + /// + /// + /// Defaults to 8,192 bytes (8 KB). + /// + public int MaxRequestLineSize + { + get + { + return _maxRequestLineSize; + } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "Value must be a positive integer."); + } + _maxRequestLineSize = value; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerOptions.cs b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerOptions.cs index 6ae42f48a8..9e4331cbbd 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerOptions.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerOptions.cs @@ -1,6 +1,3 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - using System; using Microsoft.AspNetCore.Server.Kestrel.Filter; @@ -11,10 +8,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel /// public class KestrelServerOptions { - // Matches the default client_max_body_size in nginx. Also large enough that most requests - // should be under the limit. - private long? _maxRequestBufferSize = 1024 * 1024; - /// /// Gets or sets whether the Server header should be included in each response. /// @@ -41,28 +34,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel public IConnectionFilter ConnectionFilter { get; set; } /// - /// Maximum size of the request buffer. - /// If value is null, the size of the request buffer is unlimited. + /// + /// This property is obsolete and will be removed in a future version. + /// Use Limits.MaxRequestBufferSize instead. + /// + /// + /// Gets or sets the maximum size of the request buffer. + /// /// /// + /// When set to null, the size of the request buffer is unlimited. /// Defaults to 1,048,576 bytes (1 MB). /// + [Obsolete] public long? MaxRequestBufferSize { get { - return _maxRequestBufferSize; + return Limits.MaxRequestBufferSize; } set { - if (value.HasValue && value.Value <= 0) - { - throw new ArgumentOutOfRangeException("value", "Value must be null or a positive integer."); - } - _maxRequestBufferSize = value; + Limits.MaxRequestBufferSize = value; } } + /// + /// Provides access to request limit options. + /// + public KestrelServerLimits Limits { get; } = new KestrelServerLimits(); + /// /// Set to false to enable Nagle's algorithm for all connections. /// diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs index 70bbf876f8..463a11145c 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs @@ -25,8 +25,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests get { var maxRequestBufferSizeValues = new Tuple[] { - // Smallest allowed buffer. Server should call pause/resume between each read. - Tuple.Create((long?)1, true), + // Smallest buffer that can hold a POST request line to the root. + Tuple.Create((long?)"POST / HTTP/1.1\r\n".Length, true), // Small buffer, but large enough to hold all request headers. Tuple.Create((long?)16 * 1024, true), @@ -171,8 +171,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var host = new WebHostBuilder() .UseKestrel(options => { - options.MaxRequestBufferSize = maxRequestBufferSize; + options.Limits.MaxRequestBufferSize = maxRequestBufferSize; options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); + + if (maxRequestBufferSize.HasValue && + maxRequestBufferSize.Value < options.Limits.MaxRequestLineSize) + { + options.Limits.MaxRequestLineSize = (int)maxRequestBufferSize; + } }) .UseUrls("http://127.0.0.1:0/", "https://127.0.0.1:0/") .UseContentRoot(Directory.GetCurrentDirectory()) diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestLineSizeTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestLineSizeTests.cs new file mode 100644 index 0000000000..644fef0687 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestLineSizeTests.cs @@ -0,0 +1,99 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class MaxRequestLineSizeTests + { + [Theory] + [InlineData("GET / HTTP/1.1\r\n", 16)] + [InlineData("GET / HTTP/1.1\r\n", 17)] + [InlineData("GET / HTTP/1.1\r\n", 137)] + [InlineData("POST /abc/de HTTP/1.1\r\n", 23)] + [InlineData("POST /abc/de HTTP/1.1\r\n", 24)] + [InlineData("POST /abc/de HTTP/1.1\r\n", 287)] + [InlineData("PUT /abc/de?f=ghi HTTP/1.1\r\n", 28)] + [InlineData("PUT /abc/de?f=ghi HTTP/1.1\r\n", 29)] + [InlineData("PUT /abc/de?f=ghi HTTP/1.1\r\n", 589)] + [InlineData("DELETE /a%20b%20c/d%20e?f=ghi HTTP/1.1\r\n", 40)] + [InlineData("DELETE /a%20b%20c/d%20e?f=ghi HTTP/1.1\r\n", 41)] + [InlineData("DELETE /a%20b%20c/d%20e?f=ghi HTTP/1.1\r\n", 1027)] + public async Task ServerAcceptsRequestLineWithinLimit(string requestLine, int limit) + { + var maxRequestLineSize = limit; + + using (var host = BuildWebHost(options => + { + options.Limits.MaxRequestLineSize = maxRequestLineSize; + })) + { + host.Start(); + + using (var connection = new TestConnection(host.GetPort())) + { + await connection.SendEnd($"{requestLine}\r\n"); + await connection.Receive($"HTTP/1.1 200 OK\r\n"); + } + } + } + + [Theory] + [InlineData("GET / HTTP/1.1\r\n")] + [InlineData("POST /abc/de HTTP/1.1\r\n")] + [InlineData("PUT /abc/de?f=ghi HTTP/1.1\r\n")] + [InlineData("DELETE /a%20b%20c/d%20e?f=ghi HTTP/1.1\r\n")] + public async Task ServerRejectsRequestLineExceedingLimit(string requestLine) + { + using (var host = BuildWebHost(options => + { + options.Limits.MaxRequestLineSize = requestLine.Length - 1; // stop short of the '\n' + })) + { + host.Start(); + + using (var connection = new TestConnection(host.GetPort())) + { + await connection.SendAllTryEnd($"{requestLine}\r\n"); + await connection.Receive($"HTTP/1.1 400 Bad Request\r\n"); + } + } + } + + [Theory] + [InlineData(1, 2)] + [InlineData(int.MaxValue - 1, int.MaxValue)] + public void ServerFailsToStartWhenMaxRequestBufferSizeIsLessThanMaxRequestLineSize(long maxRequestBufferSize, int maxRequestLineSize) + { + using (var host = BuildWebHost(options => + { + options.Limits.MaxRequestBufferSize = maxRequestBufferSize; + options.Limits.MaxRequestLineSize = maxRequestLineSize; + })) + { + Assert.Throws(() => host.Start()); + } + } + + private IWebHost BuildWebHost(Action options) + { + var host = new WebHostBuilder() + .UseKestrel(options) + .UseUrls("http://127.0.0.1:0/") + .Configure(app => app.Run(async context => + { + await context.Response.WriteAsync("hello, world"); + })) + .Build(); + + return host; + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/BadHttpRequestTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/BadHttpRequestTests.cs index 78f8e8d50f..ee41be7d46 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/BadHttpRequestTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/BadHttpRequestTests.cs @@ -10,64 +10,47 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { public class BadHttpRequestTests { - // Don't send more data than necessary to fail, otherwise the test throws trying to - // send data after the server has already closed the connection. This would cause the - // test to fail on Windows, due to a winsock limitation: after the error when trying - // to write to the socket closed by the server, winsock disposes all resources used - // by that socket. The test then fails when we try to read the expected response - // from the server because, although it would have been buffered, it got discarded - // by winsock on the send() error. - // The solution for this is for the client to always try to receive before doing - // any sends, that way it can detect that the connection has been closed by the server - // and not try to send() on the closed connection, triggering the error that would cause - // any buffered received data to be lost. - // We do not deem necessary to mitigate this issue in TestConnection, since it would only - // be ensuring that we have a properly implemented HTTP client that can handle the - // winsock issue. There is nothing to be verified in Kestrel in this situation. + // All test cases for this theory must end in '\n', otherwise the server will spin forever [Theory] // Incomplete request lines - [InlineData("G")] - [InlineData("GE")] - [InlineData("GET")] - [InlineData("GET ")] - [InlineData("GET /")] - [InlineData("GET / ")] - [InlineData("GET / H")] - [InlineData("GET / HT")] - [InlineData("GET / HTT")] - [InlineData("GET / HTTP")] - [InlineData("GET / HTTP/")] - [InlineData("GET / HTTP/1")] - [InlineData("GET / HTTP/1.")] - [InlineData("GET / HTTP/1.1")] - [InlineData("GET / HTTP/1.1\r")] - [InlineData("GET / HTTP/1.0")] - [InlineData("GET / HTTP/1.0\r")] + [InlineData("G\r\n")] + [InlineData("GE\r\n")] + [InlineData("GET\r\n")] + [InlineData("GET \r\n")] + [InlineData("GET /\r\n")] + [InlineData("GET / \r\n")] + [InlineData("GET / H\r\n")] + [InlineData("GET / HT\r\n")] + [InlineData("GET / HTT\r\n")] + [InlineData("GET / HTTP\r\n")] + [InlineData("GET / HTTP/\r\n")] + [InlineData("GET / HTTP/1\r\n")] + [InlineData("GET / HTTP/1.\r\n")] // Missing method - [InlineData(" ")] + [InlineData(" \r\n")] // Missing second space - [InlineData("/ ")] // This fails trying to read the '/' because that's invalid for an HTTP method + [InlineData("/ \r\n")] // This fails trying to read the '/' because that's invalid for an HTTP method [InlineData("GET /\r\n")] // Missing target - [InlineData("GET ")] + [InlineData("GET \r\n")] // Missing version - [InlineData("GET / \r")] + [InlineData("GET / \r\n")] // Missing CR [InlineData("GET / \n")] // Unrecognized HTTP version - [InlineData("GET / http/1.0\r")] - [InlineData("GET / http/1.1\r")] - [InlineData("GET / HTTP/1.1 \r")] - [InlineData("GET / HTTP/1.1a\r")] - [InlineData("GET / HTTP/1.0\n\r")] - [InlineData("GET / HTTP/1.2\r")] - [InlineData("GET / HTTP/3.0\r")] - [InlineData("GET / H\r")] - [InlineData("GET / HTTP/1.\r")] - [InlineData("GET / hello\r")] - [InlineData("GET / 8charact\r")] - // Missing LF - [InlineData("GET / HTTP/1.0\rA")] + [InlineData("GET / http/1.0\r\n")] + [InlineData("GET / http/1.1\r\n")] + [InlineData("GET / HTTP/1.1 \r\n")] + [InlineData("GET / HTTP/1.1a\r\n")] + [InlineData("GET / HTTP/1.0\n\r\n")] + [InlineData("GET / HTTP/1.2\r\n")] + [InlineData("GET / HTTP/3.0\r\n")] + [InlineData("GET / H\r\n")] + [InlineData("GET / HTTP/1.\r\n")] + [InlineData("GET / hello\r\n")] + [InlineData("GET / 8charact\r\n")] + // Missing LF after CR + [InlineData("GET / HTTP/1.0\rA\n")] // Bad HTTP Methods (invalid according to RFC) [InlineData("( / HTTP/1.0\r\n")] [InlineData(") / HTTP/1.0\r\n")] @@ -88,7 +71,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [InlineData("} / HTTP/1.0\r\n")] [InlineData("get@ / HTTP/1.0\r\n")] [InlineData("post= / HTTP/1.0\r\n")] - public async Task TestBadRequestLines(string request) + public async Task TestInvalidRequestLines(string request) { using (var server = new TestServer(context => TaskUtilities.CompletedTask)) { @@ -100,6 +83,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } } + // TODO: remove test once people agree to change this behavior + /* [Theory] [InlineData(" ")] [InlineData("GET ")] @@ -136,6 +121,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } } } + */ [Theory] // Missing final CRLF @@ -219,13 +205,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests private async Task ReceiveBadRequestResponse(TestConnection connection, string expectedDateHeaderValue) { - await connection.Receive( - "HTTP/1.1 400 Bad Request", - ""); - await connection.Receive( - "Connection: close", - ""); await connection.ReceiveForcedEnd( + "HTTP/1.1 400 Bad Request", + "Connection: close", $"Date: {expectedDateHeaderValue}", "Content-Length: 0", "", diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs new file mode 100644 index 0000000000..15a216130a --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Server.Kestrel; +using Xunit; + +namespace Microsoft.AspNetCore.Server.KestrelTests +{ + public class KestrelServerLimitsTests + { + [Fact] + public void MaxRequestBufferSizeDefault() + { + Assert.Equal(1024 * 1024, (new KestrelServerLimits()).MaxRequestBufferSize); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + public void MaxRequestBufferSizeInvalid(int value) + { + Assert.Throws(() => + { + (new KestrelServerLimits()).MaxRequestBufferSize = value; + }); + } + + [Theory] + [InlineData(null)] + [InlineData(1)] + public void MaxRequestBufferSizeValid(int? value) + { + var o = new KestrelServerLimits(); + o.MaxRequestBufferSize = value; + Assert.Equal(value, o.MaxRequestBufferSize); + } + + [Fact] + public void MaxRequestLineSizeDefault() + { + Assert.Equal(8 * 1024, (new KestrelServerLimits()).MaxRequestLineSize); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(-1)] + [InlineData(0)] + public void MaxRequestLineSizeInvalid(int value) + { + Assert.Throws(() => + { + (new KestrelServerLimits()).MaxRequestLineSize = value; + }); + } + + [Theory] + [InlineData(1)] + [InlineData(int.MaxValue)] + public void MaxRequestLineSizeValid(int value) + { + var o = new KestrelServerLimits(); + o.MaxRequestLineSize = value; + Assert.Equal(value, o.MaxRequestLineSize); + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerOptionsTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerOptionsTests.cs index 9b05072c27..8beb1f7d59 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerOptionsTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerOptionsTests.cs @@ -2,42 +2,43 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; +using System.Linq; +using System.Reflection; using Microsoft.AspNetCore.Server.Kestrel; -using Microsoft.Extensions.Configuration; using Xunit; namespace Microsoft.AspNetCore.Server.KestrelTests { public class KestrelServerInformationTests { +#pragma warning disable CS0612 [Fact] - public void MaxRequestBufferSizeDefault() + public void MaxRequestBufferSizeIsMarkedObsolete() { - Assert.Equal(1024 * 1024, (new KestrelServerOptions()).MaxRequestBufferSize); + Assert.NotNull(typeof(KestrelServerOptions) + .GetProperty(nameof(KestrelServerOptions.MaxRequestBufferSize)) + .GetCustomAttributes(false) + .OfType() + .SingleOrDefault()); } - [Theory] - [InlineData(-1)] - [InlineData(0)] - public void MaxRequestBufferSizeInvalid(int value) - { - Assert.Throws(() => - { - (new KestrelServerOptions()).MaxRequestBufferSize = value; - }); - } - - [Theory] - [InlineData(null)] - [InlineData(1)] - public void MaxRequestBufferSizeValid(int? value) + [Fact] + public void MaxRequestBufferSizeGetsLimitsProperty() { var o = new KestrelServerOptions(); - o.MaxRequestBufferSize = value; - Assert.Equal(value, o.MaxRequestBufferSize); + o.Limits.MaxRequestBufferSize = 42; + Assert.Equal(42, o.MaxRequestBufferSize); } + [Fact] + public void MaxRequestBufferSizeSetsLimitsProperty() + { + var o = new KestrelServerOptions(); + o.MaxRequestBufferSize = 42; + Assert.Equal(42, o.Limits.MaxRequestBufferSize); + } +#pragma warning restore CS0612 + [Fact] public void SetThreadCountUsingProcessorCount() { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/MemoryPoolIteratorTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/MemoryPoolIteratorTests.cs index a8c7e69510..fe450431e6 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/MemoryPoolIteratorTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/MemoryPoolIteratorTests.cs @@ -1,7 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; +using System.Collections.Generic; using System.Linq; -using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using System.Numerics; +using System.Text; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Xunit; namespace Microsoft.AspNetCore.Server.KestrelTests @@ -21,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Fact] - public void FindFirstEqualByte() + public void TestFindFirstEqualByte() { var bytes = Enumerable.Repeat(0xff, Vector.Count).ToArray(); for (int i = 0; i < Vector.Count; i++) @@ -41,7 +46,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } [Fact] - public void FindFirstEqualByteSlow() + public void TestFindFirstEqualByteSlow() { var bytes = Enumerable.Repeat(0xff, Vector.Count).ToArray(); for (int i = 0; i < Vector.Count; i++) @@ -406,6 +411,255 @@ namespace Microsoft.AspNetCore.Server.KestrelTests TestKnownStringsInterning(input, expected, MemoryPoolIteratorExtensions.GetKnownMethod); } + [Theory] + [MemberData(nameof(SeekByteLimitData))] + public void TestSeekByteLimitWithinSameBlock(string input, char seek, int limit, int expectedBytesScanned, int expectedReturnValue) + { + MemoryPoolBlock block = null; + + try + { + // Arrange + var seekVector = new Vector((byte)seek); + + block = _pool.Lease(); + var chars = input.ToString().ToCharArray().Select(c => (byte)c).ToArray(); + Buffer.BlockCopy(chars, 0, block.Array, block.Start, chars.Length); + block.End += chars.Length; + var scan = block.GetIterator(); + + // Act + int bytesScanned; + var returnValue = scan.Seek(ref seekVector, out bytesScanned, limit); + + // Assert + Assert.Equal(expectedBytesScanned, bytesScanned); + Assert.Equal(expectedReturnValue, returnValue); + + Assert.Same(block, scan.Block); + var expectedEndIndex = expectedReturnValue != -1 ? + block.Start + input.IndexOf(seek) : + block.Start + expectedBytesScanned; + Assert.Equal(expectedEndIndex, scan.Index); + } + finally + { + // Cleanup + if (block != null) _pool.Return(block); + } + } + + [Theory] + [MemberData(nameof(SeekByteLimitData))] + public void TestSeekByteLimitAcrossBlocks(string input, char seek, int limit, int expectedBytesScanned, int expectedReturnValue) + { + MemoryPoolBlock block1 = null; + MemoryPoolBlock block2 = null; + MemoryPoolBlock emptyBlock = null; + + try + { + // Arrange + var seekVector = new Vector((byte)seek); + + var input1 = input.Substring(0, input.Length / 2); + block1 = _pool.Lease(); + var chars1 = input1.ToCharArray().Select(c => (byte)c).ToArray(); + Buffer.BlockCopy(chars1, 0, block1.Array, block1.Start, chars1.Length); + block1.End += chars1.Length; + + emptyBlock = _pool.Lease(); + block1.Next = emptyBlock; + + var input2 = input.Substring(input.Length / 2); + block2 = _pool.Lease(); + var chars2 = input2.ToCharArray().Select(c => (byte)c).ToArray(); + Buffer.BlockCopy(chars2, 0, block2.Array, block2.Start, chars2.Length); + block2.End += chars2.Length; + emptyBlock.Next = block2; + + var scan = block1.GetIterator(); + + // Act + int bytesScanned; + var returnValue = scan.Seek(ref seekVector, out bytesScanned, limit); + + // Assert + Assert.Equal(expectedBytesScanned, bytesScanned); + Assert.Equal(expectedReturnValue, returnValue); + + var seekCharIndex = input.IndexOf(seek); + var expectedEndBlock = limit <= input.Length / 2 ? + block1 : + (seekCharIndex != -1 && seekCharIndex < input.Length / 2 ? block1 : block2); + Assert.Same(expectedEndBlock, scan.Block); + var expectedEndIndex = expectedReturnValue != -1 ? + expectedEndBlock.Start + (expectedEndBlock == block1 ? input1.IndexOf(seek) : input2.IndexOf(seek)) : + expectedEndBlock.Start + (expectedEndBlock == block1 ? expectedBytesScanned : expectedBytesScanned - (input.Length / 2)); + Assert.Equal(expectedEndIndex, scan.Index); + } + finally + { + // Cleanup + if (block1 != null) _pool.Return(block1); + if (emptyBlock != null) _pool.Return(emptyBlock); + if (block2 != null) _pool.Return(block2); + } + } + + [Theory] + [MemberData(nameof(SeekIteratorLimitData))] + public void TestSeekIteratorLimitWithinSameBlock(string input, char seek, char limitAt, int expectedReturnValue) + { + MemoryPoolBlock block = null; + + try + { + // Arrange + var seekVector = new Vector((byte)seek); + var limitAtVector = new Vector((byte)limitAt); + var afterSeekVector = new Vector((byte)'B'); + + block = _pool.Lease(); + var chars = input.ToCharArray().Select(c => (byte)c).ToArray(); + Buffer.BlockCopy(chars, 0, block.Array, block.Start, chars.Length); + block.End += chars.Length; + var scan1 = block.GetIterator(); + var scan2_1 = scan1; + var scan2_2 = scan1; + var scan3_1 = scan1; + var scan3_2 = scan1; + var scan3_3 = scan1; + var end = scan1; + + // Act + var endReturnValue = end.Seek(ref limitAtVector); + var returnValue1 = scan1.Seek(ref seekVector, ref end); + var returnValue2_1 = scan2_1.Seek(ref seekVector, ref afterSeekVector, ref end); + var returnValue2_2 = scan2_2.Seek(ref afterSeekVector, ref seekVector, ref end); + var returnValue3_1 = scan3_1.Seek(ref seekVector, ref afterSeekVector, ref afterSeekVector, ref end); + var returnValue3_2 = scan3_2.Seek(ref afterSeekVector, ref seekVector, ref afterSeekVector, ref end); + var returnValue3_3 = scan3_3.Seek(ref afterSeekVector, ref afterSeekVector, ref seekVector, ref end); + + // Assert + Assert.Equal(input.Contains(limitAt) ? limitAt : -1, endReturnValue); + Assert.Equal(expectedReturnValue, returnValue1); + Assert.Equal(expectedReturnValue, returnValue2_1); + Assert.Equal(expectedReturnValue, returnValue2_2); + Assert.Equal(expectedReturnValue, returnValue3_1); + Assert.Equal(expectedReturnValue, returnValue3_2); + Assert.Equal(expectedReturnValue, returnValue3_3); + + Assert.Same(block, scan1.Block); + Assert.Same(block, scan2_1.Block); + Assert.Same(block, scan2_2.Block); + Assert.Same(block, scan3_1.Block); + Assert.Same(block, scan3_2.Block); + Assert.Same(block, scan3_3.Block); + + var expectedEndIndex = expectedReturnValue != -1 ? block.Start + input.IndexOf(seek) : end.Index; + Assert.Equal(expectedEndIndex, scan1.Index); + Assert.Equal(expectedEndIndex, scan2_1.Index); + Assert.Equal(expectedEndIndex, scan2_2.Index); + Assert.Equal(expectedEndIndex, scan3_1.Index); + Assert.Equal(expectedEndIndex, scan3_2.Index); + Assert.Equal(expectedEndIndex, scan3_3.Index); + } + finally + { + // Cleanup + if (block != null) _pool.Return(block); + } + } + + [Theory] + [MemberData(nameof(SeekIteratorLimitData))] + public void TestSeekIteratorLimitAcrossBlocks(string input, char seek, char limitAt, int expectedReturnValue) + { + MemoryPoolBlock block1 = null; + MemoryPoolBlock block2 = null; + MemoryPoolBlock emptyBlock = null; + + try + { + // Arrange + var seekVector = new Vector((byte)seek); + var limitAtVector = new Vector((byte)limitAt); + var afterSeekVector = new Vector((byte)'B'); + + var input1 = input.Substring(0, input.Length / 2); + block1 = _pool.Lease(); + var chars1 = input1.ToCharArray().Select(c => (byte)c).ToArray(); + Buffer.BlockCopy(chars1, 0, block1.Array, block1.Start, chars1.Length); + block1.End += chars1.Length; + + emptyBlock = _pool.Lease(); + block1.Next = emptyBlock; + + var input2 = input.Substring(input.Length / 2); + block2 = _pool.Lease(); + var chars2 = input2.ToCharArray().Select(c => (byte)c).ToArray(); + Buffer.BlockCopy(chars2, 0, block2.Array, block2.Start, chars2.Length); + block2.End += chars2.Length; + emptyBlock.Next = block2; + + var scan1 = block1.GetIterator(); + var scan2_1 = scan1; + var scan2_2 = scan1; + var scan3_1 = scan1; + var scan3_2 = scan1; + var scan3_3 = scan1; + var end = scan1; + + // Act + var endReturnValue = end.Seek(ref limitAtVector); + var returnValue1 = scan1.Seek(ref seekVector, ref end); + var returnValue2_1 = scan2_1.Seek(ref seekVector, ref afterSeekVector, ref end); + var returnValue2_2 = scan2_2.Seek(ref afterSeekVector, ref seekVector, ref end); + var returnValue3_1 = scan3_1.Seek(ref seekVector, ref afterSeekVector, ref afterSeekVector, ref end); + var returnValue3_2 = scan3_2.Seek(ref afterSeekVector, ref seekVector, ref afterSeekVector, ref end); + var returnValue3_3 = scan3_3.Seek(ref afterSeekVector, ref afterSeekVector, ref seekVector, ref end); + + // Assert + Assert.Equal(input.Contains(limitAt) ? limitAt : -1, endReturnValue); + Assert.Equal(expectedReturnValue, returnValue1); + Assert.Equal(expectedReturnValue, returnValue2_1); + Assert.Equal(expectedReturnValue, returnValue2_2); + Assert.Equal(expectedReturnValue, returnValue3_1); + Assert.Equal(expectedReturnValue, returnValue3_2); + Assert.Equal(expectedReturnValue, returnValue3_3); + + var seekCharIndex = input.IndexOf(seek); + var limitAtIndex = input.IndexOf(limitAt); + var expectedEndBlock = seekCharIndex != -1 && seekCharIndex < input.Length / 2 ? + block1 : + (limitAtIndex != -1 && limitAtIndex < input.Length / 2 ? block1 : block2); + Assert.Same(expectedEndBlock, scan1.Block); + Assert.Same(expectedEndBlock, scan2_1.Block); + Assert.Same(expectedEndBlock, scan2_2.Block); + Assert.Same(expectedEndBlock, scan3_1.Block); + Assert.Same(expectedEndBlock, scan3_2.Block); + Assert.Same(expectedEndBlock, scan3_3.Block); + + var expectedEndIndex = expectedReturnValue != -1 ? + expectedEndBlock.Start + (expectedEndBlock == block1 ? input1.IndexOf(seek) : input2.IndexOf(seek)) : + end.Index; + Assert.Equal(expectedEndIndex, scan1.Index); + Assert.Equal(expectedEndIndex, scan2_1.Index); + Assert.Equal(expectedEndIndex, scan2_2.Index); + Assert.Equal(expectedEndIndex, scan3_1.Index); + Assert.Equal(expectedEndIndex, scan3_2.Index); + Assert.Equal(expectedEndIndex, scan3_3.Index); + } + finally + { + // Cleanup + if (block1 != null) _pool.Return(block1); + if (emptyBlock != null) _pool.Return(emptyBlock); + if (block2 != null) _pool.Return(block2); + } + } + private delegate bool GetKnownString(MemoryPoolIterator iter, out string result); private void TestKnownStringsInterning(string input, string expected, GetKnownString action) @@ -435,5 +689,127 @@ namespace Microsoft.AspNetCore.Server.KestrelTests Assert.Equal(knownString1, expected); Assert.Same(knownString1, knownString2); } + + public static IEnumerable SeekByteLimitData + { + get + { + var vectorSpan = Vector.Count; + + // string input, char seek, int limit, int expectedBytesScanned, int expectedReturnValue + var data = new List(); + + // Non-vector inputs + + data.Add(new object[] { "hello, world", 'h', 12, 1, 'h' }); + data.Add(new object[] { "hello, world", ' ', 12, 7, ' ' }); + data.Add(new object[] { "hello, world", 'd', 12, 12, 'd' }); + data.Add(new object[] { "hello, world", '!', 12, 12, -1 }); + data.Add(new object[] { "hello, world", 'h', 13, 1, 'h' }); + data.Add(new object[] { "hello, world", ' ', 13, 7, ' ' }); + data.Add(new object[] { "hello, world", 'd', 13, 12, 'd' }); + data.Add(new object[] { "hello, world", '!', 13, 12, -1 }); + data.Add(new object[] { "hello, world", 'h', 5, 1, 'h' }); + data.Add(new object[] { "hello, world", 'o', 5, 5, 'o' }); + data.Add(new object[] { "hello, world", ',', 5, 5, -1 }); + data.Add(new object[] { "hello, world", 'd', 5, 5, -1 }); + data.Add(new object[] { "abba", 'a', 4, 1, 'a' }); + data.Add(new object[] { "abba", 'b', 4, 2, 'b' }); + + // Vector inputs + + // Single vector, no seek char in input, expect failure + data.Add(new object[] { new string('a', vectorSpan), 'b', vectorSpan, vectorSpan, -1 }); + // Two vectors, no seek char in input, expect failure + data.Add(new object[] { new string('a', vectorSpan * 2), 'b', vectorSpan * 2, vectorSpan * 2, -1 }); + // Two vectors plus non vector length (thus hitting slow path too), no seek char in input, expect failure + data.Add(new object[] { new string('a', vectorSpan * 2 + vectorSpan / 2), 'b', vectorSpan * 2 + vectorSpan / 2, vectorSpan * 2 + vectorSpan / 2, -1 }); + + // For each input length from 1/2 to 3 1/2 vector spans in 1/2 vector span increments... + for (var length = vectorSpan / 2; length <= vectorSpan * 3 + vectorSpan / 2; length += vectorSpan / 2) + { + // ...place the seek char at vector and input boundaries... + for (var i = Math.Min(vectorSpan - 1, length - 1); i < length; i += ((i + 1) % vectorSpan == 0) ? 1 : Math.Min(i + (vectorSpan - 1), length - 1)) + { + var input = new StringBuilder(new string('a', length)); + input[i] = 'b'; + + // ...and check with a seek byte limit before, at, and past the seek char position... + for (var limitOffset = -1; limitOffset <= 1; limitOffset++) + { + var limit = (i + 1) + limitOffset; + + if (limit >= i + 1) + { + // ...that Seek() succeeds when the seek char is within that limit... + data.Add(new object[] { input.ToString(), 'b', limit, i + 1, 'b' }); + } + else + { + // ...and fails when it's not. + data.Add(new object[] { input.ToString(), 'b', limit, Math.Min(length, limit), -1 }); + } + } + } + } + + return data; + } + } + + public static IEnumerable SeekIteratorLimitData + { + get + { + var vectorSpan = Vector.Count; + + // string input, char seek, char limitAt, int expectedReturnValue + var data = new List(); + + // Non-vector inputs + + data.Add(new object[] { "hello, world", 'h', 'd', 'h' }); + data.Add(new object[] { "hello, world", ' ', 'd', ' ' }); + data.Add(new object[] { "hello, world", 'd', 'd', 'd' }); + data.Add(new object[] { "hello, world", '!', 'd', -1 }); + data.Add(new object[] { "hello, world", 'h', 'w', 'h' }); + data.Add(new object[] { "hello, world", 'o', 'w', 'o' }); + data.Add(new object[] { "hello, world", 'r', 'w', -1 }); + data.Add(new object[] { "hello, world", 'd', 'w', -1 }); + + // Vector inputs + + // Single vector, no seek char in input, expect failure + data.Add(new object[] { new string('a', vectorSpan), 'b', 'b', -1 }); + // Two vectors, no seek char in input, expect failure + data.Add(new object[] { new string('a', vectorSpan * 2), 'b', 'b', -1 }); + // Two vectors plus non vector length (thus hitting slow path too), no seek char in input, expect failure + data.Add(new object[] { new string('a', vectorSpan * 2 + vectorSpan / 2), 'b', 'b', -1 }); + + // For each input length from 1/2 to 3 1/2 vector spans in 1/2 vector span increments... + for (var length = vectorSpan / 2; length <= vectorSpan * 3 + vectorSpan / 2; length += vectorSpan / 2) + { + // ...place the seek char at vector and input boundaries... + for (var i = Math.Min(vectorSpan - 1, length - 1); i < length; i += ((i + 1) % vectorSpan == 0) ? 1 : Math.Min(i + (vectorSpan - 1), length - 1)) + { + var input = new StringBuilder(new string('a', length)); + input[i] = 'b'; + + // ...along with sentinel characters to seek the limit iterator to... + input[i - 1] = 'A'; + if (i < length - 1) input[i + 1] = 'B'; + + // ...and check that Seek() succeeds with a limit iterator at or past the seek char position... + data.Add(new object[] { input.ToString(), 'b', 'b', 'b' }); + if (i < length - 1) data.Add(new object[] { input.ToString(), 'b', 'B', 'b' }); + + // ...and fails with a limit iterator before the seek char position. + data.Add(new object[] { input.ToString(), 'b', 'A', -1 }); + } + } + + return data; + } + } } } \ No newline at end of file diff --git a/test/shared/TestConnection.cs b/test/shared/TestConnection.cs index a27ccb29a9..4accbe0ed1 100644 --- a/test/shared/TestConnection.cs +++ b/test/shared/TestConnection.cs @@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.Testing var task = _reader.ReadAsync(actual, offset, actual.Length - offset); if (!Debugger.IsAttached) { - Assert.True(task.Wait(4000), "timeout"); + Assert.True(await Task.WhenAny(task, Task.Delay(10000)) == task, "TestConnection.Receive timed out."); } var count = await task; if (count == 0)