// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.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 { public class HttpParser : IHttpParser where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler { private bool _showErrorDetails; public HttpParser() : this(showErrorDetails: true) { } public HttpParser(bool showErrorDetails) { _showErrorDetails = showErrorDetails; } // byte types don't have a data type annotation so we pre-cast them; to avoid in-place casts private const byte ByteCR = (byte)'\r'; private const byte ByteLF = (byte)'\n'; private const byte ByteColon = (byte)':'; private const byte ByteSpace = (byte)' '; private const byte ByteTab = (byte)'\t'; private const byte ByteQuestionMark = (byte)'?'; private const byte BytePercentage = (byte)'%'; public unsafe bool ParseRequestLine(TRequestHandler handler, in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined) { consumed = buffer.Start; examined = buffer.End; // Prepare the first span var span = buffer.First.Span; var lineIndex = span.IndexOf(ByteLF); if (lineIndex >= 0) { 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; } // Fix and parse the span fixed (byte* data = &MemoryMarshal.GetReference(span)) { ParseRequestLine(handler, data, span.Length); } examined = consumed; return true; } private unsafe void ParseRequestLine(TRequestHandler handler, byte* data, int length) { int offset; // Get Method and set the offset var method = HttpUtilities.GetKnownMethod(data, length, out offset); Span customMethod = method == HttpMethod.Custom ? GetUnknownMethod(data, length, out offset) : default; // 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) { if (pathStart == -1) { // Empty path is illegal RejectRequestLine(data, length); } break; } else if (ch == ByteQuestionMark) { if (pathStart == -1) { // Empty path is illegal RejectRequestLine(data, length); } break; } else if (ch == BytePercentage) { if (pathStart == -1) { // Path starting with % is illegal RejectRequestLine(data, length); } pathEncoded = true; } else if (pathStart == -1) { pathStart = offset; } } if (pathStart == -1) { // Start of path not found RejectRequestLine(data, length); } var pathBuffer = new Span(data + pathStart, offset - pathStart); // Query string var queryStart = offset; if (ch == ByteQuestionMark) { // We have a query string for (; offset < length; offset++) { ch = data[offset]; if (ch == ByteSpace) { break; } } } // 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); // Consume space offset++; // Version var httpVersion = HttpUtilities.GetKnownVersion(data + offset, length - offset); 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); } } // After version's 8 bytes and CR, expect LF if (data[offset + 8 + 1] != ByteLF) { RejectRequestLine(data, length); } handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod, pathEncoded); } public unsafe bool ParseHeaders(TRequestHandler handler, in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined, out int consumedBytes) { consumed = buffer.Start; examined = buffer.End; consumedBytes = 0; var bufferEnd = buffer.End; var reader = new BufferReader(buffer); var start = default(BufferReader); var done = false; try { while (!reader.End) { var span = reader.CurrentSegment; var remaining = span.Length - reader.CurrentSegmentIndex; fixed (byte* pBuffer = &MemoryMarshal.GetReference(span)) { while (remaining > 0) { var index = reader.CurrentSegmentIndex; int ch1; int ch2; var readAhead = false; // Fast path, we're still looking at the same span if (remaining >= 2) { ch1 = pBuffer[index]; ch2 = pBuffer[index + 1]; } else { // Store the reader before we look ahead 2 bytes (probably straddling // spans) start = reader; // Possibly split across spans ch1 = reader.Read(); ch2 = reader.Read(); readAhead = true; } if (ch1 == ByteCR) { // Check for final CRLF. if (ch2 == -1) { // Reset the reader so we don't consume anything reader = start; return false; } else if (ch2 == ByteLF) { // If we got 2 bytes from the span directly so skip ahead 2 so that // the reader's state matches what we expect if (!readAhead) { reader.Advance(2); } done = true; return true; } // Headers don't end in CRLF line. BadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestHeadersNoCRLF); } // We moved the reader so look ahead 2 bytes so reset both the reader // and the index if (readAhead) { reader = start; index = reader.CurrentSegmentIndex; } var endIndex = new Span(pBuffer + index, remaining).IndexOf(ByteLF); var length = 0; if (endIndex != -1) { length = endIndex + 1; var pHeader = pBuffer + index; TakeSingleHeader(pHeader, length, handler); } else { var current = reader.Position; var currentSlice = buffer.Slice(current, bufferEnd); var lineEndPosition = currentSlice.PositionOf(ByteLF); // Split buffers if (lineEndPosition == null) { // Not there return false; } var lineEnd = lineEndPosition.Value; // Make sure LF is included in lineEnd lineEnd = buffer.GetPosition(1, lineEnd); var headerSpan = buffer.Slice(current, lineEnd).ToSpan(); length = headerSpan.Length; fixed (byte* pHeader = &MemoryMarshal.GetReference(headerSpan)) { TakeSingleHeader(pHeader, length, handler); } // We're going to the next span after this since we know we crossed spans here // so mark the remaining as equal to the headerSpan so that we end up at 0 // on the next iteration remaining = length; } // Skip the reader forward past the header line reader.Advance(length); remaining -= length; } } } return false; } finally { consumed = reader.Position; consumedBytes = reader.ConsumedBytes; if (done) { examined = consumed; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private unsafe int FindEndOfName(byte* headerLine, int length) { var index = 0; var sawWhitespace = false; for (; index < length; index++) { var ch = headerLine[index]; if (ch == ByteColon) { break; } if (ch == ByteTab || ch == ByteSpace || ch == ByteCR) { sawWhitespace = true; } } if (index == length || sawWhitespace) { RejectRequestHeader(headerLine, length); } return index; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private unsafe void TakeSingleHeader(byte* headerLine, int length, TRequestHandler handler) { // Skip CR, LF from end position var valueEnd = length - 3; var nameEnd = FindEndOfName(headerLine, length); if (headerLine[valueEnd + 2] != ByteLF) { RejectRequestHeader(headerLine, length); } if (headerLine[valueEnd + 1] != ByteCR) { RejectRequestHeader(headerLine, length); } // Skip colon from value start var valueStart = nameEnd + 1; // Ignore start whitespace for (; valueStart < valueEnd; valueStart++) { var ch = headerLine[valueStart]; if (ch != ByteTab && ch != ByteSpace && ch != ByteCR) { break; } else if (ch == ByteCR) { RejectRequestHeader(headerLine, length); } } // Check for CR in value var valueBuffer = new Span(headerLine + valueStart, valueEnd - valueStart + 1); if (valueBuffer.IndexOf(ByteCR) >= 0) { RejectRequestHeader(headerLine, length); } // Ignore end whitespace var lengthChanged = false; for (; valueEnd >= valueStart; valueEnd--) { var ch = headerLine[valueEnd]; if (ch != ByteTab && ch != ByteSpace) { break; } lengthChanged = true; } if (lengthChanged) { // Length changed valueBuffer = new Span(headerLine + valueStart, valueEnd - valueStart + 1); } var nameBuffer = new Span(headerLine, nameEnd); handler.OnHeader(nameBuffer, valueBuffer); } [MethodImpl(MethodImplOptions.NoInlining)] private static bool TryGetNewLine(in ReadOnlySequence buffer, out SequencePosition found) { var byteLfPosition = buffer.PositionOf(ByteLF); if (byteLfPosition != null) { // Move 1 byte past the \n found = buffer.GetPosition(1, byteLfPosition.Value); return true; } found = default; 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 // HTTP spec: https://tools.ietf.org/html/rfc7230#section-3.2.6 return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' || c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; } [StackTraceHidden] private unsafe void RejectRequestLine(byte* requestLine, int length) => throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestLine, requestLine, length); [StackTraceHidden] private unsafe void RejectRequestHeader(byte* headerLine, int length) => throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestHeader, headerLine, length); [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) => BadHttpRequestException.GetException( reason, _showErrorDetails ? new Span(detail, length).GetAsciiStringEscaped(Constants.MaxExceptionDetailSize) : string.Empty); } }