510 lines
18 KiB
C#
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);
|
|
}
|
|
}
|