From f5fa16e998879e88524594b6b28021f297fa3ab4 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Fri, 24 Apr 2020 02:32:54 +0100 Subject: [PATCH] Make HTTP/1.1 startline parsing "safe" (#20885) --- ...pNetCore.Server.Kestrel.Core.netcoreapp.cs | 34 ++- .../Http/Http1ChunkedEncodingMessageBody.cs | 10 +- .../Core/src/Internal/Http/Http1Connection.cs | 225 +++++++++--------- .../src/Internal/Http/Http1ParsingHandler.cs | 4 +- .../Core/src/Internal/Http/HttpParser.cs | 179 +++++--------- .../Core/src/Internal/Http/HttpVersion.cs | 2 +- .../Core/src/Internal/Http/IHttpParser.cs | 2 +- .../Internal/Http/IHttpRequestLineHandler.cs | 76 +++++- .../Internal/Infrastructure/HttpCharacters.cs | 6 +- .../Internal/Infrastructure/HttpUtilities.cs | 130 +++++----- .../Kestrel/Core/test/Http1ConnectionTests.cs | 88 +++++-- .../Kestrel/Core/test/HttpParserTests.cs | 60 ++++- .../Kestrel/Core/test/HttpUtilitiesTest.cs | 4 +- .../Kestrel/Core/test/KnownStringsTests.cs | 2 +- .../Kestrel/Core/test/StartLineTests.cs | 51 ++-- .../Http1ConnectionBenchmark.cs | 10 +- ...Http1ConnectionParsingOverheadBenchmark.cs | 11 +- .../HttpParserBenchmark.cs | 14 +- .../KnownStringsBenchmark.cs | 4 +- .../Kestrel.Performance/Mocks/NullParser.cs | 18 +- .../RequestParsingBenchmark.cs | 21 +- .../BadHttpRequestTests.cs | 2 +- 22 files changed, 542 insertions(+), 411 deletions(-) diff --git a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs index f2a23ebbcc..8f042c6dae 100644 --- a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs +++ b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs @@ -227,7 +227,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public HttpParser() { } public HttpParser(bool showErrorDetails) { } public bool ParseHeaders(TRequestHandler handler, ref System.Buffers.SequenceReader reader) { throw null; } - public bool ParseRequestLine(TRequestHandler handler, in System.Buffers.ReadOnlySequence buffer, out System.SequencePosition consumed, out System.SequencePosition examined) { throw null; } + public bool ParseRequestLine(TRequestHandler handler, ref System.Buffers.SequenceReader reader) { throw null; } } public enum HttpScheme { @@ -235,13 +235,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http Http = 0, Https = 1, } - public enum HttpVersion + public enum HttpVersion : sbyte { - Unknown = -1, - Http10 = 0, - Http11 = 1, - Http2 = 2, - Http3 = 3, + Unknown = (sbyte)-1, + Http10 = (sbyte)0, + Http11 = (sbyte)1, + Http2 = (sbyte)2, + Http3 = (sbyte)3, + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct HttpVersionAndMethod + { + private int _dummyPrimitive; + public HttpVersionAndMethod(Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod method, int methodEnd) { throw null; } + public Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod Method { get { throw null; } } + public int MethodEnd { get { throw null; } } + public Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpVersion Version { get { throw null; } set { } } } public partial interface IHttpHeadersHandler { @@ -252,7 +261,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } public partial interface IHttpRequestLineHandler { - void OnStartLine(Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod method, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpVersion version, System.Span target, System.Span path, System.Span query, System.Span customMethod, bool pathEncoded); + void OnStartLine(Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpVersionAndMethod versionAndMethod, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.TargetOffsetPathLength targetPath, System.Span startLine); + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct TargetOffsetPathLength + { + private readonly int _dummyPrimitive; + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public TargetOffsetPathLength(int offset, int length, bool isEncoded) { throw null; } + public bool IsEncoded { [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]get { throw null; } } + public int Length { [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]get { throw null; } } + public int Offset { [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]get { throw null; } } } } namespace Microsoft.AspNetCore.Server.Kestrel.Https diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs index 2a2c01051d..bdbdc3c481 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs @@ -310,10 +310,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // _consumedBytes aren't tracked for trailer headers, since headers have separate limits. if (_mode == Mode.TrailerHeaders) { - if (_context.TakeMessageHeaders(readableBuffer, trailers: true, out consumed, out examined)) + var reader = new SequenceReader(readableBuffer); + if (_context.TakeMessageHeaders(ref reader, trailers: true)) { + examined = reader.Position; _mode = Mode.Complete; } + else + { + examined = readableBuffer.End; + } + + consumed = reader.Position; } return _mode == Mode.Complete; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index ef77a1f396..7432903bdb 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -139,15 +139,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http SendTimeoutResponse(); } - public void ParseRequest(in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined) + public bool ParseRequest(ref SequenceReader reader) { - consumed = buffer.Start; - examined = buffer.End; - switch (_requestProcessingStatus) { case RequestProcessingStatus.RequestPending: - if (buffer.IsEmpty) + if (reader.End) { break; } @@ -157,75 +154,68 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _requestProcessingStatus = RequestProcessingStatus.ParsingRequestLine; goto case RequestProcessingStatus.ParsingRequestLine; case RequestProcessingStatus.ParsingRequestLine: - if (TakeStartLine(buffer, out consumed, out examined)) + if (TakeStartLine(ref reader)) { - TrimAndParseHeaders(buffer, ref consumed, out examined); - return; + _requestProcessingStatus = RequestProcessingStatus.ParsingHeaders; + goto case RequestProcessingStatus.ParsingHeaders; } else { break; } case RequestProcessingStatus.ParsingHeaders: - if (TakeMessageHeaders(buffer, trailers: false, out consumed, out examined)) + if (TakeMessageHeaders(ref reader, trailers: false)) { _requestProcessingStatus = RequestProcessingStatus.AppStarted; + // Consumed preamble + return true; } break; } - void TrimAndParseHeaders(in ReadOnlySequence buffer, ref SequencePosition consumed, out SequencePosition examined) - { - var trimmedBuffer = buffer.Slice(consumed, buffer.End); - _requestProcessingStatus = RequestProcessingStatus.ParsingHeaders; - - if (TakeMessageHeaders(trimmedBuffer, trailers: false, out consumed, out examined)) - { - _requestProcessingStatus = RequestProcessingStatus.AppStarted; - } - } + // Haven't completed consuming preamble + return false; } - public bool TakeStartLine(in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined) + public bool TakeStartLine(ref SequenceReader reader) { // Make sure the buffer is limited - if (buffer.Length >= ServerOptions.Limits.MaxRequestLineSize) + if (reader.Remaining >= ServerOptions.Limits.MaxRequestLineSize) { // Input oversize, cap amount checked - return TrimAndTakeStartLine(buffer, out consumed, out examined); + return TrimAndTakeStartLine(ref reader); } - return _parser.ParseRequestLine(new Http1ParsingHandler(this), buffer, out consumed, out examined); + return _parser.ParseRequestLine(new Http1ParsingHandler(this), ref reader); - bool TrimAndTakeStartLine(in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined) + bool TrimAndTakeStartLine(ref SequenceReader reader) { - var trimmedBuffer = buffer.Slice(buffer.Start, ServerOptions.Limits.MaxRequestLineSize); + var trimmedBuffer = reader.Sequence.Slice(reader.Position, ServerOptions.Limits.MaxRequestLineSize); + var trimmedReader = new SequenceReader(trimmedBuffer); - if (!_parser.ParseRequestLine(new Http1ParsingHandler(this), trimmedBuffer, out consumed, out examined)) + if (!_parser.ParseRequestLine(new Http1ParsingHandler(this), ref trimmedReader)) { // We read the maximum allowed but didn't complete the start line. KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestLineTooLong); } + reader.Advance(trimmedReader.Consumed); return true; } } - public bool TakeMessageHeaders(in ReadOnlySequence buffer, bool trailers, out SequencePosition consumed, out SequencePosition examined) + public bool TakeMessageHeaders(ref SequenceReader reader, bool trailers) { // Make sure the buffer is limited - if (buffer.Length > _remainingRequestHeadersBytesAllowed) + if (reader.Remaining > _remainingRequestHeadersBytesAllowed) { // Input oversize, cap amount checked - return TrimAndTakeMessageHeaders(buffer, trailers, out consumed, out examined); + return TrimAndTakeMessageHeaders(ref reader, trailers); } - var reader = new SequenceReader(buffer); - var result = false; try { - result = _parser.ParseHeaders(new Http1ParsingHandler(this, trailers), ref reader); - + var result = _parser.ParseHeaders(new Http1ParsingHandler(this, trailers), ref reader); if (result) { TimeoutControl.CancelTimeout(); @@ -235,30 +225,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } finally { - consumed = reader.Position; _remainingRequestHeadersBytesAllowed -= (int)reader.Consumed; - - if (result) - { - examined = consumed; - } - else - { - examined = buffer.End; - } } - bool TrimAndTakeMessageHeaders(in ReadOnlySequence buffer, bool trailers, out SequencePosition consumed, out SequencePosition examined) + bool TrimAndTakeMessageHeaders(ref SequenceReader reader, bool trailers) { - var trimmedBuffer = buffer.Slice(buffer.Start, _remainingRequestHeadersBytesAllowed); - - var reader = new SequenceReader(trimmedBuffer); - var result = false; + var trimmedBuffer = reader.Sequence.Slice(reader.Position, _remainingRequestHeadersBytesAllowed); + var trimmedReader = new SequenceReader(trimmedBuffer); try { - result = _parser.ParseHeaders(new Http1ParsingHandler(this, trailers), ref reader); - - if (!result) + if (!_parser.ParseHeaders(new Http1ParsingHandler(this, trailers), ref trimmedReader)) { // We read the maximum allowed but didn't complete the headers. KestrelBadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize); @@ -266,44 +242,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http TimeoutControl.CancelTimeout(); - return result; + reader.Advance(trimmedReader.Consumed); + + return true; } finally { - consumed = reader.Position; _remainingRequestHeadersBytesAllowed -= (int)reader.Consumed; - - if (result) - { - examined = consumed; - } - else - { - examined = trimmedBuffer.End; - } } } } - public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) + public void OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathLength targetPath, Span startLine) { + var targetStart = targetPath.Offset; + // Slice out target + var target = startLine[targetStart..]; Debug.Assert(target.Length != 0, "Request target must be non-zero length"); - + var method = versionAndMethod.Method; var ch = target[0]; if (ch == ByteForwardSlash) { // origin-form. // The most common form of request-target. // https://tools.ietf.org/html/rfc7230#section-5.3.1 - OnOriginFormTarget(pathEncoded, target, path, query); + OnOriginFormTarget(targetPath, target); } else if (ch == ByteAsterisk && target.Length == 1) { OnAsteriskFormTarget(method); } - else if (target.GetKnownHttpScheme(out _)) + else if (startLine[targetStart..].GetKnownHttpScheme(out _)) { - OnAbsoluteFormTarget(target, query); + OnAbsoluteFormTarget(targetPath, target); } else { @@ -316,10 +287,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http Method = method; if (method == HttpMethod.Custom) { - _methodText = customMethod.GetAsciiStringNonNullCharacters(); + _methodText = startLine[..versionAndMethod.MethodEnd].GetAsciiStringNonNullCharacters(); } - _httpVersion = version; + _httpVersion = versionAndMethod.Version; Debug.Assert(RawTarget != null, "RawTarget was not set"); Debug.Assert(((IHttpRequestFeature)this).Method != null, "Method was not set"); @@ -329,7 +300,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } // Compare with Http2Stream.TryValidatePseudoHeaders - private void OnOriginFormTarget(bool pathEncoded, Span target, Span path, Span query) + private void OnOriginFormTarget(TargetOffsetPathLength targetPath, Span target) { Debug.Assert(target[0] == ByteForwardSlash, "Should only be called when path starts with /"); @@ -349,59 +320,69 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return; } + // Read raw target before mutating memory. + var previousValue = _parsedRawTarget; + if (ServerOptions.DisableStringReuse || + previousValue == null || previousValue.Length != target.Length || + !StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, target)) + { + ParseTarget(targetPath, target); + } + else + { + // As RawTarget is the same we can reuse the previous parsed values. + RawTarget = previousValue; + Path = _parsedPath; + QueryString = _parsedQueryString; + } + + // Clear parsedData for absolute target as we won't check it if we come via this path again, + // an setting to null is fast as it doesn't need to use a GC write barrier. + _parsedAbsoluteRequestTarget = null; + } + + private void ParseTarget(TargetOffsetPathLength targetPath, Span target) + { // 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; // then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs" try { - var disableStringReuse = ServerOptions.DisableStringReuse; - // Read raw target before mutating memory. - var previousValue = _parsedRawTarget; - if (disableStringReuse || - previousValue == null || previousValue.Length != target.Length || - !StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, target)) - { - // The previous string does not match what the bytes would convert to, - // so we will need to generate a new string. - RawTarget = _parsedRawTarget = target.GetAsciiStringNonNullCharacters(); + // The previous string does not match what the bytes would convert to, + // so we will need to generate a new string. + RawTarget = _parsedRawTarget = target.GetAsciiStringNonNullCharacters(); - previousValue = _parsedQueryString; - if (disableStringReuse || - previousValue == null || previousValue.Length != query.Length || - !StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, query)) + var queryLength = 0; + if (target.Length == targetPath.Length) + { + // No query string + if (ReferenceEquals(_parsedQueryString, string.Empty)) { - // The previous string does not match what the bytes would convert to, - // so we will need to generate a new string. - QueryString = _parsedQueryString = query.GetAsciiStringNonNullCharacters(); - } - else - { - // Same as previous QueryString = _parsedQueryString; } - - if (path.Length == 1) - { - // If path.Length == 1 it can only be a forward slash (e.g. home page) - Path = _parsedPath = ForwardSlash; - } else { - Path = _parsedPath = PathNormalizer.DecodePath(path, pathEncoded, RawTarget, query.Length); + QueryString = string.Empty; + _parsedQueryString = string.Empty; } } else { - // As RawTarget is the same we can reuse the previous parsed values. - RawTarget = _parsedRawTarget; - Path = _parsedPath; - QueryString = _parsedQueryString; + queryLength = ParseQuery(targetPath, target); } - // Clear parsedData for absolute target as we won't check it if we come via this path again, - // an setting to null is fast as it doesn't need to use a GC write barrier. - _parsedAbsoluteRequestTarget = null; + var pathLength = targetPath.Length; + if (pathLength == 1) + { + // If path.Length == 1 it can only be a forward slash (e.g. home page) + Path = _parsedPath = ForwardSlash; + } + else + { + var path = target[..pathLength]; + Path = _parsedPath = PathNormalizer.DecodePath(path, targetPath.IsEncoded, RawTarget, queryLength); + } } catch (InvalidOperationException) { @@ -409,6 +390,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } + private int ParseQuery(TargetOffsetPathLength targetPath, Span target) + { + var previousValue = _parsedQueryString; + var query = target[targetPath.Length..]; + var queryLength = query.Length; + if (ServerOptions.DisableStringReuse || + previousValue == null || previousValue.Length != queryLength || + !StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, query)) + { + // The previous string does not match what the bytes would convert to, + // so we will need to generate a new string. + QueryString = _parsedQueryString = query.GetAsciiStringNonNullCharacters(); + } + else + { + // Same as previous + QueryString = _parsedQueryString; + } + + return queryLength; + } + private void OnAuthorityFormTarget(HttpMethod method, Span target) { _requestTargetForm = HttpRequestTarget.AuthorityForm; @@ -480,8 +483,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _parsedAbsoluteRequestTarget = null; } - private void OnAbsoluteFormTarget(Span target, Span query) + private void OnAbsoluteFormTarget(TargetOffsetPathLength targetPath, Span target) { + Span query = target[targetPath.Length..]; _requestTargetForm = HttpRequestTarget.AbsoluteForm; // absolute-form @@ -645,12 +649,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http protected override bool TryParseRequest(ReadResult result, out bool endConnection) { - var examined = result.Buffer.End; - var consumed = result.Buffer.End; - + var reader = new SequenceReader(result.Buffer); + var isConsumed = false; try { - ParseRequest(result.Buffer, out consumed, out examined); + isConsumed = ParseRequest(ref reader); } catch (InvalidOperationException) { @@ -662,7 +665,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } finally { - Input.AdvanceTo(consumed, examined); + Input.AdvanceTo(reader.Position, isConsumed ? reader.Position : result.Buffer.End); } if (result.IsCompleted) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs index 322e46190d..33c1eec9cc 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs @@ -47,8 +47,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } - public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) - => Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded); + public void OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathLength targetPath, Span startLine) + => Connection.OnStartLine(versionAndMethod, targetPath, startLine); public void OnStaticIndexedHeader(int index) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs index 71b647b41e..4a7cb71b74 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs @@ -5,6 +5,7 @@ using System; using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http @@ -34,84 +35,55 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private const byte BytePercentage = (byte)'%'; private const int MinTlsRequestSize = 1; // We need at least 1 byte to check for a proper TLS request line - public unsafe bool ParseRequestLine(TRequestHandler handler, in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined) + public bool ParseRequestLine(TRequestHandler handler, ref SequenceReader reader) { - consumed = buffer.Start; - examined = buffer.End; - - // Prepare the first span - var span = buffer.FirstSpan; - var lineIndex = span.IndexOf(ByteLF); - if (lineIndex >= 0) + if (reader.TryReadTo(out ReadOnlySpan requestLine, ByteLF, advancePastDelimiter: true)) { - consumed = buffer.GetPosition(lineIndex + 1, consumed); - span = span.Slice(0, lineIndex + 1); - } - else if (buffer.IsSingleSegment) - { - // No request line end - return false; - } - else if (TryGetNewLine(buffer, out var found)) - { - span = buffer.Slice(consumed, found).ToSpan(); - consumed = found; - } - else - { - // No request line end - return false; + ParseRequestLine(handler, requestLine); + return true; } - // Fix and parse the span - fixed (byte* data = span) - { - ParseRequestLine(handler, data, span.Length); - } - - examined = consumed; - return true; + return false; } - private unsafe void ParseRequestLine(TRequestHandler handler, byte* data, int length) + private void ParseRequestLine(TRequestHandler handler, ReadOnlySpan requestLine) { // Get Method and set the offset - var method = HttpUtilities.GetKnownMethod(data, length, out var pathStartOffset); - - Span customMethod = default; + var method = requestLine.GetKnownMethod(out var methodEnd); if (method == HttpMethod.Custom) { - customMethod = GetUnknownMethod(data, length, out pathStartOffset); + methodEnd = GetUnknownMethodLength(requestLine); } - // Use a new offset var as pathStartOffset needs to be on stack + var versionAndMethod = new HttpVersionAndMethod(method, methodEnd); + + // Use a new offset var as methodEnd needs to be on stack // as its passed by reference above so can't be in register. // Skip space - var offset = pathStartOffset + 1; - if (offset >= length) + var offset = methodEnd + 1; + if ((uint)offset >= (uint)requestLine.Length) { // Start of path not found - RejectRequestLine(data, length); + RejectRequestLine(requestLine); } - byte ch = data[offset]; + var ch = requestLine[offset]; if (ch == ByteSpace || ch == ByteQuestionMark || ch == BytePercentage) { // Empty path is illegal, or path starting with percentage - RejectRequestLine(data, length); + RejectRequestLine(requestLine); } // Target = Path and Query + var targetStart = offset; var pathEncoded = false; - var pathStart = offset; - // Skip first char (just checked) offset++; // Find end of path and if path is encoded - for (; offset < length; offset++) + for (; (uint)offset < (uint)requestLine.Length; offset++) { - ch = data[offset]; + ch = requestLine[offset]; if (ch == ByteSpace || ch == ByteQuestionMark) { // End of path @@ -123,16 +95,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } - var pathBuffer = new Span(data + pathStart, offset - pathStart); + var path = new TargetOffsetPathLength(targetStart, length: offset - targetStart, pathEncoded); // Query string - var queryStart = offset; if (ch == ByteQuestionMark) { // We have a query string - for (; offset < length; offset++) + for (; (uint)offset < (uint)requestLine.Length; offset++) { - ch = data[offset]; + ch = requestLine[offset]; if (ch == ByteSpace) { break; @@ -140,41 +111,38 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } - // End of query string not found - if (offset == length) - { - RejectRequestLine(data, length); - } - - var targetBuffer = new Span(data + pathStart, offset - pathStart); - var query = new Span(data + queryStart, offset - queryStart); - + var queryEnd = offset; // Consume space offset++; + // If offset has overshot length, end of query string wasn't not found + if ((uint)offset > (uint)requestLine.Length) + { + RejectRequestLine(requestLine); + } + // Version - var httpVersion = HttpUtilities.GetKnownVersion(data + offset, length - offset); + var remaining = requestLine.Slice(offset); + var httpVersion = remaining.GetKnownVersionAndConfirmCR(); + versionAndMethod.Version = httpVersion; if (httpVersion == HttpVersion.Unknown) { - if (data[offset] == ByteCR || data[length - 2] != ByteCR) - { - // If missing delimiter or CR before LF, reject and log entire line - RejectRequestLine(data, length); - } - else - { - // else inform HTTP version is unsupported. - RejectUnknownVersion(data + offset, length - offset - 2); - } + // HTTP version is unsupported or incorrectly terminated. + RejectUnknownVersion(offset, requestLine); } - // After version's 8 bytes and CR, expect LF - if (data[offset + 8 + 1] != ByteLF) + // Version + CR is 8 bytes; adding 9 should take us to .Length + offset += 9; + // LF should have been dropped prior to method call, so offset should now be length + if ((uint)offset != (uint)requestLine.Length) { - RejectRequestLine(data, length); + RejectRequestLine(requestLine); } - handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod, pathEncoded); + // We need to reinterpret from ReadOnlySpan into Span to allow path mutation for + // in-place normalization and decoding to transform into a canonical path + var startLine = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetReference(requestLine), queryEnd); + handler.OnStartLine(versionAndMethod, path, startLine); } public bool ParseHeaders(TRequestHandler handler, ref SequenceReader reader) @@ -460,56 +428,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } [MethodImpl(MethodImplOptions.NoInlining)] - private static bool TryGetNewLine(in ReadOnlySequence buffer, out SequencePosition found) + private int GetUnknownMethodLength(ReadOnlySpan span) { - var byteLfPosition = buffer.PositionOf(ByteLF); - if (byteLfPosition != null) + var invalidIndex = HttpCharacters.IndexOfInvalidTokenChar(span); + + if (invalidIndex <= 0 || span[invalidIndex] != ByteSpace) { - // Move 1 byte past the \n - found = buffer.GetPosition(1, byteLfPosition.Value); - return true; + RejectRequestLine(span); } - found = default; - return false; + return invalidIndex; } - [MethodImpl(MethodImplOptions.NoInlining)] - private unsafe Span GetUnknownMethod(byte* data, int length, out int methodLength) - { - var invalidIndex = HttpCharacters.IndexOfInvalidTokenChar(data, length); - - if (invalidIndex <= 0 || data[invalidIndex] != ByteSpace) - { - RejectRequestLine(data, length); - } - - methodLength = invalidIndex; - return new Span(data, methodLength); - } - - private unsafe bool IsTlsHandshake(byte* data, int length) + private bool IsTlsHandshake(ReadOnlySpan requestLine) { const byte SslRecordTypeHandshake = (byte)0x16; // Make sure we can check at least for the existence of a TLS handshake - we check the first byte // See https://serializethoughts.com/2014/07/27/dissecting-tls-client-hello-message/ - return (length >= MinTlsRequestSize && data[0] == SslRecordTypeHandshake); + return (requestLine.Length >= MinTlsRequestSize && requestLine[0] == SslRecordTypeHandshake); } [StackTraceHidden] - private unsafe void RejectRequestLine(byte* requestLine, int length) + private void RejectRequestLine(ReadOnlySpan requestLine) { - // Check for incoming TLS handshake over HTTP - if (IsTlsHandshake(requestLine, length)) - { - throw GetInvalidRequestException(RequestRejectionReason.TlsOverHttpError, requestLine, length); - } - else - { - throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestLine, requestLine, length); - } + throw GetInvalidRequestException( + IsTlsHandshake(requestLine) ? + RequestRejectionReason.TlsOverHttpError : + RequestRejectionReason.InvalidRequestLine, + requestLine); } [StackTraceHidden] @@ -517,12 +465,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http => throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestHeader, headerLine); [StackTraceHidden] - private unsafe void RejectUnknownVersion(byte* version, int length) - => throw GetInvalidRequestException(RequestRejectionReason.UnrecognizedHTTPVersion, version, length); - - [MethodImpl(MethodImplOptions.NoInlining)] - private unsafe BadHttpRequestException GetInvalidRequestException(RequestRejectionReason reason, byte* detail, int length) - => GetInvalidRequestException(reason, new ReadOnlySpan(detail, length)); + private void RejectUnknownVersion(int offset, ReadOnlySpan requestLine) + // If CR before LF, reject and log entire line + => throw (((uint)offset >= (uint)requestLine.Length || requestLine[offset] == ByteCR || requestLine[^1] != ByteCR) ? + GetInvalidRequestException(RequestRejectionReason.InvalidRequestLine, requestLine) : + GetInvalidRequestException(RequestRejectionReason.UnrecognizedHTTPVersion, requestLine[offset..^1])); [MethodImpl(MethodImplOptions.NoInlining)] private BadHttpRequestException GetInvalidRequestException(RequestRejectionReason reason, ReadOnlySpan headerLine) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpVersion.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpVersion.cs index e6e2a0dabf..bf2ba601c8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpVersion.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpVersion.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { - public enum HttpVersion + public enum HttpVersion : sbyte { Unknown = -1, Http10 = 0, diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/IHttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/IHttpParser.cs index 20688fe291..243dd188b5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/IHttpParser.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/IHttpParser.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { internal interface IHttpParser where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler { - bool ParseRequestLine(TRequestHandler handler, in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined); + bool ParseRequestLine(TRequestHandler handler, ref SequenceReader reader); bool ParseHeaders(TRequestHandler handler, ref SequenceReader reader); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/IHttpRequestLineHandler.cs b/src/Servers/Kestrel/Core/src/Internal/Http/IHttpRequestLineHandler.cs index 494d2c4453..b1163c3ea8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/IHttpRequestLineHandler.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/IHttpRequestLineHandler.cs @@ -2,11 +2,85 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Runtime.CompilerServices; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { public interface IHttpRequestLineHandler { - void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded); + void OnStartLine( + HttpVersionAndMethod versionAndMethod, + TargetOffsetPathLength targetPath, + Span startLine); + } + + public struct HttpVersionAndMethod + { + private ulong _versionAndMethod; + + public HttpVersionAndMethod(HttpMethod method, int methodEnd) + { + _versionAndMethod = ((ulong)(uint)methodEnd << 32) | ((ulong)method << 8); + } + + public HttpVersion Version + { + get => (HttpVersion)(sbyte)(byte)_versionAndMethod; + set => _versionAndMethod = (_versionAndMethod & ~0xFFul) | (byte)value; + } + + public HttpMethod Method => (HttpMethod)(byte)(_versionAndMethod >> 8); + + public int MethodEnd => (int)(uint)(_versionAndMethod >> 32); + } + + public readonly struct TargetOffsetPathLength + { + private readonly ulong _targetOffsetPathLength; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TargetOffsetPathLength(int offset, int length, bool isEncoded) + { + if (isEncoded) + { + length = -length; + } + + _targetOffsetPathLength = ((ulong)offset << 32) | (uint)length; + } + + public int Offset + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return (int)(_targetOffsetPathLength >> 32); + } + } + + public int Length + { + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var length = (int)_targetOffsetPathLength; + if (length < 0) + { + length = -length; + } + + return length; + } + } + + public bool IsEncoded + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return (int)_targetOffsetPathLength < 0 ? true : false; + } + } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpCharacters.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpCharacters.cs index 341bd00407..8e4b5ce376 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpCharacters.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpCharacters.cs @@ -166,13 +166,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe static int IndexOfInvalidTokenChar(byte* s, int length) + public static int IndexOfInvalidTokenChar(ReadOnlySpan span) { var token = _token; - for (var i = 0; i < length; i++) + for (var i = 0; i < span.Length; i++) { - var c = s[i]; + var c = span[i]; if (c >= (uint)token.Length || !token[c]) { return i; diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs index 9cd8dce014..cb59fa8aff 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Buffers.Binary; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -50,38 +51,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure } } - private static unsafe ulong GetAsciiStringAsLong(string str) + private static ulong GetAsciiStringAsLong(string str) { Debug.Assert(str.Length == 8, "String must be exactly 8 (ASCII) characters long."); var bytes = Encoding.ASCII.GetBytes(str); - fixed (byte* ptr = &bytes[0]) - { - return *(ulong*)ptr; - } + return BinaryPrimitives.ReadUInt64LittleEndian(bytes); } - private static unsafe uint GetAsciiStringAsInt(string str) + private static uint GetAsciiStringAsInt(string str) { Debug.Assert(str.Length == 4, "String must be exactly 4 (ASCII) characters long."); var bytes = Encoding.ASCII.GetBytes(str); - - fixed (byte* ptr = &bytes[0]) - { - return *(uint*)ptr; - } + return BinaryPrimitives.ReadUInt32LittleEndian(bytes); } - private static unsafe ulong GetMaskAsLong(byte[] bytes) + private static ulong GetMaskAsLong(byte[] bytes) { Debug.Assert(bytes.Length == 8, "Mask must be exactly 8 bytes long."); - fixed (byte* ptr = bytes) - { - return *(ulong*)ptr; - } + return BinaryPrimitives.ReadUInt64LittleEndian(bytes); } // The same as GetAsciiStringNonNullCharacters but throws BadRequest @@ -142,7 +133,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure } } - private static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this Span span) + private static string GetAsciiOrUTF8StringNonNullCharacters(this Span span) => GetAsciiOrUTF8StringNonNullCharacters((ReadOnlySpan)span); public static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this ReadOnlySpan span) @@ -251,43 +242,38 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure /// To optimize performance the GET method will be checked first. /// /// true if the input matches a known string, false otherwise. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe bool GetKnownMethod(this Span span, out HttpMethod method, out int length) + public static bool GetKnownMethod(this ReadOnlySpan span, out HttpMethod method, out int length) { - fixed (byte* data = span) - { - method = GetKnownMethod(data, span.Length, out length); - return method != HttpMethod.Custom; - } + method = GetKnownMethod(span, out length); + return method != HttpMethod.Custom; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe HttpMethod GetKnownMethod(byte* data, int length, out int methodLength) + public static HttpMethod GetKnownMethod(this ReadOnlySpan span, out int methodLength) { methodLength = 0; - if (length < sizeof(uint)) + if (sizeof(uint) <= span.Length) { - return HttpMethod.Custom; - } - else if (*(uint*)data == _httpGetMethodInt) - { - methodLength = 3; - return HttpMethod.Get; - } - else if (length < sizeof(ulong)) - { - return HttpMethod.Custom; - } - else - { - var value = *(ulong*)data; - var key = GetKnownMethodIndex(value); - var x = _knownMethods[key]; - - if (x != null && (value & x.Item1) == x.Item2) + if (BinaryPrimitives.ReadUInt32LittleEndian(span) == _httpGetMethodInt) { - methodLength = x.Item4; - return x.Item3; + methodLength = 3; + return HttpMethod.Get; + } + else if (sizeof(ulong) <= span.Length) + { + var value = BinaryPrimitives.ReadUInt64LittleEndian(span); + var index = GetKnownMethodIndex(value); + var knownMehods = _knownMethods; + if ((uint)index < (uint)knownMehods.Length) + { + var knownMethod = _knownMethods[index]; + + if (knownMethod != null && (value & knownMethod.Item1) == knownMethod.Item2) + { + methodLength = knownMethod.Item4; + return knownMethod.Item3; + } + } } } @@ -386,21 +372,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure /// To optimize performance the HTTP/1.1 will be checked first. /// /// true if the input matches a known string, false otherwise. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe bool GetKnownVersion(this Span span, out HttpVersion knownVersion, out byte length) + public static bool GetKnownVersion(this ReadOnlySpan span, out HttpVersion knownVersion, out byte length) { - fixed (byte* data = span) + knownVersion = GetKnownVersionAndConfirmCR(span); + if (knownVersion != HttpVersion.Unknown) { - knownVersion = GetKnownVersion(data, span.Length); - if (knownVersion != HttpVersion.Unknown) - { - length = sizeof(ulong); - return true; - } - - length = 0; - return false; + length = sizeof(ulong); + return true; } + + length = 0; + return false; } /// @@ -415,28 +397,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure /// /// true if the input matches a known string, false otherwise. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe HttpVersion GetKnownVersion(byte* location, int length) + internal static HttpVersion GetKnownVersionAndConfirmCR(this ReadOnlySpan location) { - HttpVersion knownVersion; - var version = *(ulong*)location; - if (length < sizeof(ulong) + 1 || location[sizeof(ulong)] != (byte)'\r') + if (location.Length < sizeof(ulong)) { - knownVersion = HttpVersion.Unknown; - } - else if (version == _http11VersionLong) - { - knownVersion = HttpVersion.Http11; - } - else if (version == _http10VersionLong) - { - knownVersion = HttpVersion.Http10; + return HttpVersion.Unknown; } else { - knownVersion = HttpVersion.Unknown; + var version = BinaryPrimitives.ReadUInt64LittleEndian(location); + if (sizeof(ulong) >= (uint)location.Length || location[sizeof(ulong)] != (byte)'\r') + { + return HttpVersion.Unknown; + } + else if (version == _http11VersionLong) + { + return HttpVersion.Http11; + } + else if (version == _http10VersionLong) + { + return HttpVersion.Http10; + } } - return knownVersion; + return HttpVersion.Unknown; } /// diff --git a/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs index f60e3d9f91..666aa419d4 100644 --- a/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs @@ -95,7 +95,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.UTF8.GetBytes("\r\n\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined); + TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined); _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(headerValue, _http1Connection.RequestHeaders[headerName]); @@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(extendedAsciiEncoding.GetBytes("\r\n\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); + var exception = Assert.Throws(() => TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); } [Fact] @@ -128,7 +128,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; #pragma warning disable CS0618 // Type or member is obsolete - var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); + var exception = Assert.Throws(() => TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); #pragma warning restore CS0618 // Type or member is obsolete _transport.Input.AdvanceTo(_consumed, _examined); @@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; #pragma warning disable CS0618 // Type or member is obsolete - var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); + var exception = Assert.Throws(() => TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); #pragma warning restore CS0618 // Type or member is obsolete _transport.Input.AdvanceTo(_consumed, _examined); @@ -252,7 +252,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine1}\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - var takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined); + var takeMessageHeaders = TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined); _transport.Input.AdvanceTo(_consumed, _examined); Assert.True(takeMessageHeaders); @@ -264,7 +264,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine2}\r\n")); readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined); + takeMessageHeaders = TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined); _transport.Input.AdvanceTo(_consumed, _examined); Assert.True(takeMessageHeaders); @@ -389,7 +389,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(requestLineBytes); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - var returnValue = _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined); + var returnValue = TakeStartLine(readableBuffer, out _consumed, out _examined); _transport.Input.AdvanceTo(_consumed, _examined); Assert.True(returnValue); @@ -412,7 +412,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(requestLineBytes); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - var returnValue = _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined); + var returnValue = TakeStartLine(readableBuffer, out _consumed, out _examined); _transport.Input.AdvanceTo(_consumed, _examined); Assert.True(returnValue); @@ -426,7 +426,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { await _application.Output.WriteAsync(Encoding.ASCII.GetBytes("G")); - _http1Connection.ParseRequest((await _transport.Input.ReadAsync()).Buffer, out _consumed, out _examined); + ParseRequest((await _transport.Input.ReadAsync()).Buffer, out _consumed, out _examined); _transport.Input.AdvanceTo(_consumed, _examined); var expectedRequestHeadersTimeout = _serviceContext.ServerOptions.Limits.RequestHeadersTimeout.Ticks; @@ -443,7 +443,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; #pragma warning disable CS0618 // Type or member is obsolete - var exception = Assert.Throws(() => _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); + var exception = Assert.Throws(() => TakeStartLine(readableBuffer, out _consumed, out _examined)); #pragma warning restore CS0618 // Type or member is obsolete _transport.Input.AdvanceTo(_consumed, _examined); @@ -461,7 +461,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests #pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete - _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); + TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestTarget_Detail(target), exception.Message); @@ -477,7 +477,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests #pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete - _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); + TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestTarget_Detail(target.EscapeNonPrintable()), exception.Message); @@ -495,10 +495,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests #pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete - _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); + TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); - Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(requestLine.EscapeNonPrintable()), exception.Message); + Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(requestLine[..^1].EscapeNonPrintable()), exception.Message); } [Theory] @@ -513,7 +513,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests #pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete - _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); + TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestTarget_Detail(target.EscapeNonPrintable()), exception.Message); @@ -531,7 +531,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests #pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete - _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); + TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestTarget_Detail(target.EscapeNonPrintable()), exception.Message); @@ -548,7 +548,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests #pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete - _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); + TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(405, exception.StatusCode); @@ -795,7 +795,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests #pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete - _http1Connection.TakeStartLine(readableBuffer, out _consumed, out _examined)); + TakeStartLine(readableBuffer, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestTarget_Detail(string.Empty), exception.Message); @@ -971,6 +971,58 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("a=b"), ex.Message); } + + private bool TakeMessageHeaders(ReadOnlySequence readableBuffer, bool trailers, out SequencePosition consumed, out SequencePosition examined) + { + var reader = new SequenceReader(readableBuffer); + if (_http1Connection.TakeMessageHeaders(ref reader, trailers: trailers)) + { + consumed = reader.Position; + examined = reader.Position; + return true; + } + else + { + consumed = reader.Position; + examined = readableBuffer.End; + return false; + } + } + + private bool TakeStartLine(ReadOnlySequence readableBuffer, out SequencePosition consumed, out SequencePosition examined) + { + var reader = new SequenceReader(readableBuffer); + if (_http1Connection.TakeStartLine(ref reader)) + { + consumed = reader.Position; + examined = reader.Position; + return true; + } + else + { + consumed = reader.Position; + examined = readableBuffer.End; + return false; + } + } + + private bool ParseRequest(ReadOnlySequence readableBuffer, out SequencePosition consumed, out SequencePosition examined) + { + var reader = new SequenceReader(readableBuffer); + if (_http1Connection.ParseRequest(ref reader)) + { + consumed = reader.Position; + examined = reader.Position; + return true; + } + else + { + consumed = reader.Position; + examined = readableBuffer.End; + return false; + } + } + private static async Task WaitForCondition(TimeSpan timeout, Func condition) { const int MaxWaitLoop = 150; diff --git a/src/Servers/Kestrel/Core/test/HttpParserTests.cs b/src/Servers/Kestrel/Core/test/HttpParserTests.cs index f4497c60b5..058f170c91 100644 --- a/src/Servers/Kestrel/Core/test/HttpParserTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpParserTests.cs @@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(requestLine)); var requestHandler = new RequestHandler(); - Assert.True(parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined)); + Assert.True(ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined)); Assert.Equal(requestHandler.Method, expectedMethod); Assert.Equal(requestHandler.Version, expectedVersion); @@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(requestLine)); var requestHandler = new RequestHandler(); - Assert.False(parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined)); + Assert.False(ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined)); } [Theory] @@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(requestLine)); var requestHandler = new RequestHandler(); - Assert.False(parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined)); + Assert.False(ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined)); Assert.Equal(buffer.Start, consumed); Assert.True(buffer.Slice(examined).IsEmpty); @@ -93,9 +93,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests #pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete - parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined)); + ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined)); - Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(requestLine.EscapeNonPrintable()), exception.Message); + Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(requestLine[..^1].EscapeNonPrintable()), exception.Message); Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); } @@ -117,9 +117,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests #pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete - parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined)); + ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined)); - Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(method.EscapeNonPrintable() + @" / HTTP/1.1\x0D\x0A"), exception.Message); + Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(method.EscapeNonPrintable() + @" / HTTP/1.1\x0D"), exception.Message); Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); } @@ -141,7 +141,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests #pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete - parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined)); + ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined)); Assert.Equal(CoreStrings.FormatBadRequest_UnrecognizedHTTPVersion(httpVersion), exception.Message); Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, exception.StatusCode); @@ -363,7 +363,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests #pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete - parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined)); + ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined)); Assert.Equal("Invalid request line: ''", exception.Message); Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); @@ -374,7 +374,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests #pragma warning disable CS0618 // Type or member is obsolete exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete - parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined)); + ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined)); Assert.Equal(CoreStrings.FormatBadRequest_UnrecognizedHTTPVersion(string.Empty), exception.Message); Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, exception.StatusCode); @@ -403,7 +403,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Encoding.ASCII.GetBytes("/")); var requestHandler = new RequestHandler(); - var result = parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined); + var result = ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined); Assert.False(result); Assert.Equal(buffer.Start, consumed); @@ -422,7 +422,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var badHttpRequestException = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete { - parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined); + ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined); }); Assert.Equal(badHttpRequestException.StatusCode, StatusCodes.Status400BadRequest); @@ -480,6 +480,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.True(result); } + + private bool ParseRequestLine(IHttpParser parser, RequestHandler requestHandler, ReadOnlySequence readableBuffer, out SequencePosition consumed, out SequencePosition examined) + { + var reader = new SequenceReader(readableBuffer); + if (parser.ParseRequestLine(requestHandler, ref reader)) + { + consumed = reader.Position; + examined = reader.Position; + return true; + } + else + { + consumed = reader.Position; + examined = readableBuffer.End; + return false; + } + } + private void VerifyHeader( string headerName, string rawHeaderValue, @@ -565,6 +583,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests PathEncoded = pathEncoded; } + public void OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathLength targetPath, Span startLine) + { + var method = versionAndMethod.Method; + var version = versionAndMethod.Version; + var customMethod = startLine[..versionAndMethod.MethodEnd]; + var targetStart = targetPath.Offset; + var target = startLine[targetStart..]; + var path = target[..targetPath.Length]; + var query = target[targetPath.Length..]; + + Method = method != HttpMethod.Custom ? HttpUtilities.MethodToString(method) : customMethod.GetAsciiStringNonNullCharacters(); + Version = HttpUtilities.VersionToString(version); + RawTarget = target.GetAsciiStringNonNullCharacters(); + RawPath = path.GetAsciiStringNonNullCharacters(); + Query = query.GetAsciiStringNonNullCharacters(); + PathEncoded = targetPath.IsEncoded; + } + public void OnStaticIndexedHeader(int index) { throw new NotImplementedException(); diff --git a/src/Servers/Kestrel/Core/test/HttpUtilitiesTest.cs b/src/Servers/Kestrel/Core/test/HttpUtilitiesTest.cs index e8778dc390..fd9e9ce8c2 100644 --- a/src/Servers/Kestrel/Core/test/HttpUtilitiesTest.cs +++ b/src/Servers/Kestrel/Core/test/HttpUtilitiesTest.cs @@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { var expectedMethod = (HttpMethod)intExpectedMethod; // Arrange - var block = new Span(Encoding.ASCII.GetBytes(input)); + var block = new ReadOnlySpan(Encoding.ASCII.GetBytes(input)); // Act var result = block.GetKnownMethod(out var knownMethod, out var length); @@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { var version = (HttpVersion)intVersion; // Arrange - var block = new Span(Encoding.ASCII.GetBytes(input)); + var block = new ReadOnlySpan(Encoding.ASCII.GetBytes(input)); // Act var result = block.GetKnownVersion(out HttpVersion knownVersion, out var length); diff --git a/src/Servers/Kestrel/Core/test/KnownStringsTests.cs b/src/Servers/Kestrel/Core/test/KnownStringsTests.cs index 22388b4393..a78099ee06 100644 --- a/src/Servers/Kestrel/Core/test/KnownStringsTests.cs +++ b/src/Servers/Kestrel/Core/test/KnownStringsTests.cs @@ -75,7 +75,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests public void GetsKnownMethod(byte[] methodData, int intExpectedMethod, int expectedLength, bool expectedResult) { var expectedMethod = (HttpMethod)intExpectedMethod; - var data = new Span(methodData); + var data = new ReadOnlySpan(methodData); var result = data.GetKnownMethod(out var method, out var length); diff --git a/src/Servers/Kestrel/Core/test/StartLineTests.cs b/src/Servers/Kestrel/Core/test/StartLineTests.cs index c53e9f3980..a3163d5039 100644 --- a/src/Servers/Kestrel/Core/test/StartLineTests.cs +++ b/src/Servers/Kestrel/Core/test/StartLineTests.cs @@ -38,7 +38,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Assert.Null(Http1Connection.QueryString); var ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"POST {rawTarget} HTTP/1.1\r\n")); - Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _)); + var reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); // Equal the inputs. Assert.Equal(rawTarget, Http1Connection.RawTarget); @@ -64,7 +65,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Assert.Null(Http1Connection.QueryString); var ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n")); - Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _)); + var reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); // Equal the inputs. Assert.Equal(rawTarget, Http1Connection.RawTarget); @@ -91,7 +93,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Assert.Null(Http1Connection.QueryString); var ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n")); - Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _)); + var reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); // Equal the inputs. Assert.Equal(rawTarget, Http1Connection.RawTarget); @@ -117,7 +120,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Assert.Null(Http1Connection.QueryString); var ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"OPTIONS {rawTarget} HTTP/1.1\r\n")); - Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _)); + var reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); // Equal the inputs. Assert.Equal(rawTarget, Http1Connection.RawTarget); @@ -140,7 +144,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests var query = "?q=123&w=xyzw12"; Http1Connection.Reset(); var ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"POST {rawTarget} HTTP/1.1\r\n")); - Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _)); + var reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); // Equal the inputs. Assert.Equal(rawTarget, Http1Connection.RawTarget); @@ -184,7 +189,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { Http1Connection.Reset(); var ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n")); - Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _)); + var reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); var prevRequestUrl = Http1Connection.RawTarget; var prevPath = Http1Connection.Path; @@ -201,7 +207,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests // Parser decodes % encoding in place, so we need to recreate the ROS ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n")); - Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _)); + reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); // Equal the inputs. Assert.Equal(rawTarget, Http1Connection.RawTarget); @@ -234,7 +241,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Http1Connection.Reset(); ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n")); - Parser.ParseRequestLine(ParsingHandler, ros, out _, out _); + reader = new SequenceReader(ros); + Parser.ParseRequestLine(ParsingHandler, ref reader); // Equal the inputs. Assert.Equal(rawTarget, Http1Connection.RawTarget); @@ -275,7 +283,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { Http1Connection.Reset(); var ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n")); - Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _)); + var reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); var prevRequestUrl = Http1Connection.RawTarget; var prevPath = Http1Connection.Path; @@ -291,7 +300,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Assert.Null(Http1Connection.QueryString); ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n")); - Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _)); + reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); // Equal the inputs. Assert.Equal(rawTarget, Http1Connection.RawTarget); @@ -324,7 +334,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Http1Connection.Reset(); ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n")); - Parser.ParseRequestLine(ParsingHandler, ros, out _, out _); + reader = new SequenceReader(ros); + Parser.ParseRequestLine(ParsingHandler, ref reader); // Equal the inputs. Assert.Equal(rawTarget, Http1Connection.RawTarget); @@ -353,7 +364,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Http1Connection.Reset(); var ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"OPTIONS {rawTarget} HTTP/1.1\r\n")); - Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _)); + var reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); var prevRequestUrl = Http1Connection.RawTarget; var prevPath = Http1Connection.Path; @@ -369,7 +381,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Assert.Null(Http1Connection.QueryString); ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"OPTIONS {rawTarget} HTTP/1.1\r\n")); - Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _)); + reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); // Equal the inputs. Assert.Equal(rawTarget, Http1Connection.RawTarget); @@ -400,7 +413,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Http1Connection.Reset(); ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n")); - Parser.ParseRequestLine(ParsingHandler, ros, out _, out _); + reader = new SequenceReader(ros); + Parser.ParseRequestLine(ParsingHandler, ref reader); // Equal the inputs. Assert.Equal(rawTarget, Http1Connection.RawTarget); @@ -431,7 +445,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { Http1Connection.Reset(); var ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n")); - Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _)); + var reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); var prevRequestUrl = Http1Connection.RawTarget; var prevPath = Http1Connection.Path; @@ -447,7 +462,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Assert.Null(Http1Connection.QueryString); ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n")); - Assert.True(Parser.ParseRequestLine(ParsingHandler, ros, out _, out _)); + reader = new SequenceReader(ros); + Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader)); // Equal the inputs. Assert.Equal(rawTarget, Http1Connection.RawTarget); @@ -478,7 +494,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Http1Connection.Reset(); ros = new ReadOnlySequence(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n")); - Parser.ParseRequestLine(ParsingHandler, ros, out _, out _); + reader = new SequenceReader(ros); + Parser.ParseRequestLine(ParsingHandler, ref reader); // Equal the inputs. Assert.Equal(rawTarget, Http1Connection.RawTarget); diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs index d54f4a879f..febd2bdad3 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs @@ -79,14 +79,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance private void ParseData() { - if (!_parser.ParseRequestLine(new Adapter(this), _buffer, out var consumed, out var examined)) + var reader = new SequenceReader(_buffer); + if (!_parser.ParseRequestLine(new Adapter(this), ref reader)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } - _buffer = _buffer.Slice(consumed, _buffer.End); - var reader = new SequenceReader(_buffer); - if (!_parser.ParseHeaders(new Adapter(this), ref reader)) { ErrorUtilities.ThrowInvalidRequestHeaders(); @@ -112,8 +110,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance public void OnHeadersComplete(bool endStream) => RequestHandler.Connection.OnHeadersComplete(); - public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) - => RequestHandler.Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded); + public void OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathLength targetPath, Span startLine) + => RequestHandler.Connection.OnStartLine(versionAndMethod, targetPath, startLine); public void OnStaticIndexedHeader(int index) { diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs index bb103df34e..a8ffcf4212 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs @@ -77,12 +77,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance { _http1Connection.Reset(); - if (!_http1Connection.TakeStartLine(_buffer, out var consumed, out var examined)) + var reader = new SequenceReader(_buffer); + if (!_http1Connection.TakeStartLine(ref reader)) { ErrorUtilities.ThrowInvalidRequestLine(); } - if (!_http1Connection.TakeMessageHeaders(_buffer, trailers: false, out consumed, out examined)) + if (!_http1Connection.TakeMessageHeaders(ref reader, trailers: false)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } @@ -92,7 +93,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance { _http1Connection.Reset(); - if (!_http1Connection.TakeStartLine(_buffer, out var consumed, out var examined)) + var reader = new SequenceReader(_buffer); + if (!_http1Connection.TakeStartLine(ref reader)) { ErrorUtilities.ThrowInvalidRequestLine(); } @@ -102,7 +104,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance { _http1Connection.Reset(); - if (!_http1Connection.TakeMessageHeaders(_buffer, trailers: false, out var consumed, out var examined)) + var reader = new SequenceReader(_buffer); + if (!_http1Connection.TakeMessageHeaders(ref reader, trailers: false)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs index 8a38f97709..b332b3958c 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs @@ -10,7 +10,7 @@ using HttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMe namespace Microsoft.AspNetCore.Server.Kestrel.Performance { - internal class HttpParserBenchmark : IHttpRequestLineHandler, IHttpHeadersHandler + public class HttpParserBenchmark : IHttpRequestLineHandler, IHttpHeadersHandler { private readonly HttpParser _parser = new HttpParser(); @@ -63,21 +63,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance private void ParseData() { - if (!_parser.ParseRequestLine(new Adapter(this), _buffer, out var consumed, out var examined)) + var reader = new SequenceReader(_buffer); + if (!_parser.ParseRequestLine(new Adapter(this), ref reader)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } - _buffer = _buffer.Slice(consumed, _buffer.End); - - var reader = new SequenceReader(_buffer); if (!_parser.ParseHeaders(new Adapter(this), ref reader)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } } - public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) + public void OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathLength targetPath, Span startLine) { } @@ -114,8 +112,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance public void OnHeadersComplete(bool endStream) => RequestHandler.OnHeadersComplete(endStream); - public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) - => RequestHandler.OnStartLine(method, version, target, path, query, customMethod, pathEncoded); + public void OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathLength targetPath, Span startLine) + => RequestHandler.OnStartLine(versionAndMethod, targetPath, startLine); public void OnStaticIndexedHeader(int index) { diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/KnownStringsBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/KnownStringsBenchmark.cs index 69bf2d5adb..cefde8b71c 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/KnownStringsBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/KnownStringsBenchmark.cs @@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance return GetKnownMethod(data); } - private int GetKnownMethod(Span data) + private int GetKnownMethod(ReadOnlySpan data) { int len = 0; HttpMethod method; @@ -129,7 +129,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance { int len = 0; HttpVersion version; - Span data = _version; + ReadOnlySpan data = _version; for (int i = 0; i < loops; i++) { data.GetKnownVersion(out version, out var length); diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/NullParser.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/NullParser.cs index 53bae7b1b5..279fb965ce 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/NullParser.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/NullParser.cs @@ -3,7 +3,6 @@ using System; using System.Buffers; -using System.Net.Http; using System.Text; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using HttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod; @@ -13,7 +12,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance internal class NullParser : IHttpParser where TRequestHandler : struct, IHttpHeadersHandler, IHttpRequestLineHandler { private readonly byte[] _startLine = Encoding.ASCII.GetBytes("GET /plaintext HTTP/1.1\r\n"); - private readonly byte[] _target = Encoding.ASCII.GetBytes("/plaintext"); private readonly byte[] _hostHeaderName = Encoding.ASCII.GetBytes("Host"); private readonly byte[] _hostHeaderValue = Encoding.ASCII.GetBytes("www.example.com"); private readonly byte[] _acceptHeaderName = Encoding.ASCII.GetBytes("Accept"); @@ -33,18 +31,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance return true; } - public bool ParseRequestLine(TRequestHandler handler, in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined) + public bool ParseRequestLine(TRequestHandler handler, ref SequenceReader reader) { - handler.OnStartLine(HttpMethod.Get, - HttpVersion.Http11, - new Span(_target), - new Span(_target), - Span.Empty, - Span.Empty, - false); + Span startLine = _startLine; - consumed = buffer.Start; - examined = buffer.End; + handler.OnStartLine( + new HttpVersionAndMethod(HttpMethod.Get, 3) { Version = HttpVersion.Http11 }, + new TargetOffsetPathLength(3, startLine.Length - 3, false), + startLine); return true; } diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs index 9e0f842fc1..8941b76f6d 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs @@ -147,25 +147,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance } var readableBuffer = awaitable.GetAwaiter().GetResult().Buffer; + var reader = new SequenceReader(readableBuffer); do { Http1Connection.Reset(); - if (!Http1Connection.TakeStartLine(readableBuffer, out var consumed, out var examined)) + if (!Http1Connection.TakeStartLine(ref reader)) { ErrorUtilities.ThrowInvalidRequestLine(); } - readableBuffer = readableBuffer.Slice(consumed); - - if (!Http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out consumed, out examined)) + if (!Http1Connection.TakeMessageHeaders(ref reader, trailers: false)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } - - readableBuffer = readableBuffer.Slice(consumed); } - while (readableBuffer.Length > 0); + while (!reader.End); Pipe.Reader.AdvanceTo(readableBuffer.End); } @@ -183,23 +180,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance var result = awaitable.GetAwaiter().GetResult(); var readableBuffer = result.Buffer; + var reader = new SequenceReader(readableBuffer); Http1Connection.Reset(); - if (!Http1Connection.TakeStartLine(readableBuffer, out var consumed, out var examined)) + if (!Http1Connection.TakeStartLine(ref reader)) { ErrorUtilities.ThrowInvalidRequestLine(); } - Pipe.Reader.AdvanceTo(consumed, examined); + Pipe.Reader.AdvanceTo(reader.Position, reader.Position); result = Pipe.Reader.ReadAsync().GetAwaiter().GetResult(); readableBuffer = result.Buffer; + reader = new SequenceReader(readableBuffer); - if (!Http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out consumed, out examined)) + if (!Http1Connection.TakeMessageHeaders(ref reader, trailers: false)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } - Pipe.Reader.AdvanceTo(consumed, examined); + Pipe.Reader.AdvanceTo(reader.Position, reader.Position); } while (true); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs index f6cbc2f0a9..58ee057824 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs @@ -238,7 +238,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests foreach (var requestLine in HttpParsingData.RequestLineInvalidData) { - data.Add(requestLine, CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(requestLine.EscapeNonPrintable())); + data.Add(requestLine, CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(requestLine[..^1].EscapeNonPrintable())); } foreach (var target in HttpParsingData.TargetWithEncodedNullCharData)