aspnetcore/src/Kestrel.Core/Internal/Http/HttpParser.cs

510 lines
18 KiB
C#

// 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<TRequestHandler> : IHttpParser<TRequestHandler> 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<byte> 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<byte> 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<byte>(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<byte>(data + pathStart, offset - pathStart);
var query = new Span<byte>(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<byte> 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<byte>(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<byte>(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<byte>(headerLine + valueStart, valueEnd - valueStart + 1);
}
var nameBuffer = new Span<byte>(headerLine, nameEnd);
handler.OnHeader(nameBuffer, valueBuffer);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool TryGetNewLine(in ReadOnlySequence<byte> 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<byte> 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<byte>(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<byte>(detail, length).GetAsciiStringEscaped(Constants.MaxExceptionDetailSize)
: string.Empty);
}
}