Add IHttpParser interface (#1414)
This commit is contained in:
parent
c56de066d3
commit
d3694f085a
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
{
|
||||
public enum HttpVersion
|
||||
{
|
||||
Unset = -1,
|
||||
Unknown = -1,
|
||||
Http10 = 0,
|
||||
Http11 = 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
},
|
||||
AppLifetime = _applicationLifetime,
|
||||
Log = trace,
|
||||
HttpParser = new KestrelHttpParser(trace),
|
||||
ThreadPool = threadPool,
|
||||
DateHeaderValueManager = dateHeaderValueManager,
|
||||
ServerOptions = Options
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue