Add IHttpParser interface (#1414)

This commit is contained in:
Pavel Krymets 2017-03-01 11:55:36 -08:00 committed by GitHub
parent c56de066d3
commit d3694f085a
19 changed files with 1018 additions and 1071 deletions

View File

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26223.1
VisualStudioVersion = 15.0.26222.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7972A5D6-3385-4127-9277-428506DD44FF}"
ProjectSection(SolutionItems) = preProject

View File

@ -16,7 +16,6 @@ using System.Text.Utf8;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Adapter;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Internal;
@ -27,15 +26,8 @@ using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public abstract partial class Frame : IFrameControl
public abstract partial class Frame : IFrameControl, IHttpRequestLineHandler, IHttpHeadersHandler
{
// 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)'%';
private static readonly ArraySegment<byte> _endChunkedResponseBytes = CreateAsciiByteArraySegment("0\r\n\r\n");
@ -83,6 +75,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
protected long _responseBytesWritten;
private readonly IHttpParser _parser;
public Frame(ConnectionContext context)
{
ConnectionContext = context;
@ -92,6 +86,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
ServerOptions = context.ListenerContext.ServiceContext.ServerOptions;
_pathBase = context.ListenerContext.ListenOptions.PathBase;
_parser = context.ListenerContext.ServiceContext.HttpParser;
FrameControl = this;
_keepAliveMilliseconds = (long)ServerOptions.Limits.KeepAliveTimeout.TotalMilliseconds;
@ -173,7 +168,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
else
{
_httpVersion = Http.HttpVersion.Unset;
_httpVersion = Http.HttpVersion.Unknown;
}
}
@ -199,6 +194,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
private string _reasonPhrase;
public string ReasonPhrase
{
get
@ -349,7 +345,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
PathBase = null;
Path = null;
QueryString = null;
_httpVersion = Http.HttpVersion.Unset;
_httpVersion = Http.HttpVersion.Unknown;
StatusCode = StatusCodes.Status200OK;
ReasonPhrase = null;
@ -378,7 +374,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
_manuallySetRequestAbortToken = null;
_abortedCts = null;
_remainingRequestHeadersBytesAllowed = ServerOptions.Limits.MaxRequestHeadersTotalSize;
// Allow to bytes for \r\n after headers
_remainingRequestHeadersBytesAllowed = ServerOptions.Limits.MaxRequestHeadersTotalSize + 2;
_requestHeadersParsed = 0;
_responseBytesWritten = 0;
@ -982,15 +979,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
Output.ProducingComplete(end);
}
public unsafe bool TakeStartLine(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined)
public bool TakeStartLine(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined)
{
var start = buffer.Start;
var end = buffer.Start;
var bufferEnd = buffer.End;
examined = buffer.End;
consumed = buffer.Start;
if (_requestProcessingStatus == RequestProcessingStatus.RequestPending)
{
ConnectionControl.ResetTimeout(_requestHeadersTimeoutMilliseconds, TimeoutAction.SendTimeoutResponse);
@ -1001,305 +991,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
var overLength = false;
if (buffer.Length >= ServerOptions.Limits.MaxRequestLineSize)
{
bufferEnd = buffer.Move(start, ServerOptions.Limits.MaxRequestLineSize);
buffer = buffer.Slice(buffer.Start, ServerOptions.Limits.MaxRequestLineSize);
overLength = true;
}
if (ReadCursorOperations.Seek(start, bufferEnd, out end, ByteLF) == -1)
var result = _parser.ParseRequestLine(this, buffer, out consumed, out examined);
if (!result && overLength)
{
if (overLength)
{
RejectRequest(RequestRejectionReason.RequestLineTooLong);
}
else
{
return false;
}
RejectRequest(RequestRejectionReason.RequestLineTooLong);
}
const int stackAllocLimit = 512;
// Move 1 byte past the \n
end = buffer.Move(end, 1);
var startLineBuffer = buffer.Slice(start, end);
Span<byte> span;
if (startLineBuffer.IsSingleSpan)
{
// No copies, directly use the one and only span
span = startLineBuffer.First.Span;
}
else if (startLineBuffer.Length < stackAllocLimit)
{
// Multiple buffers and < stackAllocLimit, copy into a stack buffer
byte* stackBuffer = stackalloc byte[startLineBuffer.Length];
span = new Span<byte>(stackBuffer, startLineBuffer.Length);
startLineBuffer.CopyTo(span);
}
else
{
// We're not a single span here but we can use pooled arrays to avoid allocations in the rare case
span = new Span<byte>(new byte[startLineBuffer.Length]);
startLineBuffer.CopyTo(span);
}
var needDecode = false;
var pathStart = -1;
var queryStart = -1;
var queryEnd = -1;
var pathEnd = -1;
var versionStart = -1;
var queryString = "";
var httpVersion = "";
var method = "";
var state = StartLineState.KnownMethod;
fixed (byte* data = &span.DangerousGetPinnableReference())
{
var length = span.Length;
for (var i = 0; i < length; i++)
{
var ch = data[i];
switch (state)
{
case StartLineState.KnownMethod:
if (span.GetKnownMethod(out method))
{
// Update the index, current char, state and jump directly
// to the next state
i += method.Length + 1;
ch = data[i];
state = StartLineState.Path;
goto case StartLineState.Path;
}
state = StartLineState.UnknownMethod;
goto case StartLineState.UnknownMethod;
case StartLineState.UnknownMethod:
if (ch == ByteSpace)
{
method = span.Slice(0, i).GetAsciiString();
if (method == null)
{
RejectRequestLine(start, end);
}
state = StartLineState.Path;
}
else if (!IsValidTokenChar((char)ch))
{
RejectRequestLine(start, end);
}
break;
case StartLineState.Path:
if (ch == ByteSpace)
{
pathEnd = i;
if (pathStart == -1)
{
// Empty path is illegal
RejectRequestLine(start, end);
}
// No query string found
queryStart = queryEnd = i;
state = StartLineState.KnownVersion;
}
else if (ch == ByteQuestionMark)
{
pathEnd = i;
if (pathStart == -1)
{
// Empty path is illegal
RejectRequestLine(start, end);
}
queryStart = i;
state = StartLineState.QueryString;
}
else if (ch == BytePercentage)
{
if (pathStart == -1)
{
// Empty path is illegal
RejectRequestLine(start, end);
}
needDecode = true;
}
if (pathStart == -1)
{
pathStart = i;
}
break;
case StartLineState.QueryString:
if (ch == ByteSpace)
{
queryEnd = i;
state = StartLineState.KnownVersion;
queryString = span.Slice(queryStart, queryEnd - queryStart).GetAsciiString() ?? string.Empty;
}
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))
{
// Update the index, current char, state and jump directly
// to the next state
i += httpVersion.Length + 1;
ch = data[i];
state = StartLineState.NewLine;
goto case StartLineState.NewLine;
}
versionStart = i;
state = StartLineState.UnknownVersion;
goto case StartLineState.UnknownVersion;
case StartLineState.UnknownVersion:
if (ch == ByteCR)
{
var versionSpan = span.Slice(versionStart, i - versionStart);
if (versionSpan.Length == 0)
{
RejectRequestLine(start, end);
}
else
{
RejectRequest(RequestRejectionReason.UnrecognizedHTTPVersion, versionSpan.GetAsciiStringEscaped());
}
}
break;
case StartLineState.NewLine:
if (ch != ByteLF)
{
RejectRequestLine(start, end);
}
state = StartLineState.Complete;
break;
case StartLineState.Complete:
break;
default:
break;
}
}
}
if (state != StartLineState.Complete)
{
RejectRequestLine(start, end);
}
var pathBuffer = span.Slice(pathStart, pathEnd - pathStart);
var targetBuffer = span.Slice(pathStart, queryEnd - pathStart);
// 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;
if (needDecode)
{
// Read raw target before mutating memory.
rawTarget = targetBuffer.GetAsciiString() ?? string.Empty;
// URI was encoded, unescape and then parse as utf8
var pathSpan = pathBuffer;
int pathLength = UrlEncoder.Decode(pathSpan, pathSpan);
requestUrlPath = new Utf8String(pathSpan.Slice(0, pathLength)).ToString();
}
else
{
// URI wasn't encoded, parse as ASCII
requestUrlPath = pathBuffer.GetAsciiString() ?? string.Empty;
if (queryString.Length == 0)
{
// No need to allocate an extra string if the path didn't need
// decoding and there's no query string following it.
rawTarget = requestUrlPath;
}
else
{
rawTarget = targetBuffer.GetAsciiString() ?? string.Empty;
}
}
var normalizedTarget = PathNormalizer.RemoveDotSegments(requestUrlPath);
consumed = end;
examined = end;
Method = method;
QueryString = queryString;
RawTarget = rawTarget;
HttpVersion = httpVersion;
if (RequestUrlStartsWithPathBase(normalizedTarget, out bool caseMatches))
{
PathBase = caseMatches ? _pathBase : normalizedTarget.Substring(0, _pathBase.Length);
Path = normalizedTarget.Substring(_pathBase.Length);
}
else if (rawTarget[0] == '/') // check rawTarget since normalizedTarget can be "" or "/" after dot segment removal
{
Path = normalizedTarget;
}
else
{
Path = string.Empty;
PathBase = string.Empty;
QueryString = string.Empty;
}
return true;
}
private void RejectRequestLine(ReadCursor start, ReadCursor end)
{
const int MaxRequestLineError = 32;
RejectRequest(RequestRejectionReason.InvalidRequestLine,
Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxRequestLineError) : string.Empty);
}
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 == '~';
return result;
}
private bool RequestUrlStartsWithPathBase(string requestUrl, out bool caseMatches)
@ -1334,282 +1036,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return true;
}
public unsafe bool TakeMessageHeaders(ReadableBuffer buffer, FrameRequestHeaders requestHeaders, out ReadCursor consumed, out ReadCursor examined)
public bool TakeMessageHeaders(ReadableBuffer buffer, FrameRequestHeaders requestHeaders, out ReadCursor consumed, out ReadCursor examined)
{
consumed = buffer.Start;
examined = buffer.End;
var bufferEnd = buffer.End;
var reader = new ReadableBufferReader(buffer);
// Make sure the buffer is limited
var overLength = false;
bool overLength = false;
if (buffer.Length >= _remainingRequestHeadersBytesAllowed)
{
bufferEnd = buffer.Move(consumed, _remainingRequestHeadersBytesAllowed);
buffer = buffer.Slice(buffer.Start, _remainingRequestHeadersBytesAllowed);
// If we sliced it means the current buffer bigger than what we're
// allowed to look at
overLength = true;
}
while (true)
var result = _parser.ParseHeaders(this, buffer, out consumed, out examined, out var consumedBytes);
_remainingRequestHeadersBytesAllowed -= consumedBytes;
if (!result && overLength)
{
var start = reader;
int ch1 = reader.Take();
var ch2 = reader.Take();
if (ch1 == -1)
{
return false;
}
if (ch1 == ByteCR)
{
// Check for final CRLF.
if (ch2 == -1)
{
return false;
}
else if (ch2 == ByteLF)
{
consumed = reader.Cursor;
examined = consumed;
ConnectionControl.CancelTimeout();
return true;
}
// Headers don't end in CRLF line.
RejectRequest(RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence);
}
else if (ch1 == ByteSpace || ch1 == ByteTab)
{
RejectRequest(RequestRejectionReason.HeaderLineMustNotStartWithWhitespace);
}
// If we've parsed the max allowed numbers of headers and we're starting a new
// one, we've gone over the limit.
if (_requestHeadersParsed == ServerOptions.Limits.MaxRequestHeaderCount)
{
RejectRequest(RequestRejectionReason.TooManyHeaders);
}
// Reset the reader since we're not at the end of headers
reader = start;
if (ReadCursorOperations.Seek(consumed, bufferEnd, out var lineEnd, ByteLF) == -1)
{
// We didn't find a \n in the current buffer and we had to slice it so it's an issue
if (overLength)
{
RejectRequest(RequestRejectionReason.HeadersExceedMaxTotalSize);
}
else
{
return false;
}
}
const int stackAllocLimit = 512;
if (lineEnd != bufferEnd)
{
lineEnd = buffer.Move(lineEnd, 1);
}
var headerBuffer = buffer.Slice(consumed, lineEnd);
Span<byte> span;
if (headerBuffer.IsSingleSpan)
{
// No copies, directly use the one and only span
span = headerBuffer.First.Span;
}
else if (headerBuffer.Length < stackAllocLimit)
{
// Multiple buffers and < stackAllocLimit, copy into a stack buffer
byte* stackBuffer = stackalloc byte[headerBuffer.Length];
span = new Span<byte>(stackBuffer, headerBuffer.Length);
headerBuffer.CopyTo(span);
}
else
{
// We're not a single span here but we can use pooled arrays to avoid allocations in the rare case
span = new Span<byte>(new byte[headerBuffer.Length]);
headerBuffer.CopyTo(span);
}
var state = HeaderState.Name;
var nameStart = 0;
var nameEnd = -1;
var valueStart = -1;
var valueEnd = -1;
var nameHasWhitespace = false;
var previouslyWhitespace = false;
var headerLineLength = span.Length;
fixed (byte* data = &span.DangerousGetPinnableReference())
{
for (var i = 0; i < headerLineLength; i++)
{
var ch = data[i];
switch (state)
{
case HeaderState.Name:
if (ch == ByteColon)
{
if (nameHasWhitespace)
{
RejectRequest(RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName);
}
state = HeaderState.Whitespace;
nameEnd = i;
}
if (ch == ByteSpace || ch == ByteTab)
{
nameHasWhitespace = true;
}
break;
case HeaderState.Whitespace:
{
var whitespace = ch == ByteTab || ch == ByteSpace || ch == ByteCR;
if (!whitespace)
{
// Mark the first non whitespace char as the start of the
// header value and change the state to expect to the header value
valueStart = i;
state = HeaderState.ExpectValue;
}
// If we see a CR then jump to the next state directly
else if (ch == ByteCR)
{
state = HeaderState.ExpectValue;
goto case HeaderState.ExpectValue;
}
}
break;
case HeaderState.ExpectValue:
{
var whitespace = ch == ByteTab || ch == ByteSpace;
if (whitespace)
{
if (!previouslyWhitespace)
{
// If we see a whitespace char then maybe it's end of the
// header value
valueEnd = i;
}
}
else if (ch == ByteCR)
{
// If we see a CR and we haven't ever seen whitespace then
// this is the end of the header value
if (valueEnd == -1)
{
valueEnd = i;
}
// We never saw a non whitespace character before the CR
if (valueStart == -1)
{
valueStart = valueEnd;
}
state = HeaderState.ExpectNewLine;
}
else
{
// If we find a non whitespace char that isn't CR then reset the end index
valueEnd = -1;
}
previouslyWhitespace = whitespace;
}
break;
case HeaderState.ExpectNewLine:
if (ch != ByteLF)
{
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
}
state = HeaderState.Complete;
break;
default:
break;
}
}
}
if (state == HeaderState.Name)
{
RejectRequest(RequestRejectionReason.NoColonCharacterFoundInHeaderLine);
}
if (state == HeaderState.ExpectValue || state == HeaderState.Whitespace)
{
RejectRequest(RequestRejectionReason.MissingCRInHeaderLine);
}
if (state != HeaderState.Complete)
{
return false;
}
// Skip the reader forward past the header line
reader.Skip(headerLineLength);
// Before accepting the header line, we need to see at least one character
// > so we can make sure there's no space or tab
var next = reader.Peek();
// TODO: We don't need to reject the line here, we can use the state machine
// to store the fact that we're reading a header value
if (next == -1)
{
// If we can't see the next char then reject the entire line
return false;
}
if (next == ByteSpace || next == ByteTab)
{
// From https://tools.ietf.org/html/rfc7230#section-3.2.4:
//
// Historically, HTTP header field values could be extended over
// multiple lines by preceding each extra line with at least one space
// or horizontal tab (obs-fold). This specification deprecates such
// line folding except within the message/http media type
// (Section 8.3.1). A sender MUST NOT generate a message that includes
// line folding (i.e., that has any field-value that contains a match to
// the obs-fold rule) unless the message is intended for packaging
// within the message/http media type.
//
// A server that receives an obs-fold in a request message that is not
// within a message/http container MUST either reject the message by
// sending a 400 (Bad Request), preferably with a representation
// explaining that obsolete line folding is unacceptable, or replace
// each received obs-fold with one or more SP octets prior to
// interpreting the field value or forwarding the message downstream.
RejectRequest(RequestRejectionReason.HeaderValueLineFoldingNotSupported);
}
var nameBuffer = span.Slice(nameStart, nameEnd - nameStart);
var valueBuffer = span.Slice(valueStart, valueEnd - valueStart);
var value = valueBuffer.GetAsciiString() ?? string.Empty;
// Update the frame state only after we know there's no header line continuation
_remainingRequestHeadersBytesAllowed -= headerLineLength;
_requestHeadersParsed++;
requestHeaders.Append(nameBuffer, value);
consumed = reader.Cursor;
RejectRequest(RequestRejectionReason.HeadersExceedMaxTotalSize);
}
if (result)
{
ConnectionControl.CancelTimeout();
}
return result;
}
public bool StatusCanHaveBody(int statusCode)
@ -1750,25 +1201,81 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
ResponseStarted
}
private enum StartLineState
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod)
{
KnownMethod,
UnknownMethod,
Path,
QueryString,
KnownVersion,
UnknownVersion,
NewLine,
Complete
// 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)
{
// Read raw target before mutating memory.
rawTarget = target.GetAsciiString() ?? string.Empty;
// URI was encoded, unescape and then parse as utf8
int pathLength = UrlEncoder.Decode(path, path);
requestUrlPath = new Utf8String(path.Slice(0, pathLength)).ToString();
}
else
{
// URI wasn't encoded, parse as ASCII
requestUrlPath = path.GetAsciiString() ?? string.Empty;
if (query.Length == 0)
{
// No need to allocate an extra string if the path didn't need
// decoding and there's no query string following it.
rawTarget = requestUrlPath;
}
else
{
rawTarget = target.GetAsciiString() ?? string.Empty;
}
}
var normalizedTarget = PathNormalizer.RemoveDotSegments(requestUrlPath);
if (method != HttpMethod.Custom)
{
Method = HttpUtilities.MethodToString(method) ?? String.Empty;
}
else
{
Method = customMethod.GetAsciiString() ?? string.Empty;
}
QueryString = query.GetAsciiString() ?? string.Empty;
RawTarget = rawTarget;
HttpVersion = HttpUtilities.VersionToString(version);
if (RequestUrlStartsWithPathBase(normalizedTarget, out bool caseMatches))
{
PathBase = caseMatches ? _pathBase : normalizedTarget.Substring(0, _pathBase.Length);
Path = normalizedTarget.Substring(_pathBase.Length);
}
else if (rawTarget[0] == '/') // check rawTarget since normalizedTarget can be "" or "/" after dot segment removal
{
Path = normalizedTarget;
}
else
{
Path = string.Empty;
PathBase = string.Empty;
QueryString = string.Empty;
}
}
private enum HeaderState
public void OnHeader(Span<byte> name, Span<byte> value)
{
Name,
Whitespace,
ExpectValue,
ExpectNewLine,
Complete
_requestHeadersParsed++;
if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
{
RejectRequest(RequestRejectionReason.TooManyHeaders);
}
var valueString = value.GetAsciiString() ?? string.Empty;
FrameRequestHeaders.Append(name, valueString);
}
}
}

View File

@ -0,0 +1,20 @@
// 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.
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public enum HttpMethod: byte
{
Get,
Put,
Delete,
Post,
Head,
Trace,
Patch,
Connect,
Options,
Custom,
}
}

View File

@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public enum HttpVersion
{
Unset = -1,
Unknown = -1,
Http10 = 0,
Http11 = 1
}

View File

@ -0,0 +1,12 @@
// 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;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public interface IHttpHeadersHandler
{
void OnHeader(Span<byte> name, Span<byte> value);
}
}

View File

@ -0,0 +1,14 @@
// 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.IO.Pipelines;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public interface IHttpParser
{
bool ParseRequestLine<T>(T handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined) where T : IHttpRequestLineHandler;
bool ParseHeaders<T>(T handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes) where T : IHttpHeadersHandler;
}
}

View File

@ -0,0 +1,12 @@
// 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;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public interface IHttpRequestLineHandler
{
void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod);
}
}

View File

@ -0,0 +1,549 @@
// 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.IO.Pipelines;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public class KestrelHttpParser : IHttpParser
{
public KestrelHttpParser(IKestrelTrace log)
{
Log = log;
}
private IKestrelTrace Log { get; }
// 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<T>(T handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined) where T : IHttpRequestLineHandler
{
consumed = buffer.Start;
examined = buffer.End;
var start = buffer.Start;
ReadCursor end;
if (ReadCursorOperations.Seek(start, buffer.End, out end, ByteLF) == -1)
{
return false;
}
const int stackAllocLimit = 512;
// Move 1 byte past the \n
end = buffer.Move(end, 1);
var startLineBuffer = buffer.Slice(start, end);
Span<byte> span;
if (startLineBuffer.IsSingleSpan)
{
// No copies, directly use the one and only span
span = startLineBuffer.First.Span;
}
else if (startLineBuffer.Length < stackAllocLimit)
{
// Multiple buffers and < stackAllocLimit, copy into a stack buffer
byte* stackBuffer = stackalloc byte[startLineBuffer.Length];
span = new Span<byte>(stackBuffer, startLineBuffer.Length);
startLineBuffer.CopyTo(span);
}
else
{
// We're not a single span here but we can use pooled arrays to avoid allocations in the rare case
span = new Span<byte>(new byte[startLineBuffer.Length]);
startLineBuffer.CopyTo(span);
}
var pathStart = -1;
var queryStart = -1;
var queryEnd = -1;
var pathEnd = -1;
var versionStart = -1;
HttpVersion httpVersion = HttpVersion.Unknown;
HttpMethod method = HttpMethod.Custom;
Span<byte> customMethod;
var state = StartLineState.KnownMethod;
int i;
fixed (byte* data = &span.DangerousGetPinnableReference())
{
var length = span.Length;
for (i = 0; i < length; i++)
{
var ch = data[i];
switch (state)
{
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;
ch = data[i];
state = StartLineState.Path;
goto case StartLineState.Path;
}
state = StartLineState.UnknownMethod;
goto case StartLineState.UnknownMethod;
case StartLineState.UnknownMethod:
if (ch == ByteSpace)
{
customMethod = span.Slice(0, i);
if (customMethod.Length == 0)
{
RejectRequestLine(span);
}
state = StartLineState.Path;
}
else if (!IsValidTokenChar((char)ch))
{
RejectRequestLine(span);
}
break;
case StartLineState.Path:
if (ch == ByteSpace)
{
pathEnd = i;
if (pathStart == -1)
{
// Empty path is illegal
RejectRequestLine(span);
}
// No query string found
queryStart = queryEnd = i;
state = StartLineState.KnownVersion;
}
else if (ch == ByteQuestionMark)
{
pathEnd = i;
if (pathStart == -1)
{
// Empty path is illegal
RejectRequestLine(span);
}
queryStart = i;
state = StartLineState.QueryString;
}
else if (ch == BytePercentage)
{
if (pathStart == -1)
{
RejectRequestLine(span);
}
}
if (pathStart == -1)
{
pathStart = i;
}
break;
case StartLineState.QueryString:
if (ch == ByteSpace)
{
queryEnd = i;
state = 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;
ch = data[i];
state = StartLineState.NewLine;
goto case StartLineState.NewLine;
}
versionStart = i;
state = StartLineState.UnknownVersion;
goto case StartLineState.UnknownVersion;
case StartLineState.UnknownVersion:
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 (ch != ByteLF)
{
RejectRequestLine(span);
}
state = StartLineState.Complete;
break;
case StartLineState.Complete:
break;
}
}
}
if (state != StartLineState.Complete)
{
RejectRequestLine(span);
}
var pathBuffer = span.Slice(pathStart, pathEnd - pathStart);
var targetBuffer = span.Slice(pathStart, queryEnd - pathStart);
var query = span.Slice(queryStart, queryEnd - queryStart);
handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod);
consumed = buffer.Move(start, i);
examined = consumed;
return true;
}
public unsafe bool ParseHeaders<T>(T handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes) where T : IHttpHeadersHandler
{
consumed = buffer.Start;
examined = buffer.End;
consumedBytes = 0;
var bufferEnd = buffer.End;
var reader = new ReadableBufferReader(buffer);
while (true)
{
var start = reader;
int ch1 = reader.Take();
var ch2 = reader.Take();
if (ch1 == -1)
{
return false;
}
if (ch1 == ByteCR)
{
// Check for final CRLF.
if (ch2 == -1)
{
return false;
}
else if (ch2 == ByteLF)
{
consumed = reader.Cursor;
examined = consumed;
return true;
}
// Headers don't end in CRLF line.
RejectRequest(RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence);
}
else if (ch1 == ByteSpace || ch1 == ByteTab)
{
RejectRequest(RequestRejectionReason.HeaderLineMustNotStartWithWhitespace);
}
// Reset the reader since we're not at the end of headers
reader = start;
if (ReadCursorOperations.Seek(consumed, bufferEnd, out var lineEnd, ByteLF) == -1)
{
return false;
}
const int stackAllocLimit = 512;
if (lineEnd != bufferEnd)
{
lineEnd = buffer.Move(lineEnd, 1);
}
var headerBuffer = buffer.Slice(consumed, lineEnd);
Span<byte> span;
if (headerBuffer.IsSingleSpan)
{
// No copies, directly use the one and only span
span = headerBuffer.First.Span;
}
else if (headerBuffer.Length < stackAllocLimit)
{
// Multiple buffers and < stackAllocLimit, copy into a stack buffer
byte* stackBuffer = stackalloc byte[headerBuffer.Length];
span = new Span<byte>(stackBuffer, headerBuffer.Length);
headerBuffer.CopyTo(span);
}
else
{
// We're not a single span here but we can use pooled arrays to avoid allocations in the rare case
span = new Span<byte>(new byte[headerBuffer.Length]);
headerBuffer.CopyTo(span);
}
var state = HeaderState.Name;
var nameStart = 0;
var nameEnd = -1;
var valueStart = -1;
var valueEnd = -1;
var nameHasWhitespace = false;
var previouslyWhitespace = false;
var headerLineLength = span.Length;
fixed (byte* data = &span.DangerousGetPinnableReference())
{
for (var i = 0; i < headerLineLength; i++)
{
var ch = data[i];
switch (state)
{
case HeaderState.Name:
if (ch == ByteColon)
{
if (nameHasWhitespace)
{
RejectRequest(RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName);
}
state = HeaderState.Whitespace;
nameEnd = i;
}
if (ch == ByteSpace || ch == ByteTab)
{
nameHasWhitespace = true;
}
break;
case HeaderState.Whitespace:
{
var whitespace = ch == ByteTab || ch == ByteSpace || ch == ByteCR;
if (!whitespace)
{
// Mark the first non whitespace char as the start of the
// header value and change the state to expect to the header value
valueStart = i;
state = HeaderState.ExpectValue;
}
// If we see a CR then jump to the next state directly
else if (ch == ByteCR)
{
state = HeaderState.ExpectValue;
goto case HeaderState.ExpectValue;
}
}
break;
case HeaderState.ExpectValue:
{
var whitespace = ch == ByteTab || ch == ByteSpace;
if (whitespace)
{
if (!previouslyWhitespace)
{
// If we see a whitespace char then maybe it's end of the
// header value
valueEnd = i;
}
}
else if (ch == ByteCR)
{
// If we see a CR and we haven't ever seen whitespace then
// this is the end of the header value
if (valueEnd == -1)
{
valueEnd = i;
}
// We never saw a non whitespace character before the CR
if (valueStart == -1)
{
valueStart = valueEnd;
}
state = HeaderState.ExpectNewLine;
}
else
{
// If we find a non whitespace char that isn't CR then reset the end index
valueEnd = -1;
}
previouslyWhitespace = whitespace;
}
break;
case HeaderState.ExpectNewLine:
if (ch != ByteLF)
{
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
}
state = HeaderState.Complete;
break;
default:
break;
}
}
}
if (state == HeaderState.Name)
{
RejectRequest(RequestRejectionReason.NoColonCharacterFoundInHeaderLine);
}
if (state == HeaderState.ExpectValue || state == HeaderState.Whitespace)
{
RejectRequest(RequestRejectionReason.MissingCRInHeaderLine);
}
if (state != HeaderState.Complete)
{
return false;
}
// Skip the reader forward past the header line
reader.Skip(headerLineLength);
// Before accepting the header line, we need to see at least one character
// > so we can make sure there's no space or tab
var next = reader.Peek();
// TODO: We don't need to reject the line here, we can use the state machine
// to store the fact that we're reading a header value
if (next == -1)
{
// If we can't see the next char then reject the entire line
return false;
}
if (next == ByteSpace || next == ByteTab)
{
// From https://tools.ietf.org/html/rfc7230#section-3.2.4:
//
// Historically, HTTP header field values could be extended over
// multiple lines by preceding each extra line with at least one space
// or horizontal tab (obs-fold). This specification deprecates such
// line folding except within the message/http media type
// (Section 8.3.1). A sender MUST NOT generate a message that includes
// line folding (i.e., that has any field-value that contains a match to
// the obs-fold rule) unless the message is intended for packaging
// within the message/http media type.
//
// A server that receives an obs-fold in a request message that is not
// within a message/http container MUST either reject the message by
// sending a 400 (Bad Request), preferably with a representation
// explaining that obsolete line folding is unacceptable, or replace
// each received obs-fold with one or more SP octets prior to
// interpreting the field value or forwarding the message downstream.
RejectRequest(RequestRejectionReason.HeaderValueLineFoldingNotSupported);
}
var nameBuffer = span.Slice(nameStart, nameEnd - nameStart);
var valueBuffer = span.Slice(valueStart, valueEnd - valueStart);
consumedBytes += headerLineLength;
handler.OnHeader(nameBuffer, valueBuffer);
consumed = reader.Cursor;
}
}
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 == '~';
}
public void RejectRequest(RequestRejectionReason reason)
{
RejectRequest(BadHttpRequestException.GetException(reason));
}
public void RejectRequest(RequestRejectionReason reason, string value)
{
RejectRequest(BadHttpRequestException.GetException(reason, value));
}
private void RejectRequest(BadHttpRequestException ex)
{
throw ex;
}
private void RejectRequestLine(Span<byte> span)
{
const int MaxRequestLineError = 32;
RejectRequest(RequestRejectionReason.InvalidRequestLine,
Log.IsEnabled(LogLevel.Information) ? span.GetAsciiStringEscaped(MaxRequestLineError) : string.Empty);
}
private enum HeaderState
{
Name,
Whitespace,
ExpectValue,
ExpectNewLine,
Complete
}
private enum StartLineState
{
KnownMethod,
UnknownMethod,
Path,
QueryString,
KnownVersion,
UnknownVersion,
NewLine,
Complete
}
}
}

View File

@ -1,16 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.Diagnostics;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
{
public static class MemoryPoolIteratorExtensions
public static class HttpUtilities
{
public const string Http10Version = "HTTP/1.0";
public const string Http11Version = "HTTP/1.1";
@ -18,7 +18,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
// readonly primitive statics can be Jit'd to consts https://github.com/dotnet/coreclr/issues/1079
private readonly static ulong _httpConnectMethodLong = GetAsciiStringAsLong("CONNECT ");
private readonly static ulong _httpDeleteMethodLong = GetAsciiStringAsLong("DELETE \0");
private readonly static ulong _httpGetMethodLong = GetAsciiStringAsLong("GET \0\0\0\0");
private const uint _httpGetMethodInt = 542393671; // retun of GetAsciiStringAsInt("GET "); const results in better codegen
private readonly static ulong _httpHeadMethodLong = GetAsciiStringAsLong("HEAD \0\0\0");
private readonly static ulong _httpPatchMethodLong = GetAsciiStringAsLong("PATCH \0\0");
@ -36,18 +35,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
private readonly static ulong _mask5Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00 });
private readonly static ulong _mask4Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00 });
private readonly static Tuple<ulong, ulong, string>[] _knownMethods = new Tuple<ulong, ulong, string>[8];
private readonly static Tuple<ulong, ulong, HttpMethod, int>[] _knownMethods = new Tuple<ulong, ulong, HttpMethod, int>[8];
static MemoryPoolIteratorExtensions()
private readonly static string[] _methodNames = new string[9];
static HttpUtilities()
{
_knownMethods[0] = Tuple.Create(_mask4Chars, _httpPutMethodLong, HttpMethods.Put);
_knownMethods[1] = Tuple.Create(_mask5Chars, _httpPostMethodLong, HttpMethods.Post);
_knownMethods[2] = Tuple.Create(_mask5Chars, _httpHeadMethodLong, HttpMethods.Head);
_knownMethods[3] = Tuple.Create(_mask6Chars, _httpTraceMethodLong, HttpMethods.Trace);
_knownMethods[4] = Tuple.Create(_mask6Chars, _httpPatchMethodLong, HttpMethods.Patch);
_knownMethods[5] = Tuple.Create(_mask7Chars, _httpDeleteMethodLong, HttpMethods.Delete);
_knownMethods[6] = Tuple.Create(_mask8Chars, _httpConnectMethodLong, HttpMethods.Connect);
_knownMethods[7] = Tuple.Create(_mask8Chars, _httpOptionsMethodLong, HttpMethods.Options);
_knownMethods[0] = Tuple.Create(_mask4Chars, _httpPutMethodLong, HttpMethod.Put, 3);
_knownMethods[1] = Tuple.Create(_mask5Chars, _httpPostMethodLong, HttpMethod.Post, 4);
_knownMethods[2] = Tuple.Create(_mask5Chars, _httpHeadMethodLong, HttpMethod.Head, 4);
_knownMethods[3] = Tuple.Create(_mask6Chars, _httpTraceMethodLong, HttpMethod.Trace, 5);
_knownMethods[4] = Tuple.Create(_mask6Chars, _httpPatchMethodLong, HttpMethod.Patch, 5);
_knownMethods[5] = Tuple.Create(_mask7Chars, _httpDeleteMethodLong, HttpMethod.Delete, 6);
_knownMethods[6] = Tuple.Create(_mask8Chars, _httpConnectMethodLong, HttpMethod.Connect, 7);
_knownMethods[7] = Tuple.Create(_mask8Chars, _httpOptionsMethodLong, HttpMethod.Options, 7);
_methodNames[(byte)HttpMethod.Get] = HttpMethods.Get;
_methodNames[(byte)HttpMethod.Put] = HttpMethods.Put;
_methodNames[(byte)HttpMethod.Delete] = HttpMethods.Delete;
_methodNames[(byte)HttpMethod.Post] = HttpMethods.Post;
_methodNames[(byte)HttpMethod.Head] = HttpMethods.Head;
_methodNames[(byte)HttpMethod.Trace] = HttpMethods.Trace;
_methodNames[(byte)HttpMethod.Patch] = HttpMethods.Patch;
_methodNames[(byte)HttpMethod.Connect] = HttpMethods.Connect;
_methodNames[(byte)HttpMethod.Options] = HttpMethods.Options;
}
private unsafe static ulong GetAsciiStringAsLong(string str)
@ -84,67 +94,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
}
}
public static string GetAsciiStringEscaped(this ReadCursor start, ReadCursor end, int maxChars)
{
var sb = new StringBuilder();
var reader = new ReadableBufferReader(start, end);
while (maxChars > 0 && !reader.End)
{
var ch = reader.Take();
sb.Append(ch < 0x20 || ch >= 0x7F ? $"<0x{ch:X2}>" : ((char)ch).ToString());
maxChars--;
}
if (!reader.End)
{
sb.Append("...");
}
return sb.ToString();
}
public static string GetAsciiStringEscaped(this Span<byte> span)
public static string GetAsciiStringEscaped(this Span<byte> span, int maxChars)
{
var sb = new StringBuilder();
for (var i = 0; i < span.Length; ++i)
int i;
for (i = 0; i < Math.Min(span.Length, maxChars); ++i)
{
var ch = span[i];
sb.Append(ch < 0x20 || ch >= 0x7F ? $"<0x{ch:X2}>" : ((char)ch).ToString());
}
if (span.Length > maxChars)
{
sb.Append("...");
}
return sb.ToString();
}
public static ArraySegment<byte> PeekArraySegment(this MemoryPoolIterator iter)
{
if (iter.IsDefault || iter.IsEnd)
{
return default(ArraySegment<byte>);
}
if (iter.Index < iter.Block.End)
{
return new ArraySegment<byte>(iter.Block.Array, iter.Index, iter.Block.End - iter.Index);
}
var block = iter.Block.Next;
while (block != null)
{
if (block.Start < block.End)
{
return new ArraySegment<byte>(block.Array, block.Start, block.End - block.Start);
}
block = block.Next;
}
// The following should be unreachable due to the IsEnd check above.
throw new InvalidOperationException("This should be unreachable!");
}
/// <summary>
/// Checks that up to 8 bytes from <paramref name="begin"/> correspond to a known HTTP method.
/// Checks that up to 8 bytes from <paramref name="span"/> correspond to a known HTTP method.
/// </summary>
/// <remarks>
/// A "known HTTP method" can be an HTTP method name defined in the HTTP/1.1 RFC.
@ -154,44 +123,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
/// and will be compared with the required space. A mask is used if the Known method is less than 8 bytes.
/// To optimize performance the GET method will be checked first.
/// </remarks>
/// <param name="begin">The iterator from which to start the known string lookup.</param>
/// <param name="knownMethod">A reference to a pre-allocated known string, if the input matches any.</param>
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool GetKnownMethod(this ReadableBuffer begin, out string knownMethod)
{
knownMethod = null;
if (begin.Length < sizeof(ulong))
{
return false;
}
ulong value = begin.ReadLittleEndian<ulong>();
if ((value & _mask4Chars) == _httpGetMethodLong)
{
knownMethod = HttpMethods.Get;
return true;
}
foreach (var x in _knownMethods)
{
if ((value & x.Item1) == x.Item2)
{
knownMethod = x.Item3;
return true;
}
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool GetKnownMethod(this Span<byte> span, out string knownMethod)
public static bool GetKnownMethod(this Span<byte> span, out HttpMethod method, out int length)
{
if (span.TryRead<uint>(out var possiblyGet))
{
if (possiblyGet == _httpGetMethodInt)
{
knownMethod = HttpMethods.Get;
length = 3;
method = HttpMethod.Get;
return true;
}
}
@ -202,18 +143,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
{
if ((value & x.Item1) == x.Item2)
{
knownMethod = x.Item3;
method = x.Item3;
length = x.Item4;
return true;
}
}
}
knownMethod = null;
method = HttpMethod.Custom;
length = 0;
return false;
}
/// <summary>
/// Checks 9 bytes from <paramref name="begin"/> correspond to a known HTTP version.
/// Checks 9 bytes from <paramref name="span"/> correspond to a known HTTP version.
/// </summary>
/// <remarks>
/// A "known HTTP version" Is is either HTTP/1.0 or HTTP/1.1.
@ -222,56 +165,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
/// The Known versions will be checked with the required '\r'.
/// To optimize performance the HTTP/1.1 will be checked first.
/// </remarks>
/// <param name="begin">The iterator from which to start the known string lookup.</param>
/// <param name="knownVersion">A reference to a pre-allocated known string, if the input matches any.</param>
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool GetKnownVersion(this ReadableBuffer begin, out string knownVersion)
{
knownVersion = null;
if (begin.Length < sizeof(ulong))
{
return false;
}
var value = begin.ReadLittleEndian<ulong>();
if (value == _http11VersionLong)
{
knownVersion = Http11Version;
}
else if (value == _http10VersionLong)
{
knownVersion = Http10Version;
}
if (knownVersion != null)
{
if (begin.Slice(sizeof(ulong)).Peek() != '\r')
{
knownVersion = null;
}
}
return knownVersion != null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool GetKnownVersion(this Span<byte> span, out string knownVersion)
public static bool GetKnownVersion(this Span<byte> span, out HttpVersion knownVersion, out byte length)
{
if (span.TryRead<ulong>(out var version))
{
if (version == _http11VersionLong)
{
knownVersion = Http11Version;
length = sizeof(ulong);
knownVersion = HttpVersion.Http11;
}
else if (version == _http10VersionLong)
{
knownVersion = Http10Version;
length = sizeof(ulong);
knownVersion = HttpVersion.Http10;
}
else
{
knownVersion = null;
length = 0;
knownVersion = HttpVersion.Unknown;
return false;
}
@ -281,8 +194,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
}
}
knownVersion = null;
knownVersion = HttpVersion.Unknown;
length = 0;
return false;
}
public static string VersionToString(HttpVersion httpVersion)
{
switch (httpVersion)
{
case HttpVersion.Http10:
return Http10Version;
case HttpVersion.Http11:
return Http11Version;
default:
return null;
}
}
public static string MethodToString(HttpMethod method)
{
int methodIndex = (int)method;
if (methodIndex >= 0 && methodIndex <= 8)
{
return _methodNames[methodIndex];
}
return null;
}
}
}

View File

@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
public IThreadPool ThreadPool { get; set; }
public IHttpParser HttpParser { get; set; }
public Func<ConnectionContext, Frame> FrameFactory { get; set; }
public DateHeaderValueManager DateHeaderValueManager { get; set; }

View File

@ -99,6 +99,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel
},
AppLifetime = _applicationLifetime,
Log = trace,
HttpParser = new KestrelHttpParser(trace),
ThreadPool = threadPool,
DateHeaderValueManager = dateHeaderValueManager,
ServerOptions = Options

View File

@ -4,6 +4,7 @@
using System;
using System.Text;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
@ -18,29 +19,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
public int GetKnownMethod_GET()
{
int len = 0;
string method;
HttpMethod method;
Span<byte> data = _method;
for (int i = 0; i < loops; i++) {
data.GetKnownMethod(out method);
len += method.Length;
data.GetKnownMethod(out method);
len += method.Length;
data.GetKnownMethod(out method);
len += method.Length;
data.GetKnownMethod(out method);
len += method.Length;
data.GetKnownMethod(out method);
len += method.Length;
data.GetKnownMethod(out method);
len += method.Length;
data.GetKnownMethod(out method);
len += method.Length;
data.GetKnownMethod(out method);
len += method.Length;
data.GetKnownMethod(out method);
len += method.Length;
data.GetKnownMethod(out method);
len += method.Length;
data.GetKnownMethod(out method, out var length);
len += length;
data.GetKnownMethod(out method, out length);
len += length;
data.GetKnownMethod(out method, out length);
len += length;
data.GetKnownMethod(out method, out length);
len += length;
data.GetKnownMethod(out method, out length);
len += length;
data.GetKnownMethod(out method, out length);
len += length;
data.GetKnownMethod(out method, out length);
len += length;
data.GetKnownMethod(out method, out length);
len += length;
data.GetKnownMethod(out method, out length);
len += length;
data.GetKnownMethod(out method, out length);
len += length;
}
return len;
}
@ -49,29 +50,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
public int GetKnownVersion_HTTP1_1()
{
int len = 0;
string version;
HttpVersion version;
Span<byte> data = _version;
for (int i = 0; i < loops; i++) {
data.GetKnownVersion(out version);
len += version.Length;
data.GetKnownVersion(out version);
len += version.Length;
data.GetKnownVersion(out version);
len += version.Length;
data.GetKnownVersion(out version);
len += version.Length;
data.GetKnownVersion(out version);
len += version.Length;
data.GetKnownVersion(out version);
len += version.Length;
data.GetKnownVersion(out version);
len += version.Length;
data.GetKnownVersion(out version);
len += version.Length;
data.GetKnownVersion(out version);
len += version.Length;
data.GetKnownVersion(out version);
len += version.Length;
data.GetKnownVersion(out version, out var length);
len += length;
data.GetKnownVersion(out version, out length);
len += length;
data.GetKnownVersion(out version, out length);
len += length;
data.GetKnownVersion(out version, out length);
len += length;
data.GetKnownVersion(out version, out length);
len += length;
data.GetKnownVersion(out version, out length);
len += length;
data.GetKnownVersion(out version, out length);
len += length;
data.GetKnownVersion(out version, out length);
len += length;
data.GetKnownVersion(out version, out length);
len += length;
data.GetKnownVersion(out version, out length);
len += length;
}
return len;
}

View File

@ -58,6 +58,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
private static readonly byte[] _unicodePipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(unicodeRequest, Pipelining)));
private static readonly byte[] _unicodeRequest = Encoding.ASCII.GetBytes(unicodeRequest);
[Params(typeof(KestrelHttpParser))]
public Type ParserType { get; set; }
[Benchmark(Baseline = true, OperationsPerInvoke = InnerLoopCount)]
public void ParsePlaintext()
{
@ -176,6 +179,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
public void Setup()
{
var connectionContext = new MockConnection(new KestrelServerOptions());
connectionContext.ListenerContext.ServiceContext.HttpParser = (IHttpParser) Activator.CreateInstance(ParserType, connectionContext.ListenerContext.ServiceContext.Log);
Frame = new Frame<object>(application: null, context: connectionContext);
PipelineFactory = new PipeFactory();
Pipe = PipelineFactory.Create();

View File

@ -55,6 +55,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
HttpParser = new KestrelHttpParser(trace),
Log = trace
};
var listenerContext = new ListenerContext(_serviceContext)

View File

@ -0,0 +1,122 @@
// 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.Text;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests
{
public class HttpUtilitiesTest
{
[Theory]
[InlineData("CONNECT / HTTP/1.1", true, "CONNECT", HttpMethod.Connect)]
[InlineData("DELETE / HTTP/1.1", true, "DELETE", HttpMethod.Delete)]
[InlineData("GET / HTTP/1.1", true, "GET", HttpMethod.Get)]
[InlineData("HEAD / HTTP/1.1", true, "HEAD", HttpMethod.Head)]
[InlineData("PATCH / HTTP/1.1", true, "PATCH", HttpMethod.Patch)]
[InlineData("POST / HTTP/1.1", true, "POST", HttpMethod.Post)]
[InlineData("PUT / HTTP/1.1", true, "PUT", HttpMethod.Put)]
[InlineData("OPTIONS / HTTP/1.1", true, "OPTIONS", HttpMethod.Options)]
[InlineData("TRACE / HTTP/1.1", true, "TRACE", HttpMethod.Trace)]
[InlineData("GET/ HTTP/1.1", false, null, HttpMethod.Custom)]
[InlineData("get / HTTP/1.1", false, null, HttpMethod.Custom)]
[InlineData("GOT / HTTP/1.1", false, null, HttpMethod.Custom)]
[InlineData("ABC / HTTP/1.1", false, null, HttpMethod.Custom)]
[InlineData("PO / HTTP/1.1", false, null, HttpMethod.Custom)]
[InlineData("PO ST / HTTP/1.1", false, null, HttpMethod.Custom)]
[InlineData("short ", false, null, HttpMethod.Custom)]
public void GetsKnownMethod(string input, bool expectedResult, string expectedKnownString, HttpMethod expectedMethod)
{
// Arrange
var block = new Span<byte>(Encoding.ASCII.GetBytes(input));
// Act
HttpMethod knownMethod;
var result = block.GetKnownMethod(out knownMethod, out var length);
string toString = null;
if (knownMethod != HttpMethod.Custom)
{
toString = HttpUtilities.MethodToString(knownMethod);
}
// Assert
Assert.Equal(expectedResult, result);
Assert.Equal(expectedMethod, knownMethod);
Assert.Equal(toString, expectedKnownString);
Assert.Equal(length, expectedKnownString?.Length ?? 0);
}
[Theory]
[InlineData("HTTP/1.0\r", true, HttpUtilities.Http10Version, HttpVersion.Http10)]
[InlineData("HTTP/1.1\r", true, HttpUtilities.Http11Version, HttpVersion.Http11)]
[InlineData("HTTP/3.0\r", false, null, HttpVersion.Unknown)]
[InlineData("http/1.0\r", false, null, HttpVersion.Unknown)]
[InlineData("http/1.1\r", false, null, HttpVersion.Unknown)]
[InlineData("short ", false, null, HttpVersion.Unknown)]
public void GetsKnownVersion(string input, bool expectedResult, string expectedKnownString, HttpVersion version)
{
// Arrange
var block = new Span<byte>(Encoding.ASCII.GetBytes(input));
// Act
HttpVersion knownVersion;
var result = block.GetKnownVersion(out knownVersion, out var length);
string toString = null;
if (knownVersion != HttpVersion.Unknown)
{
toString = HttpUtilities.VersionToString(knownVersion);
}
// Assert
Assert.Equal(expectedResult, result);
Assert.Equal(expectedKnownString, toString);
Assert.Equal(expectedKnownString?.Length ?? 0, length);
}
[Theory]
[InlineData("HTTP/1.0\r", "HTTP/1.0")]
[InlineData("HTTP/1.1\r", "HTTP/1.1")]
public void KnownVersionsAreInterned(string input, string expected)
{
TestKnownStringsInterning(input, expected, span =>
{
HttpUtilities.GetKnownVersion(span, out var version, out var lenght);
return HttpUtilities.VersionToString(version);
});
}
[Theory]
[InlineData("CONNECT / HTTP/1.1", "CONNECT")]
[InlineData("DELETE / HTTP/1.1", "DELETE")]
[InlineData("GET / HTTP/1.1", "GET")]
[InlineData("HEAD / HTTP/1.1", "HEAD")]
[InlineData("PATCH / HTTP/1.1", "PATCH")]
[InlineData("POST / HTTP/1.1", "POST")]
[InlineData("PUT / HTTP/1.1", "PUT")]
[InlineData("OPTIONS / HTTP/1.1", "OPTIONS")]
[InlineData("TRACE / HTTP/1.1", "TRACE")]
public void KnownMethodsAreInterned(string input, string expected)
{
TestKnownStringsInterning(input, expected, span =>
{
HttpUtilities.GetKnownMethod(span, out var method, out var length);
return HttpUtilities.MethodToString(method);
});
}
private void TestKnownStringsInterning(string input, string expected, Func<Span<byte>, string> action)
{
// Act
var knownString1 = action(new Span<byte>(Encoding.ASCII.GetBytes(input)));
var knownString2 = action(new Span<byte>(Encoding.ASCII.GetBytes(input)));
// Assert
Assert.Equal(knownString1, expected);
Assert.Same(knownString1, knownString2);
}
}
}

View File

@ -45,6 +45,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
DateHeaderValueManager = serviceContextPrimary.DateHeaderValueManager,
ServerOptions = serviceContextPrimary.ServerOptions,
ThreadPool = serviceContextPrimary.ThreadPool,
HttpParser = new KestrelHttpParser(serviceContextPrimary.Log),
FrameFactory = context =>
{
return new Frame<DefaultHttpContext>(new TestApplication(c =>
@ -121,6 +122,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
DateHeaderValueManager = serviceContextPrimary.DateHeaderValueManager,
ServerOptions = serviceContextPrimary.ServerOptions,
ThreadPool = serviceContextPrimary.ThreadPool,
HttpParser = new KestrelHttpParser(serviceContextPrimary.Log),
FrameFactory = context =>
{
return new Frame<DefaultHttpContext>(new TestApplication(c =>
@ -243,6 +245,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
DateHeaderValueManager = serviceContextPrimary.DateHeaderValueManager,
ServerOptions = serviceContextPrimary.ServerOptions,
ThreadPool = serviceContextPrimary.ThreadPool,
HttpParser = new KestrelHttpParser(serviceContextPrimary.Log),
FrameFactory = context =>
{
return new Frame<DefaultHttpContext>(new TestApplication(c =>

View File

@ -7,6 +7,8 @@ using System.IO.Pipelines;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Xunit;
using MemoryPool = Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure.MemoryPool;
@ -166,109 +168,77 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Fact]
public void PeekArraySegment()
public async Task PeekArraySegment()
{
// Arrange
var block = _pool.Lease();
var bytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 };
Buffer.BlockCopy(bytes, 0, block.Array, block.Start, bytes.Length);
block.End += bytes.Length;
var scan = block.GetIterator();
var originalIndex = scan.Index;
using (var pipeFactory = new PipeFactory())
{
// Arrange
var pipe = pipeFactory.Create();
var buffer = pipe.Writer.Alloc();
buffer.Append(ReadableBuffer.Create(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }));
await buffer.FlushAsync();
// Act
var result = await pipe.Reader.PeekAsync();
// Act
var result = scan.PeekArraySegment();
// Assert
Assert.Equal(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}, result);
// Assert
Assert.Equal(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }, result);
Assert.Equal(originalIndex, scan.Index);
_pool.Return(block);
pipe.Writer.Complete();
pipe.Reader.Complete();
}
}
[Fact]
public void PeekArraySegmentOnDefaultIteratorReturnsDefaultArraySegment()
public async Task PeekArraySegmentAtEndOfDataReturnsDefaultArraySegment()
{
// Assert.Equals doesn't work since xunit tries to access the underlying array.
Assert.True(default(ArraySegment<byte>).Equals(default(MemoryPoolIterator).PeekArraySegment()));
using (var pipeFactory = new PipeFactory())
{
// Arrange
var pipe = pipeFactory.Create();
pipe.Writer.Complete();
// Act
var result = await pipe.Reader.PeekAsync();
// Assert
// Assert.Equals doesn't work since xunit tries to access the underlying array.
Assert.True(default(ArraySegment<byte>).Equals(result));
pipe.Reader.Complete();
}
}
[Fact]
public void PeekArraySegmentAtEndOfDataReturnsDefaultArraySegment()
public async Task PeekArraySegmentAtBlockBoundary()
{
// Arrange
var block = _pool.Lease();
var bytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 };
Buffer.BlockCopy(bytes, 0, block.Array, block.Start, bytes.Length);
block.End += bytes.Length;
block.Start = block.End;
using (var pipeFactory = new PipeFactory())
{
var pipe = pipeFactory.Create();
var buffer = pipe.Writer.Alloc();
buffer.Append(ReadableBuffer.Create(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }));
buffer.Append(ReadableBuffer.Create(new byte[] { 8, 9, 10, 11, 12, 13, 14, 15 }));
await buffer.FlushAsync();
var scan = block.GetIterator();
// Act
var result = await pipe.Reader.PeekAsync();
// Act
var result = scan.PeekArraySegment();
// Assert
Assert.Equal(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }, result);
// Assert
// Assert.Equals doesn't work since xunit tries to access the underlying array.
Assert.True(default(ArraySegment<byte>).Equals(result));
// Act
// Advance past the data in the first block
var readResult = pipe.Reader.ReadAsync().GetAwaiter().GetResult();
pipe.Reader.Advance(readResult.Buffer.Move(readResult.Buffer.Start, 8));
result = await pipe.Reader.PeekAsync();
_pool.Return(block);
}
// Assert
Assert.Equal(new byte[] { 8, 9, 10, 11, 12, 13, 14, 15 }, result);
[Fact]
public void PeekArraySegmentAtBlockBoundary()
{
// Arrange
var firstBlock = _pool.Lease();
var lastBlock = _pool.Lease();
pipe.Writer.Complete();
pipe.Reader.Complete();
}
var firstBytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 };
var lastBytes = new byte[] { 8, 9, 10, 11, 12, 13, 14, 15 };
Buffer.BlockCopy(firstBytes, 0, firstBlock.Array, firstBlock.Start, firstBytes.Length);
firstBlock.End += lastBytes.Length;
firstBlock.Next = lastBlock;
Buffer.BlockCopy(lastBytes, 0, lastBlock.Array, lastBlock.Start, lastBytes.Length);
lastBlock.End += lastBytes.Length;
var scan = firstBlock.GetIterator();
var originalIndex = scan.Index;
var originalBlock = scan.Block;
// Act
var result = scan.PeekArraySegment();
// Assert
Assert.Equal(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }, result);
Assert.Equal(originalBlock, scan.Block);
Assert.Equal(originalIndex, scan.Index);
// Act
// Advance past the data in the first block
scan.Skip(8);
result = scan.PeekArraySegment();
// Assert
Assert.Equal(new byte[] { 8, 9, 10, 11, 12, 13, 14, 15 }, result);
Assert.Equal(originalBlock, scan.Block);
Assert.Equal(originalIndex + 8, scan.Index);
// Act
// Add anther empty block between the first and last block
var middleBlock = _pool.Lease();
firstBlock.Next = middleBlock;
middleBlock.Next = lastBlock;
result = scan.PeekArraySegment();
// Assert
Assert.Equal(new byte[] { 8, 9, 10, 11, 12, 13, 14, 15 }, result);
Assert.Equal(originalBlock, scan.Block);
Assert.Equal(originalIndex + 8, scan.Index);
_pool.Return(firstBlock);
_pool.Return(middleBlock);
_pool.Return(lastBlock);
}
[Fact]
@ -542,198 +512,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
_pool.Return(finalBlock);
}
[Theory]
[InlineData("CONNECT / HTTP/1.1", true, "CONNECT")]
[InlineData("DELETE / HTTP/1.1", true, "DELETE")]
[InlineData("GET / HTTP/1.1", true, "GET")]
[InlineData("HEAD / HTTP/1.1", true, "HEAD")]
[InlineData("PATCH / HTTP/1.1", true, "PATCH")]
[InlineData("POST / HTTP/1.1", true, "POST")]
[InlineData("PUT / HTTP/1.1", true, "PUT")]
[InlineData("OPTIONS / HTTP/1.1", true, "OPTIONS")]
[InlineData("TRACE / HTTP/1.1", true, "TRACE")]
[InlineData("GET/ HTTP/1.1", false, null)]
[InlineData("get / HTTP/1.1", false, null)]
[InlineData("GOT / HTTP/1.1", false, null)]
[InlineData("ABC / HTTP/1.1", false, null)]
[InlineData("PO / HTTP/1.1", false, null)]
[InlineData("PO ST / HTTP/1.1", false, null)]
[InlineData("short ", false, null)]
public void GetsKnownMethod(string input, bool expectedResult, string expectedKnownString)
{
// Arrange
var block = ReadableBuffer.Create(Encoding.ASCII.GetBytes(input));
// Act
string knownString;
var result = block.GetKnownMethod(out knownString);
// Assert
Assert.Equal(expectedResult, result);
Assert.Equal(expectedKnownString, knownString);
}
[Theory]
[InlineData("CONNECT / HTTP/1.1", true, "CONNECT")]
[InlineData("DELETE / HTTP/1.1", true, "DELETE")]
[InlineData("GET / HTTP/1.1", true, "GET")]
[InlineData("HEAD / HTTP/1.1", true, "HEAD")]
[InlineData("PATCH / HTTP/1.1", true, "PATCH")]
[InlineData("POST / HTTP/1.1", true, "POST")]
[InlineData("PUT / HTTP/1.1", true, "PUT")]
[InlineData("OPTIONS / HTTP/1.1", true, "OPTIONS")]
[InlineData("TRACE / HTTP/1.1", true, "TRACE")]
[InlineData("GET/ HTTP/1.1", false, null)]
[InlineData("get / HTTP/1.1", false, null)]
[InlineData("GOT / HTTP/1.1", false, null)]
[InlineData("ABC / HTTP/1.1", false, null)]
[InlineData("PO / HTTP/1.1", false, null)]
[InlineData("PO ST / HTTP/1.1", false, null)]
[InlineData("short ", false, null)]
public void GetsKnownMethodOnBoundary(string input, bool expectedResult, string expectedKnownString)
{
// Test at boundary
var maxSplit = Math.Min(input.Length, 8);
for (var split = 0; split <= maxSplit; split++)
{
using (var pipelineFactory = new PipeFactory())
{
// Arrange
var pipe = pipelineFactory.Create();
var buffer = pipe.Writer.Alloc();
var block1Input = input.Substring(0, split);
var block2Input = input.Substring(split);
buffer.Append(ReadableBuffer.Create(Encoding.ASCII.GetBytes(block1Input)));
buffer.Append(ReadableBuffer.Create(Encoding.ASCII.GetBytes(block2Input)));
buffer.FlushAsync().GetAwaiter().GetResult();
var readResult = pipe.Reader.ReadAsync().GetAwaiter().GetResult();
// Act
string boundaryKnownString;
var boundaryResult = readResult.Buffer.GetKnownMethod(out boundaryKnownString);
// Assert
Assert.Equal(expectedResult, boundaryResult);
Assert.Equal(expectedKnownString, boundaryKnownString);
}
}
}
[Theory]
[InlineData("HTTP/1.0\r", true, MemoryPoolIteratorExtensions.Http10Version)]
[InlineData("HTTP/1.1\r", true, MemoryPoolIteratorExtensions.Http11Version)]
[InlineData("HTTP/3.0\r", false, null)]
[InlineData("http/1.0\r", false, null)]
[InlineData("http/1.1\r", false, null)]
[InlineData("short ", false, null)]
public void GetsKnownVersion(string input, bool expectedResult, string expectedKnownString)
{
// Arrange
var block = ReadableBuffer.Create(Encoding.ASCII.GetBytes(input));
// Act
string knownString;
var result = block.GetKnownVersion(out knownString);
// Assert
Assert.Equal(expectedResult, result);
Assert.Equal(expectedKnownString, knownString);
}
[Theory]
[InlineData("HTTP/1.0\r", true, MemoryPoolIteratorExtensions.Http10Version)]
[InlineData("HTTP/1.1\r", true, MemoryPoolIteratorExtensions.Http11Version)]
[InlineData("HTTP/3.0\r", false, null)]
[InlineData("http/1.0\r", false, null)]
[InlineData("http/1.1\r", false, null)]
[InlineData("short ", false, null)]
public void GetsKnownVersionOnBoundary(string input, bool expectedResult, string expectedKnownString)
{
// Test at boundary
var maxSplit = Math.Min(input.Length, 9);
for (var split = 0; split <= maxSplit; split++)
{
using (var pipelineFactory = new PipeFactory())
{
// Arrange
var pipe = pipelineFactory.Create();
var buffer = pipe.Writer.Alloc();
var block1Input = input.Substring(0, split);
var block2Input = input.Substring(split);
buffer.Append(ReadableBuffer.Create(Encoding.ASCII.GetBytes(block1Input)));
buffer.Append(ReadableBuffer.Create(Encoding.ASCII.GetBytes(block2Input)));
buffer.FlushAsync().GetAwaiter().GetResult();
var readResult = pipe.Reader.ReadAsync().GetAwaiter().GetResult();
// Act
string boundaryKnownString;
var boundaryResult = readResult.Buffer.GetKnownVersion(out boundaryKnownString);
// Assert
Assert.Equal(expectedResult, boundaryResult);
Assert.Equal(expectedKnownString, boundaryKnownString);
}
}
}
[Theory]
[InlineData("HTTP/1.0\r", "HTTP/1.0")]
[InlineData("HTTP/1.1\r", "HTTP/1.1")]
public void KnownVersionsAreInterned(string input, string expected)
{
TestKnownStringsInterning(input, expected, MemoryPoolIteratorExtensions.GetKnownVersion);
}
[Theory]
[InlineData("", "HTTP/1.1\r")]
[InlineData("H", "TTP/1.1\r")]
[InlineData("HT", "TP/1.1\r")]
[InlineData("HTT", "P/1.1\r")]
[InlineData("HTTP", "/1.1\r")]
[InlineData("HTTP/", "1.1\r")]
[InlineData("HTTP/1", ".1\r")]
[InlineData("HTTP/1.", "1\r")]
[InlineData("HTTP/1.1", "\r")]
[InlineData("HTTP/1.1\r", "")]
public void KnownVersionCanBeReadAtAnyBlockBoundary(string block1Input, string block2Input)
{
using (var pipelineFactory = new PipeFactory())
{
// Arrange
var pipe = pipelineFactory.Create();
var buffer = pipe.Writer.Alloc();
buffer.Append(ReadableBuffer.Create(Encoding.ASCII.GetBytes(block1Input)));
buffer.Append(ReadableBuffer.Create(Encoding.ASCII.GetBytes(block2Input)));
buffer.FlushAsync().GetAwaiter().GetResult();
var readResult = pipe.Reader.ReadAsync().GetAwaiter().GetResult();
// Act
string knownVersion;
var result = readResult.Buffer.GetKnownVersion(out knownVersion);
// Assert
Assert.True(result);
Assert.Equal("HTTP/1.1", knownVersion);
}
}
[Theory]
[InlineData("CONNECT / HTTP/1.1", "CONNECT")]
[InlineData("DELETE / HTTP/1.1", "DELETE")]
[InlineData("GET / HTTP/1.1", "GET")]
[InlineData("HEAD / HTTP/1.1", "HEAD")]
[InlineData("PATCH / HTTP/1.1", "PATCH")]
[InlineData("POST / HTTP/1.1", "POST")]
[InlineData("PUT / HTTP/1.1", "PUT")]
[InlineData("OPTIONS / HTTP/1.1", "OPTIONS")]
[InlineData("TRACE / HTTP/1.1", "TRACE")]
public void KnownMethodsAreInterned(string input, string expected)
{
TestKnownStringsInterning(input, expected, MemoryPoolIteratorExtensions.GetKnownMethod);
}
[Theory]
[MemberData(nameof(SeekByteLimitData))]
@ -1200,10 +978,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
try
{
// Arrange
var buffer = ReadableBuffer.Create(Encoding.ASCII.GetBytes(input));
var buffer = new Span<byte>(Encoding.ASCII.GetBytes(input));
// Act
var result = buffer.Start.GetAsciiStringEscaped(buffer.End, maxChars);
var result = buffer.GetAsciiStringEscaped(maxChars);
// Assert
Assert.Equal(expected, result);
@ -1294,22 +1072,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
}
private delegate bool GetKnownString(ReadableBuffer iter, out string result);
private void TestKnownStringsInterning(string input, string expected, GetKnownString action)
{
// Act
string knownString1, knownString2;
var result1 = action(ReadableBuffer.Create(Encoding.ASCII.GetBytes(input)), out knownString1);
var result2 = action(ReadableBuffer.Create(Encoding.ASCII.GetBytes(input)), out knownString2);
// Assert
Assert.True(result1);
Assert.True(result2);
Assert.Equal(knownString1, expected);
Assert.Same(knownString1, knownString2);
}
public static IEnumerable<object[]> SeekByteLimitData
{
get

View File

@ -10,7 +10,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
using MemoryPool = Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure.MemoryPool;
@ -25,11 +24,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
public TestInput()
{
var trace = new KestrelTrace(new TestKestrelTrace());
var ltp = new LoggingThreadPool(trace);
var serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions()
ServerOptions = new KestrelServerOptions(),
HttpParser = new KestrelHttpParser(trace),
};
var listenerContext = new ListenerContext(serviceContext)
{

View File

@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Testing
ThreadPool = new LoggingThreadPool(Log);
DateHeaderValueManager = new DateHeaderValueManager(systemClock: new MockSystemClock());
DateHeaderValue = DateHeaderValueManager.GetDateHeaderValues().String;
HttpParser = new KestrelHttpParser(Log);
ServerOptions = new KestrelServerOptions
{
AddServerHeader = false,