From 6016e70285345dd90029607fced73aac045043f6 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Mon, 13 Apr 2020 17:46:43 +0100 Subject: [PATCH] Make header parsing "safe" (#20562) * OnHeadersComplete improve speculation and memory ordering (VTune) --- .../Core/src/Internal/Http/HttpParser.cs | 298 +++++++++++------- .../Core/src/Internal/Http/HttpProtocol.cs | 4 +- .../src/Internal/Http/HttpRequestHeaders.cs | 20 +- .../Internal/Infrastructure/HttpUtilities.cs | 2 +- .../Kestrel/Core/test/HttpParserTests.cs | 25 ++ .../Kestrel/shared/test/HttpParsingData.cs | 48 +-- .../ServerInfrastructure/BufferExtensions.cs | 44 +++ 7 files changed, 293 insertions(+), 148 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs index d34dc047c2..71b647b41e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs @@ -4,9 +4,7 @@ using System; using System.Buffers; using System.Diagnostics; -using System.Net.Http; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http @@ -179,7 +177,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod, pathEncoded); } - public unsafe bool ParseHeaders(TRequestHandler handler, ref SequenceReader reader) + public bool ParseHeaders(TRequestHandler handler, ref SequenceReader reader) { while (!reader.End) { @@ -236,159 +234,229 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // in the span to contain a header. if (readAhead == 0) { - length = span.IndexOf(ByteLF) + 1; - if (length > 0) + length = span.IndexOfAny(ByteCR, ByteLF); + // If not found length with be -1; casting to uint will turn it to uint.MaxValue + // which will be larger than any possible span.Length. This also serves to eliminate + // the bounds check for the next lookup of span[length] + if ((uint)length < (uint)span.Length) { - // Potentially found the end, or an invalid header. - fixed (byte* pHeader = span) + // Early memory read to hide latency + var expectedCR = span[length]; + // Correctly has a CR, move to next + length++; + + if (expectedCR != ByteCR) { - TakeSingleHeader(pHeader, length, handler); + // Sequence needs to be CRLF not LF first. + RejectRequestHeader(span[..length]); + } + + if ((uint)length < (uint)span.Length) + { + // Early memory read to hide latency + var expectedLF = span[length]; + // Correctly has a LF, move to next + length++; + + if (expectedLF != ByteLF || + length < 5 || + // Exclude the CRLF from the headerLine and parse the header name:value pair + !TryTakeSingleHeader(handler, span[..(length - 2)])) + { + // Sequence needs to be CRLF and not contain an inner CR not part of terminator. + // Less than min possible headerSpan of 5 bytes a:b\r\n + // Not parsable as a valid name:value header pair. + RejectRequestHeader(span[..length]); + } + + // Read the header successfully, skip the reader forward past the headerSpan. + span = span.Slice(length); + reader.Advance(length); + } + else + { + // No enough data, set length to 0. + length = 0; } - // Read the header successfully, skip the reader forward past the header line. - reader.Advance(length); - span = span.Slice(length); } } - // End not found in current span - if (length <= 0) + // End found in current span + if (length > 0) { - // We moved the reader to look ahead 2 bytes so rewind the reader - if (readAhead > 0) - { - reader.Rewind(readAhead); - } - - length = ParseMultiSpanHeader(handler, ref reader); - if (length < 0) - { - // Not there - return false; - } - - reader.Advance(length); - // As we crossed spans set the current span to default - // so we move to the next span on the next iteration - span = default; + continue; } + + // We moved the reader to look ahead 2 bytes so rewind the reader + if (readAhead > 0) + { + reader.Rewind(readAhead); + } + + length = ParseMultiSpanHeader(handler, ref reader); + if (length < 0) + { + // Not there + return false; + } + + reader.Advance(length); + // As we crossed spans set the current span to default + // so we move to the next span on the next iteration + span = default; } } return false; } - private unsafe int ParseMultiSpanHeader(TRequestHandler handler, ref SequenceReader reader) + private int ParseMultiSpanHeader(TRequestHandler handler, ref SequenceReader reader) { - var buffer = reader.Sequence; - var currentSlice = buffer.Slice(reader.Position, reader.Remaining); - var lineEndPosition = currentSlice.PositionOf(ByteLF); - // Split buffers + var currentSlice = reader.UnreadSequence; + var lineEndPosition = currentSlice.PositionOfAny(ByteCR, ByteLF); + if (lineEndPosition == null) { - // Not there + // Not there. return -1; } - var lineEnd = lineEndPosition.Value; - - // Make sure LF is included in lineEnd - lineEnd = buffer.GetPosition(1, lineEnd); - var headerSpan = buffer.Slice(reader.Position, lineEnd).ToSpan(); - var length = headerSpan.Length; - - fixed (byte* pHeader = headerSpan) + SequencePosition lineEnd; + ReadOnlySpan headerSpan; + if (currentSlice.Slice(reader.Position, lineEndPosition.Value).Length == currentSlice.Length - 1) { - TakeSingleHeader(pHeader, length, handler); + // No enough data, so CRLF can't currently be there. + // However, we need to check the found char is CR and not LF + + // Advance 1 to include CR/LF in lineEnd + lineEnd = currentSlice.GetPosition(1, lineEndPosition.Value); + headerSpan = currentSlice.Slice(reader.Position, lineEnd).ToSpan(); + if (headerSpan[^1] != ByteCR) + { + RejectRequestHeader(headerSpan); + } + return -1; } - return length; + // Advance 2 to include CR{LF?} in lineEnd + lineEnd = currentSlice.GetPosition(2, lineEndPosition.Value); + headerSpan = currentSlice.Slice(reader.Position, lineEnd).ToSpan(); + + if (headerSpan.Length < 5) + { + // Less than min possible headerSpan is 5 bytes a:b\r\n + RejectRequestHeader(headerSpan); + } + + if (headerSpan[^2] != ByteCR) + { + // Sequence needs to be CRLF not LF first. + RejectRequestHeader(headerSpan[..^1]); + } + + if (headerSpan[^1] != ByteLF || + // Exclude the CRLF from the headerLine and parse the header name:value pair + !TryTakeSingleHeader(handler, headerSpan[..^2])) + { + // Sequence needs to be CRLF and not contain an inner CR not part of terminator. + // Not parsable as a valid name:value header pair. + RejectRequestHeader(headerSpan); + } + + return headerSpan.Length; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private unsafe int FindEndOfName(byte* headerLine, int length) + private static bool TryTakeSingleHeader(TRequestHandler handler, ReadOnlySpan headerLine) { - var index = 0; - var sawWhitespace = false; - for (; index < length; index++) + // We are looking for a colon to terminate the header name. + // However, the header name cannot contain a space or tab so look for all three + // and see which is found first. + var nameEnd = headerLine.IndexOfAny(ByteColon, ByteSpace, ByteTab); + // If not found length with be -1; casting to uint will turn it to uint.MaxValue + // which will be larger than any possible headerLine.Length. This also serves to eliminate + // the bounds check for the next lookup of headerLine[nameEnd] + if ((uint)nameEnd >= (uint)headerLine.Length) { - var ch = headerLine[index]; - if (ch == ByteColon) - { - break; - } - if (ch == ByteTab || ch == ByteSpace || ch == ByteCR) - { - sawWhitespace = true; - } + // Colon not found. + return false; } - if (index == length || sawWhitespace) + // Early memory read to hide latency + var expectedColon = headerLine[nameEnd]; + if (nameEnd == 0) { - // Set to -1 to indicate invalid. - index = -1; + // Header name is empty. + return false; + } + if (expectedColon != ByteColon) + { + // Header name space or tab. + return false; } - return index; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private unsafe void TakeSingleHeader(byte* headerLine, int length, TRequestHandler handler) - { - // Skip CR, LF from end position - var valueEnd = length - 3; - var nameEnd = FindEndOfName(headerLine, length); - - // Header name is empty, invalid, or doesn't end in CRLF - if (nameEnd <= 0 || headerLine[valueEnd + 2] != ByteLF || headerLine[valueEnd + 1] != ByteCR) - { - RejectRequestHeader(headerLine, length); - } - - // Skip colon from value start + // Skip colon to get to the value start. var valueStart = nameEnd + 1; - // Ignore start whitespace - for (; valueStart < valueEnd; valueStart++) + + // Generally there will only be one space, so we will check it directly + if ((uint)valueStart < (uint)headerLine.Length) { var ch = headerLine[valueStart]; - if (ch != ByteTab && ch != ByteSpace && ch != ByteCR) + if (ch == ByteSpace || ch == ByteTab) { - break; - } - else if (ch == ByteCR) - { - RejectRequestHeader(headerLine, length); + // Ignore first whitespace. + valueStart++; + + // More header chars? + if ((uint)valueStart < (uint)headerLine.Length) + { + ch = headerLine[valueStart]; + // Do a fast check; as we now expect non-space, before moving into loop. + if (ch <= ByteSpace && (ch == ByteSpace || ch == ByteTab)) + { + valueStart++; + // Is more whitespace, so we will loop to find the end. This is the slow path. + for (; valueStart < headerLine.Length; valueStart++) + { + ch = headerLine[valueStart]; + if (ch != ByteTab && ch != ByteSpace) + { + // Non-whitespace char found, valueStart is now start of value. + break; + } + } + } + } } } - // Check for CR in value - var valueBuffer = new Span(headerLine + valueStart, valueEnd - valueStart + 1); - if (valueBuffer.Contains(ByteCR)) - { - RejectRequestHeader(headerLine, length); - } - - // Ignore end whitespace - var lengthChanged = false; - for (; valueEnd >= valueStart; valueEnd--) + var valueEnd = headerLine.Length - 1; + // Ignore end whitespace. Generally there will no spaces + // so we will check the first before moving to a loop. + if (valueEnd > valueStart) { var ch = headerLine[valueEnd]; - if (ch != ByteTab && ch != ByteSpace) + // Do a fast check; as we now expect non-space, before moving into loop. + if (ch <= ByteSpace && (ch == ByteSpace || ch == ByteTab)) { - break; + // Is whitespace so move to loop + valueEnd--; + for (; valueEnd > valueStart; valueEnd--) + { + ch = headerLine[valueEnd]; + if (ch != ByteTab && ch != ByteSpace) + { + // Non-whitespace char found, valueEnd is now start of value. + break; + } + } } - - lengthChanged = true; } - if (lengthChanged) - { - // Length changed - valueBuffer = new Span(headerLine + valueStart, valueEnd - valueStart + 1); - } + // Range end is exclusive, so add 1 to valueEnd + valueEnd++; + handler.OnHeader(name: headerLine[..nameEnd], value: headerLine[valueStart..valueEnd]); - var nameBuffer = new Span(headerLine, nameEnd); - - handler.OnHeader(nameBuffer, valueBuffer); + return true; } [MethodImpl(MethodImplOptions.NoInlining)] @@ -445,8 +513,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } [StackTraceHidden] - private unsafe void RejectRequestHeader(byte* headerLine, int length) - => throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestHeader, headerLine, length); + private void RejectRequestHeader(ReadOnlySpan headerLine) + => throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestHeader, headerLine); [StackTraceHidden] private unsafe void RejectUnknownVersion(byte* version, int length) @@ -454,10 +522,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http [MethodImpl(MethodImplOptions.NoInlining)] private unsafe BadHttpRequestException GetInvalidRequestException(RequestRejectionReason reason, byte* detail, int length) + => GetInvalidRequestException(reason, new ReadOnlySpan(detail, length)); + + [MethodImpl(MethodImplOptions.NoInlining)] + private BadHttpRequestException GetInvalidRequestException(RequestRejectionReason reason, ReadOnlySpan headerLine) => KestrelBadHttpRequestException.GetException( reason, _showErrorDetails - ? new Span(detail, length).GetAsciiStringEscaped(Constants.MaxExceptionDetailSize) + ? headerLine.GetAsciiStringEscaped(Constants.MaxExceptionDetailSize) : string.Empty); } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index cfe97f0f1e..ca612a85ab 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -603,7 +603,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http BeginRequestProcessing(); var result = default(ReadResult); - var endConnection = false; + bool endConnection; do { if (BeginRead(out var awaitable)) @@ -1274,7 +1274,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http => throw GetInvalidRequestTargetException(target); [MethodImpl(MethodImplOptions.NoInlining)] - private BadHttpRequestException GetInvalidRequestTargetException(Span target) + private BadHttpRequestException GetInvalidRequestTargetException(ReadOnlySpan target) => KestrelBadHttpRequestException.GetException( RequestRejectionReason.InvalidRequestTarget, Log.IsEnabled(LogLevel.Information) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs index 95f5c69e79..c2a75857c2 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs @@ -27,18 +27,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public void OnHeadersComplete() { - var bitsToClear = _previousBits & ~_bits; + var newHeaderFlags = _bits; + var previousHeaderFlags = _previousBits; _previousBits = 0; - if (bitsToClear != 0) + var headersToClear = (~newHeaderFlags) & previousHeaderFlags; + if (headersToClear == 0) { - // Some previous headers were not reused or overwritten. - - // While they cannot be accessed by the current request (as they were not supplied by it) - // there is no point in holding on to them, so clear them now, - // to allow them to get collected by the GC. - Clear(bitsToClear); + // All headers were resued. + return; } + + // Some previous headers were not reused or overwritten. + // While they cannot be accessed by the current request (as they were not supplied by it) + // there is no point in holding on to them, so clear them now, + // to allow them to get collected by the GC. + Clear(headersToClear); } protected override void ClearFast() diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs index 9e21228c6d..9cd8dce014 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs @@ -221,7 +221,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure public static string GetRequestHeaderStringNonNullCharacters(this ReadOnlySpan span, bool useLatin1) => useLatin1 ? GetLatin1StringNonNullCharacters(span) : GetAsciiOrUTF8StringNonNullCharacters(span); - public static string GetAsciiStringEscaped(this Span span, int maxChars) + public static string GetAsciiStringEscaped(this ReadOnlySpan span, int maxChars) { var sb = new StringBuilder(); diff --git a/src/Servers/Kestrel/Core/test/HttpParserTests.cs b/src/Servers/Kestrel/Core/test/HttpParserTests.cs index b8029aa403..f4497c60b5 100644 --- a/src/Servers/Kestrel/Core/test/HttpParserTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpParserTests.cs @@ -429,6 +429,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(RequestRejectionReason.TlsOverHttpError, badHttpRequestException.Reason); } + [Theory] + [MemberData(nameof(RequestHeaderInvalidData))] + public void ParseHeadersThrowsOnInvalidRequestHeadersWithGratuitouslySplitBuffers(string rawHeaders, string expectedExceptionMessage) + { + var mockTrace = new Mock(); + mockTrace + .Setup(trace => trace.IsEnabled(LogLevel.Information)) + .Returns(true); + + var parser = CreateParser(mockTrace.Object); + var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(rawHeaders); + var requestHandler = new RequestHandler(); + +#pragma warning disable CS0618 // Type or member is obsolete + var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete + { + var reader = new SequenceReader(buffer); + parser.ParseHeaders(requestHandler, ref reader); + }); + + Assert.Equal(expectedExceptionMessage, exception.Message); + Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); + } + [Fact] public void ParseHeadersWithGratuitouslySplitBuffers() { diff --git a/src/Servers/Kestrel/shared/test/HttpParsingData.cs b/src/Servers/Kestrel/shared/test/HttpParsingData.cs index 68a8c4857b..8d9cbabb23 100644 --- a/src/Servers/Kestrel/shared/test/HttpParsingData.cs +++ b/src/Servers/Kestrel/shared/test/HttpParsingData.cs @@ -377,19 +377,19 @@ namespace Microsoft.AspNetCore.Testing new[] { "Header-1: value1\r\n\tHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09Header-2: value2\x0D\x0A") }, // CR in value - new[] { "Header-1: value1\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: value1\x0D\x0D\x0A") }, - new[] { "Header-1: val\rue1\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: val\x0Due1\x0D\x0A") }, - new[] { "Header-1: value1\rHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: value1\x0DHeader-2: value2\x0D\x0A") }, - new[] { "Header-1: value1\r\nHeader-2: value2\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2: value2\x0D\x0D\x0A") }, - new[] { "Header-1: value1\r\nHeader-2: v\ralue2\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2: v\x0Dalue2\x0D\x0A") }, - new[] { "Header-1: Value__\rVector16________Vector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value__\x0DVector16________Vector32\x0D\x0A") }, - new[] { "Header-1: Value___Vector16\r________Vector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16\x0D________Vector32\x0D\x0A") }, - new[] { "Header-1: Value___Vector16_______\rVector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16_______\x0DVector32\x0D\x0A") }, - new[] { "Header-1: Value___Vector16________Vector32\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32\x0D\x0D\x0A") }, - new[] { "Header-1: Value___Vector16________Vector32_\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32_\x0D\x0D\x0A") }, - new[] { "Header-1: Value___Vector16________Vector32Value___Vector16_______\rVector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32Value___Vector16_______\x0DVector32\x0D\x0A") }, - new[] { "Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\x0D\x0D\x0A") }, - new[] { "Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\x0D\x0D\x0A") }, + new[] { "Header-1: value1\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: value1\x0D\x0D") }, + new[] { "Header-1: val\rue1\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: val\x0Du") }, + new[] { "Header-1: value1\rHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: value1\x0DH") }, + new[] { "Header-1: value1\r\nHeader-2: value2\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2: value2\x0D\x0D") }, + new[] { "Header-1: value1\r\nHeader-2: v\ralue2\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2: v\x0Da") }, + new[] { "Header-1: Value__\rVector16________Vector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value__\x0DV") }, + new[] { "Header-1: Value___Vector16\r________Vector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16\x0D_") }, + new[] { "Header-1: Value___Vector16_______\rVector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16_______\x0DV") }, + new[] { "Header-1: Value___Vector16________Vector32\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32\x0D\x0D") }, + new[] { "Header-1: Value___Vector16________Vector32_\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32_\x0D\x0D") }, + new[] { "Header-1: Value___Vector16________Vector32Value___Vector16_______\rVector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32Value___Vector16_______\x0DV") }, + new[] { "Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\x0D\x0D") }, + new[] { "Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\x0D\x0D") }, // Missing colon new[] { "Header-1 value1\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1 value1\x0D\x0A") }, @@ -406,20 +406,20 @@ namespace Microsoft.AspNetCore.Testing // Whitespace in header name new[] { "Header : value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header : value\x0D\x0A") }, new[] { "Header\t: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header\x09: value\x0D\x0A") }, - new[] { "Header\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header\x0D: value\x0D\x0A") }, - new[] { "Header_\rVector16: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header_\x0DVector16: value\x0D\x0A") }, - new[] { "Header__Vector16\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16\x0D: value\x0D\x0A") }, - new[] { "Header__Vector16_\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16_\x0D: value\x0D\x0A") }, - new[] { "Header_\rVector16________Vector32: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header_\x0DVector16________Vector32: value\x0D\x0A") }, - new[] { "Header__Vector16________Vector32\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32\x0D: value\x0D\x0A") }, - new[] { "Header__Vector16________Vector32_\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32_\x0D: value\x0D\x0A") }, - new[] { "Header__Vector16________Vector32Header_\rVector16________Vector32: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32Header_\x0DVector16________Vector32: value\x0D\x0A") }, - new[] { "Header__Vector16________Vector32Header__Vector16________Vector32\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32Header__Vector16________Vector32\x0D: value\x0D\x0A") }, - new[] { "Header__Vector16________Vector32Header__Vector16________Vector32_\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32Header__Vector16________Vector32_\x0D: value\x0D\x0A") }, + new[] { "Header\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header\x0D:") }, + new[] { "Header_\rVector16: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header_\x0DV") }, + new[] { "Header__Vector16\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16\x0D:") }, + new[] { "Header__Vector16_\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16_\x0D:") }, + new[] { "Header_\rVector16________Vector32: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header_\x0DV") }, + new[] { "Header__Vector16________Vector32\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32\x0D:") }, + new[] { "Header__Vector16________Vector32_\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32_\x0D:") }, + new[] { "Header__Vector16________Vector32Header_\rVector16________Vector32: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32Header_\x0DV") }, + new[] { "Header__Vector16________Vector32Header__Vector16________Vector32\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32Header__Vector16________Vector32\x0D:") }, + new[] { "Header__Vector16________Vector32Header__Vector16________Vector32_\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32Header__Vector16________Vector32_\x0D:") }, new[] { "Header 1: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 1: value1\x0D\x0A") }, new[] { "Header 1 : value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 1 : value1\x0D\x0A") }, new[] { "Header 1\t: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 1\x09: value1\x0D\x0A") }, - new[] { "Header 1\r: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 1\x0D: value1\x0D\x0A") }, + new[] { "Header 1\r: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 1\x0D:") }, new[] { "Header-1: value1\r\nHeader 2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 2: value2\x0D\x0A") }, new[] { "Header-1: value1\r\nHeader-2 : value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2 : value2\x0D\x0A") }, new[] { "Header-1: value1\r\nHeader-2\t: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2\x09: value2\x0D\x0A") }, diff --git a/src/Shared/ServerInfrastructure/BufferExtensions.cs b/src/Shared/ServerInfrastructure/BufferExtensions.cs index 34cbeb5357..0ba5911c0a 100644 --- a/src/Shared/ServerInfrastructure/BufferExtensions.cs +++ b/src/Shared/ServerInfrastructure/BufferExtensions.cs @@ -40,6 +40,50 @@ namespace System.Buffers return result; } + /// + /// Returns position of first occurrence of item in the + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SequencePosition? PositionOfAny(in this ReadOnlySequence source, T value0, T value1) where T : IEquatable + { + if (source.IsSingleSegment) + { + int index = source.First.Span.IndexOfAny(value0, value1); + if (index != -1) + { + return source.GetPosition(index); + } + + return null; + } + else + { + return PositionOfAnyMultiSegment(source, value0, value1); + } + } + + private static SequencePosition? PositionOfAnyMultiSegment(in ReadOnlySequence source, T value0, T value1) where T : IEquatable + { + SequencePosition position = source.Start; + SequencePosition result = position; + while (source.TryGet(ref position, out ReadOnlyMemory memory)) + { + int index = memory.Span.IndexOfAny(value0, value1); + if (index != -1) + { + return source.GetPosition(index, result); + } + else if (position.GetObject() == null) + { + break; + } + + result = position; + } + + return null; + } + internal static void WriteAscii(ref this BufferWriter buffer, string data) { if (string.IsNullOrEmpty(data))