diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index bc6757b30e..c8a5e20033 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -1216,15 +1216,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http Log.ApplicationError(ConnectionId, ex); } - public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod) + public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) { // 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" string requestUrlPath; string rawTarget; - var needDecode = path.IndexOf(BytePercentage) >= 0; - if (needDecode) + if (pathEncoded) { // Read raw target before mutating memory. rawTarget = target.GetAsciiStringNonNullCharacters(); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IHttpRequestLineHandler.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IHttpRequestLineHandler.cs index ddb6473882..83481002b3 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IHttpRequestLineHandler.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IHttpRequestLineHandler.cs @@ -7,6 +7,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { public interface IHttpRequestLineHandler { - void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod); + void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/KestrelHttpParser.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/KestrelHttpParser.cs index 79ecd4b9f0..02363a2325 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/KestrelHttpParser.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/KestrelHttpParser.cs @@ -34,219 +34,130 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http consumed = buffer.Start; examined = buffer.End; - ReadCursor end; - Span span; - - // If the buffer is a single span then use it to find the LF - if (buffer.IsSingleSpan) + // Prepare the first span + var span = buffer.First.Span; + var lineIndex = span.IndexOfVectorized(ByteLF); + if (lineIndex >= 0) { - var startLineSpan = buffer.First.Span; - var lineIndex = startLineSpan.IndexOfVectorized(ByteLF); - - if (lineIndex == -1) - { - return false; - } - - end = buffer.Move(consumed, lineIndex + 1); - span = startLineSpan.Slice(0, lineIndex + 1); + consumed = buffer.Move(consumed, lineIndex + 1); + span = span.Slice(0, lineIndex + 1); } - else + else if (buffer.IsSingleSpan || !TryGetNewLineSpan(ref buffer, ref span, out consumed)) { - var start = buffer.Start; - if (ReadCursorOperations.Seek(start, buffer.End, out end, ByteLF) == -1) - { - return false; - } - - // Move 1 byte past the \n - end = buffer.Move(end, 1); - var startLineBuffer = buffer.Slice(start, end); - - span = startLineBuffer.ToSpan(); + // No request line end + return false; } - var pathStart = -1; - var queryStart = -1; - var queryEnd = -1; - var pathEnd = -1; - var versionStart = -1; - - var httpVersion = HttpVersion.Unknown; - HttpMethod method; - Span customMethod; - var i = 0; - var length = span.Length; - var done = false; - + // Fix and parse the span fixed (byte* data = &span.DangerousGetPinnableReference()) { - switch (StartLineState.KnownMethod) + ParseRequestLine(handler, data, span.Length); + } + + examined = consumed; + return true; + } + + private unsafe void ParseRequestLine(T handler, byte* data, int length) where T : IHttpRequestLineHandler + { + int offset; + Span customMethod; + // Get Method and set the offset + var method = HttpUtilities.GetKnownMethod(data, length, out offset); + if (method == HttpMethod.Custom) + { + customMethod = GetUnknownMethod(data, length, out offset); + } + + // Skip space + offset++; + + byte ch = 0; + // Target = Path and Query + var pathEncoded = false; + var pathStart = -1; + for (; offset < length; offset++) + { + ch = data[offset]; + if (ch == ByteSpace) { - case StartLineState.KnownMethod: - if (span.GetKnownMethod(out method, out var methodLength)) - { - // Update the index, current char, state and jump directly - // to the next state - i += methodLength + 1; + if (pathStart == -1) + { + // Empty path is illegal + RejectRequestLine(data, length); + } - goto case StartLineState.Path; - } - goto case StartLineState.UnknownMethod; + break; + } + else if (ch == ByteQuestionMark) + { + if (pathStart == -1) + { + // Empty path is illegal + RejectRequestLine(data, length); + } - case StartLineState.UnknownMethod: - for (; i < length; i++) - { - var ch = data[i]; + break; + } + else if (ch == BytePercentage) + { + if (pathStart == -1) + { + // Path starting with % is illegal + RejectRequestLine(data, length); + } - if (ch == ByteSpace) - { - customMethod = span.Slice(0, i); - - if (customMethod.Length == 0) - { - RejectRequestLine(span); - } - // Consume space - i++; - - goto case StartLineState.Path; - } - - if (!IsValidTokenChar((char)ch)) - { - RejectRequestLine(span); - } - } - - break; - case StartLineState.Path: - for (; i < length; i++) - { - var ch = data[i]; - if (ch == ByteSpace) - { - pathEnd = i; - - if (pathStart == -1) - { - // Empty path is illegal - RejectRequestLine(span); - } - - // No query string found - queryStart = queryEnd = i; - - // Consume space - i++; - - goto case StartLineState.KnownVersion; - } - else if (ch == ByteQuestionMark) - { - pathEnd = i; - - if (pathStart == -1) - { - // Empty path is illegal - RejectRequestLine(span); - } - - queryStart = i; - goto case StartLineState.QueryString; - } - else if (ch == BytePercentage) - { - if (pathStart == -1) - { - RejectRequestLine(span); - } - } - - if (pathStart == -1) - { - pathStart = i; - } - } - break; - case StartLineState.QueryString: - for (; i < length; i++) - { - var ch = data[i]; - if (ch == ByteSpace) - { - queryEnd = i; - - // Consume space - i++; - - goto case StartLineState.KnownVersion; - } - } - break; - case StartLineState.KnownVersion: - // REVIEW: We don't *need* to slice here but it makes the API - // nicer, slicing should be free :) - if (span.Slice(i).GetKnownVersion(out httpVersion, out var versionLenght)) - { - // Update the index, current char, state and jump directly - // to the next state - i += versionLenght + 1; - goto case StartLineState.NewLine; - } - - versionStart = i; - - goto case StartLineState.UnknownVersion; - - case StartLineState.UnknownVersion: - for (; i < length; i++) - { - var ch = data[i]; - if (ch == ByteCR) - { - var versionSpan = span.Slice(versionStart, i - versionStart); - - if (versionSpan.Length == 0) - { - RejectRequestLine(span); - } - else - { - RejectRequest(RequestRejectionReason.UnrecognizedHTTPVersion, - versionSpan.GetAsciiStringEscaped(32)); - } - } - } - break; - case StartLineState.NewLine: - if (data[i] != ByteLF) - { - RejectRequestLine(span); - } - i++; - - goto case StartLineState.Complete; - case StartLineState.Complete: - done = true; - break; + pathEncoded = true; + } + else if (pathStart == -1) + { + pathStart = offset; } } - if (!done) + if (pathStart == -1) { - RejectRequestLine(span); + // End of path not found + RejectRequestLine(data, length); } - var pathBuffer = span.Slice(pathStart, pathEnd - pathStart); - var targetBuffer = span.Slice(pathStart, queryEnd - pathStart); - var query = span.Slice(queryStart, queryEnd - queryStart); + var pathBuffer = new Span(data + pathStart, offset - pathStart); - handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod); + var queryStart = offset; + // Query string + if (ch == ByteQuestionMark) + { + // We have a query string + for (; offset < length; offset++) + { + ch = data[offset]; + if (ch == ByteSpace) + { + break; + } + } + } - consumed = end; - examined = consumed; - return true; + var targetBuffer = new Span(data + pathStart, offset - pathStart); + var query = new Span(data + queryStart, offset - queryStart); + + // Consume space + offset++; + + // Version + var httpVersion = HttpUtilities.GetKnownVersion(data + offset, length - offset); + if (httpVersion == HttpVersion.Unknown) + { + RejectUnknownVersion(data, length, offset); + } + + // After version 8 bytes and cr 1 byte, expect lf + if (data[offset + 8 + 1] != ByteLF) + { + RejectRequestLine(data, length); + } + + handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod, pathEncoded); } public unsafe bool ParseHeaders(T handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes) where T : IHttpHeadersHandler @@ -502,6 +413,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http return true; } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool TryGetNewLineSpan(ref ReadableBuffer buffer, ref Span span, out ReadCursor end) + { + var start = buffer.Start; + if (ReadCursorOperations.Seek(start, buffer.End, out end, ByteLF) != -1) + { + // Move 1 byte past the \n + end = buffer.Move(end, 1); + span = buffer.Slice(start, end).ToSpan(); + return true; + } + return false; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private unsafe Span GetUnknownMethod(byte* data, int length, out int methodLength) + { + methodLength = 0; + for (var i = 0; i < length; i++) + { + var ch = data[i]; + + if (ch == ByteSpace) + { + if (i == 0) + { + RejectRequestLine(data, length); + } + + methodLength = i; + break; + } + else if (!IsValidTokenChar((char)ch)) + { + RejectRequestLine(data, length); + } + } + + return new Span(data, methodLength); + } + private static bool IsValidTokenChar(char c) { // Determines if a character is valid as a 'token' as defined in the @@ -532,9 +485,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http throw BadHttpRequestException.GetException(reason); } - public static void RejectRequest(RequestRejectionReason reason, string value) + private unsafe void RejectUnknownVersion(byte* data, int length, int versionStart) { - throw BadHttpRequestException.GetException(reason, value); + throw GetRejectUnknownVersion(data, length, versionStart); + } + + private unsafe void RejectRequestLine(byte* data, int length) + { + throw GetRejectRequestLineException(new Span(data, length)); } private void RejectRequestLine(Span span) @@ -549,6 +507,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http Log.IsEnabled(LogLevel.Information) ? span.GetAsciiStringEscaped(MaxRequestLineError) : string.Empty); } + private unsafe BadHttpRequestException GetRejectUnknownVersion(byte* data, int length, int versionStart) + { + var span = new Span(data, length); + length -= versionStart; + for (var i = 0; i < length; i++) + { + var ch = span[i + versionStart]; + if (ch == ByteCR) + { + if (i == 0) + { + return GetRejectRequestLineException(span); + } + else + { + return BadHttpRequestException.GetException(RequestRejectionReason.UnrecognizedHTTPVersion, + span.Slice(versionStart, i).GetAsciiStringEscaped(32)); + } + } + } + + return GetRejectRequestLineException(span); + } + private unsafe void RejectRequestHeader(byte* headerLine, int length) { RejectRequestHeader(new Span(headerLine, length)); @@ -578,26 +560,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http // https://github.com/dotnet/coreclr/issues/7459#issuecomment-253965670 return Vector.AsVectorByte(new Vector(vectorByte * 0x01010101u)); } - - private enum HeaderState - { - Name, - Whitespace, - ExpectValue, - ExpectNewLine, - Complete - } - - private enum StartLineState - { - KnownMethod, - UnknownMethod, - Path, - QueryString, - KnownVersion, - UnknownVersion, - NewLine, - Complete - } } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/HttpUtilities.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/HttpUtilities.cs index f93ceafc00..182d424eb9 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/HttpUtilities.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/HttpUtilities.cs @@ -147,34 +147,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure /// /// true if the input matches a known string, false otherwise. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetKnownMethod(this Span span, out HttpMethod method, out int length) + public static unsafe bool GetKnownMethod(this Span span, out HttpMethod method, out int length) { - if (span.TryRead(out var possiblyGet)) + fixed (byte* data = &span.DangerousGetPinnableReference()) { - if (possiblyGet == _httpGetMethodInt) - { - length = 3; - method = HttpMethod.Get; - return true; - } + method = GetKnownMethod(data, span.Length, out length); + return method != HttpMethod.Custom; } + } - if (span.TryRead(out var value)) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal unsafe static HttpMethod GetKnownMethod(byte* data, int length, out int methodLength) + { + methodLength = 0; + if (length < sizeof(uint)) { + 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; foreach (var x in _knownMethods) { if ((value & x.Item1) == x.Item2) { - method = x.Item3; - length = x.Item4; - return true; + methodLength = x.Item4; + return x.Item3; } } } - method = HttpMethod.Custom; - length = 0; - return false; + return HttpMethod.Custom; } /// @@ -189,36 +201,56 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure /// /// true if the input matches a known string, false otherwise. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetKnownVersion(this Span span, out HttpVersion knownVersion, out byte length) + public static unsafe bool GetKnownVersion(this Span span, out HttpVersion knownVersion, out byte length) { - if (span.TryRead(out var version)) + fixed (byte* data = &span.DangerousGetPinnableReference()) { - if (version == _http11VersionLong) + knownVersion = GetKnownVersion(data, span.Length); + if (knownVersion != HttpVersion.Unknown) { length = sizeof(ulong); - knownVersion = HttpVersion.Http11; - } - else if (version == _http10VersionLong) - { - length = sizeof(ulong); - knownVersion = HttpVersion.Http10; - } - else - { - length = 0; - knownVersion = HttpVersion.Unknown; - return false; - } - - if (span[sizeof(ulong)] == (byte)'\r') - { return true; } + + length = 0; + return false; + } + } + + /// + /// Checks 9 bytes from correspond to a known HTTP version. + /// + /// + /// A "known HTTP version" Is is either HTTP/1.0 or HTTP/1.1. + /// Since those fit in 8 bytes, they can be optimally looked up by reading those bytes as a long. Once + /// in that format, it can be checked against the known versions. + /// The Known versions will be checked with the required '\r'. + /// To optimize performance the HTTP/1.1 will be checked first. + /// + /// true if the input matches a known string, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal unsafe static HttpVersion GetKnownVersion(byte* location, int length) + { + HttpVersion knownVersion; + var version = *(ulong*)location; + if (length < sizeof(ulong) + 1 || location[sizeof(ulong)] != (byte)'\r') + { + knownVersion = HttpVersion.Unknown; + } + else if (version == _http11VersionLong) + { + knownVersion = HttpVersion.Http11; + } + else if (version == _http10VersionLong) + { + knownVersion = HttpVersion.Http10; + } + else + { + knownVersion = HttpVersion.Unknown; } - knownVersion = HttpVersion.Unknown; - length = 0; - return false; + return knownVersion; } public static string VersionToString(HttpVersion httpVersion) diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/FrameParsingOverhead.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/FrameParsingOverhead.cs index acb39238bc..3c077d6bf5 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/FrameParsingOverhead.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/FrameParsingOverhead.cs @@ -119,7 +119,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance public bool ParseRequestLine(T handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined) where T : IHttpRequestLineHandler { - handler.OnStartLine(HttpMethod.Get, HttpVersion.Http11, new Span(_target), new Span(_target), Span.Empty, Span.Empty); + handler.OnStartLine(HttpMethod.Get, HttpVersion.Http11, new Span(_target), new Span(_target), Span.Empty, Span.Empty, false); consumed = buffer.Start; examined = buffer.End; diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/KestrelHttpParser.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/KestrelHttpParser.cs index f77346ee8b..a4da66cbe6 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/KestrelHttpParser.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/KestrelHttpParser.cs @@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance } } - public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod) + public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) { } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/HttpParserTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/HttpParserTests.cs index 6ad29a68cb..4605f5d8a0 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/HttpParserTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/HttpParserTests.cs @@ -49,14 +49,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests It.IsAny>(), It.IsAny>(), It.IsAny>(), - It.IsAny>())) - .Callback, Span, Span, Span>((method, version, target, path, query, customMethod) => + It.IsAny>(), + It.IsAny())) + .Callback, Span, Span, Span, bool>((method, version, target, path, query, customMethod, pathEncoded) => { parsedMethod = method != HttpMethod.Custom ? HttpUtilities.MethodToString(method) : customMethod.GetAsciiStringNonNullCharacters(); parsedVersion = HttpUtilities.VersionToString(version); parsedRawTarget = target.GetAsciiStringNonNullCharacters(); parsedRawPath = path.GetAsciiStringNonNullCharacters(); parsedQuery = query.GetAsciiStringNonNullCharacters(); + pathEncoded = false; }); Assert.True(parser.ParseRequestLine(requestLineHandler.Object, buffer, out var consumed, out var examined));