Merge branch 'dev' into knownmethods-optimizations
This commit is contained in:
commit
d755f6183a
|
|
@ -16,8 +16,7 @@ env:
|
|||
global:
|
||||
- DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
- DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
mono:
|
||||
- 4.0.5
|
||||
mono: none
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
|
|
|||
|
|
@ -4,38 +4,39 @@
|
|||
using System.IO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel
|
||||
{
|
||||
public sealed class BadHttpRequestException : IOException
|
||||
{
|
||||
private BadHttpRequestException(string message, int statusCode)
|
||||
: this(message, statusCode, null)
|
||||
{ }
|
||||
|
||||
private BadHttpRequestException(string message, int statusCode, HttpMethod? requiredMethod)
|
||||
: base(message)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
|
||||
if (requiredMethod.HasValue)
|
||||
{
|
||||
AllowedHeader = HttpUtilities.MethodToString(requiredMethod.Value);
|
||||
}
|
||||
}
|
||||
|
||||
internal int StatusCode { get; }
|
||||
|
||||
internal StringValues AllowedHeader { get; }
|
||||
|
||||
internal static BadHttpRequestException GetException(RequestRejectionReason reason)
|
||||
{
|
||||
BadHttpRequestException ex;
|
||||
switch (reason)
|
||||
{
|
||||
case RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence:
|
||||
ex = new BadHttpRequestException("Headers corrupted, invalid header sequence.", StatusCodes.Status400BadRequest);
|
||||
break;
|
||||
case RequestRejectionReason.NoColonCharacterFoundInHeaderLine:
|
||||
ex = new BadHttpRequestException("No ':' character found in header line.", StatusCodes.Status400BadRequest);
|
||||
break;
|
||||
case RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName:
|
||||
ex = new BadHttpRequestException("Whitespace is not allowed in header name.", StatusCodes.Status400BadRequest);
|
||||
break;
|
||||
case RequestRejectionReason.HeaderValueMustNotContainCR:
|
||||
ex = new BadHttpRequestException("Header value must not contain CR characters.", StatusCodes.Status400BadRequest);
|
||||
break;
|
||||
case RequestRejectionReason.HeaderValueLineFoldingNotSupported:
|
||||
ex = new BadHttpRequestException("Header value line folding not supported.", StatusCodes.Status400BadRequest);
|
||||
case RequestRejectionReason.InvalidRequestHeadersNoCRLF:
|
||||
ex = new BadHttpRequestException("Invalid request headers: missing final CRLF in header fields.", StatusCodes.Status400BadRequest);
|
||||
break;
|
||||
case RequestRejectionReason.InvalidRequestLine:
|
||||
ex = new BadHttpRequestException("Invalid request line.", StatusCodes.Status400BadRequest);
|
||||
|
|
@ -58,30 +59,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
case RequestRejectionReason.ChunkedRequestIncomplete:
|
||||
ex = new BadHttpRequestException("Chunked request incomplete.", StatusCodes.Status400BadRequest);
|
||||
break;
|
||||
case RequestRejectionReason.PathContainsNullCharacters:
|
||||
ex = new BadHttpRequestException("The path contains null characters.", StatusCodes.Status400BadRequest);
|
||||
break;
|
||||
case RequestRejectionReason.InvalidCharactersInHeaderName:
|
||||
ex = new BadHttpRequestException("Invalid characters in header name.", StatusCodes.Status400BadRequest);
|
||||
break;
|
||||
case RequestRejectionReason.NonAsciiOrNullCharactersInInputString:
|
||||
ex = new BadHttpRequestException("The input string contains non-ASCII or null characters.", StatusCodes.Status400BadRequest);
|
||||
break;
|
||||
case RequestRejectionReason.RequestLineTooLong:
|
||||
ex = new BadHttpRequestException("Request line too long.", StatusCodes.Status414UriTooLong);
|
||||
break;
|
||||
case RequestRejectionReason.HeadersExceedMaxTotalSize:
|
||||
ex = new BadHttpRequestException("Request headers too long.", StatusCodes.Status431RequestHeaderFieldsTooLarge);
|
||||
break;
|
||||
case RequestRejectionReason.MissingCRInHeaderLine:
|
||||
ex = new BadHttpRequestException("No CR character found in header line.", StatusCodes.Status400BadRequest);
|
||||
break;
|
||||
case RequestRejectionReason.TooManyHeaders:
|
||||
ex = new BadHttpRequestException("Request contains too many headers.", StatusCodes.Status431RequestHeaderFieldsTooLarge);
|
||||
break;
|
||||
case RequestRejectionReason.RequestTimeout:
|
||||
ex = new BadHttpRequestException("Request timed out.", StatusCodes.Status408RequestTimeout);
|
||||
break;
|
||||
case RequestRejectionReason.OptionsMethodRequired:
|
||||
ex = new BadHttpRequestException("Method not allowed.", StatusCodes.Status405MethodNotAllowed, HttpMethod.Options);
|
||||
break;
|
||||
case RequestRejectionReason.ConnectMethodRequired:
|
||||
ex = new BadHttpRequestException("Method not allowed.", StatusCodes.Status405MethodNotAllowed, HttpMethod.Connect);
|
||||
break;
|
||||
default:
|
||||
ex = new BadHttpRequestException("Bad request.", StatusCodes.Status400BadRequest);
|
||||
break;
|
||||
|
|
@ -95,7 +93,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
switch (reason)
|
||||
{
|
||||
case RequestRejectionReason.InvalidRequestLine:
|
||||
ex = new BadHttpRequestException($"Invalid request line: {value}", StatusCodes.Status400BadRequest);
|
||||
ex = new BadHttpRequestException($"Invalid request line: '{value}'", StatusCodes.Status400BadRequest);
|
||||
break;
|
||||
case RequestRejectionReason.InvalidRequestHeader:
|
||||
ex = new BadHttpRequestException($"Invalid request header: '{value}'", StatusCodes.Status400BadRequest);
|
||||
break;
|
||||
case RequestRejectionReason.InvalidContentLength:
|
||||
ex = new BadHttpRequestException($"Invalid content length: {value}", StatusCodes.Status400BadRequest);
|
||||
|
|
|
|||
|
|
@ -6,13 +6,11 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.IO.Pipelines.Text.Primitives;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web.Utf8;
|
||||
using System.Text.Utf8;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -28,6 +26,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
{
|
||||
public abstract partial class Frame : IFrameControl, IHttpRequestLineHandler, IHttpHeadersHandler
|
||||
{
|
||||
private const byte ByteAsterisk = (byte)'*';
|
||||
private const byte ByteForwardSlash = (byte)'/';
|
||||
private const byte BytePercentage = (byte)'%';
|
||||
|
||||
private static readonly ArraySegment<byte> _endChunkedResponseBytes = CreateAsciiByteArraySegment("0\r\n\r\n");
|
||||
|
|
@ -40,6 +40,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n");
|
||||
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: Kestrel");
|
||||
|
||||
private const string EmptyPath = "/";
|
||||
private const string Asterisk = "*";
|
||||
|
||||
private readonly object _onStartingSync = new Object();
|
||||
private readonly object _onCompletedSync = new Object();
|
||||
|
||||
|
|
@ -819,7 +822,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
// that should take precedence.
|
||||
if (_requestRejectedException != null)
|
||||
{
|
||||
SetErrorResponseHeaders(statusCode: _requestRejectedException.StatusCode);
|
||||
SetErrorResponseException(_requestRejectedException);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1078,7 +1081,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
{
|
||||
buffer = buffer.Slice(buffer.Start, _remainingRequestHeadersBytesAllowed);
|
||||
|
||||
// If we sliced it means the current buffer bigger than what we're
|
||||
// If we sliced it means the current buffer bigger than what we're
|
||||
// allowed to look at
|
||||
overLength = true;
|
||||
}
|
||||
|
|
@ -1128,6 +1131,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
}
|
||||
}
|
||||
|
||||
private void SetErrorResponseException(BadHttpRequestException ex)
|
||||
{
|
||||
SetErrorResponseHeaders(ex.StatusCode);
|
||||
|
||||
if (!StringValues.IsNullOrEmpty(ex.AllowedHeader))
|
||||
{
|
||||
FrameResponseHeaders.HeaderAllow = ex.AllowedHeader;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetErrorResponseHeaders(int statusCode)
|
||||
{
|
||||
Debug.Assert(!HasResponseStarted, $"{nameof(SetErrorResponseHeaders)} called after response had already started.");
|
||||
|
|
@ -1180,6 +1193,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
throw BadHttpRequestException.GetException(reason, value);
|
||||
}
|
||||
|
||||
private void RejectRequestLine(Span<byte> requestLine)
|
||||
{
|
||||
Debug.Assert(Log.IsEnabled(LogLevel.Information) == true, "Use RejectRequest instead to improve inlining when log is disabled");
|
||||
|
||||
const int MaxRequestLineError = 32;
|
||||
var line = requestLine.GetAsciiStringEscaped(MaxRequestLineError);
|
||||
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine, line);
|
||||
}
|
||||
|
||||
public void SetBadRequestState(RequestRejectionReason reason)
|
||||
{
|
||||
SetBadRequestState(BadHttpRequestException.GetException(reason));
|
||||
|
|
@ -1191,7 +1213,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
|
||||
if (!HasResponseStarted)
|
||||
{
|
||||
SetErrorResponseHeaders(ex.StatusCode);
|
||||
SetErrorResponseException(ex);
|
||||
}
|
||||
|
||||
_keepAlive = false;
|
||||
|
|
@ -1217,22 +1239,64 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
Log.ApplicationError(ConnectionId, ex);
|
||||
}
|
||||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod)
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, Span<byte> line, bool pathEncoded)
|
||||
{
|
||||
Debug.Assert(target.Length != 0, "Request target must be non-zero length");
|
||||
|
||||
var ch = target[0];
|
||||
if (ch == ByteForwardSlash)
|
||||
{
|
||||
// origin-form.
|
||||
// The most common form of request-target.
|
||||
// https://tools.ietf.org/html/rfc7230#section-5.3.1
|
||||
OnOriginFormTarget(method, version, target, path, query, customMethod, pathEncoded);
|
||||
}
|
||||
else if (ch == ByteAsterisk && target.Length == 1)
|
||||
{
|
||||
OnAsteriskFormTarget(method);
|
||||
}
|
||||
else if (target.GetKnownHttpScheme(out var scheme))
|
||||
{
|
||||
OnAbsoluteFormTarget(target, query, line);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assume anything else is considered authority form.
|
||||
// FYI: this should be an edge case. This should only happen when
|
||||
// a client mistakenly things this server is a proxy server.
|
||||
|
||||
OnAuthorityFormTarget(method, target, line);
|
||||
}
|
||||
|
||||
Method = method != HttpMethod.Custom
|
||||
? HttpUtilities.MethodToString(method) ?? string.Empty
|
||||
: customMethod.GetAsciiStringNonNullCharacters();
|
||||
HttpVersion = HttpUtilities.VersionToString(version);
|
||||
|
||||
Debug.Assert(RawTarget != null, "RawTarget was not set");
|
||||
Debug.Assert(Method != null, "Method was not set");
|
||||
Debug.Assert(Path != null, "Path was not set");
|
||||
Debug.Assert(QueryString != "QueryString was not set");
|
||||
Debug.Assert(HttpVersion != "HttpVersion was not set");
|
||||
}
|
||||
|
||||
private void OnOriginFormTarget(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
{
|
||||
Debug.Assert(target[0] == ByteForwardSlash, "Should only be called when path starts with /");
|
||||
|
||||
// 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)
|
||||
if (pathEncoded)
|
||||
{
|
||||
// Read raw target before mutating memory.
|
||||
rawTarget = target.GetAsciiStringNonNullCharacters();
|
||||
|
||||
// URI was encoded, unescape and then parse as utf8
|
||||
int pathLength = UrlEncoder.Decode(path, path);
|
||||
requestUrlPath = new Utf8String(path.Slice(0, pathLength)).ToString();
|
||||
requestUrlPath = GetUtf8String(path.Slice(0, pathLength));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1251,35 +1315,126 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
}
|
||||
}
|
||||
|
||||
var normalizedTarget = PathNormalizer.RemoveDotSegments(requestUrlPath);
|
||||
if (method != HttpMethod.Custom)
|
||||
{
|
||||
Method = HttpUtilities.MethodToString(method) ?? string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
Method = customMethod.GetAsciiStringNonNullCharacters();
|
||||
}
|
||||
|
||||
QueryString = query.GetAsciiStringNonNullCharacters();
|
||||
RawTarget = rawTarget;
|
||||
HttpVersion = HttpUtilities.VersionToString(version);
|
||||
SetNormalizedPath(requestUrlPath);
|
||||
}
|
||||
|
||||
private void OnAuthorityFormTarget(HttpMethod method, Span<byte> target, Span<byte> line)
|
||||
{
|
||||
// TODO Validate that target is a correct host[:port] string.
|
||||
// Reject as 400 if not. This is just a quick scan for invalid characters
|
||||
// but doesn't check that the target fully matches the URI spec.
|
||||
for (var i = 0; i < target.Length; i++)
|
||||
{
|
||||
var ch = target[i];
|
||||
if (!UriUtilities.IsValidAuthorityCharacter(ch))
|
||||
{
|
||||
if (Log.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
RejectRequestLine(line);
|
||||
}
|
||||
|
||||
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine);
|
||||
}
|
||||
}
|
||||
|
||||
// The authority-form of request-target is only used for CONNECT
|
||||
// requests (https://tools.ietf.org/html/rfc7231#section-4.3.6).
|
||||
if (method != HttpMethod.Connect)
|
||||
{
|
||||
RejectRequest(RequestRejectionReason.ConnectMethodRequired);
|
||||
}
|
||||
|
||||
// When making a CONNECT request to establish a tunnel through one or
|
||||
// more proxies, a client MUST send only the target URI's authority
|
||||
// component(excluding any userinfo and its "@" delimiter) as the
|
||||
// request - target.For example,
|
||||
//
|
||||
// CONNECT www.example.com:80 HTTP/1.1
|
||||
//
|
||||
// Allowed characters in the 'host + port' section of authority.
|
||||
// See https://tools.ietf.org/html/rfc3986#section-3.2
|
||||
|
||||
RawTarget = target.GetAsciiStringNonNullCharacters();
|
||||
Path = string.Empty;
|
||||
PathBase = string.Empty;
|
||||
QueryString = string.Empty;
|
||||
}
|
||||
|
||||
private void OnAsteriskFormTarget(HttpMethod method)
|
||||
{
|
||||
// The asterisk-form of request-target is only used for a server-wide
|
||||
// OPTIONS request (https://tools.ietf.org/html/rfc7231#section-4.3.7).
|
||||
if (method != HttpMethod.Options)
|
||||
{
|
||||
RejectRequest(RequestRejectionReason.OptionsMethodRequired);
|
||||
}
|
||||
|
||||
RawTarget = Asterisk;
|
||||
Path = string.Empty;
|
||||
PathBase = string.Empty;
|
||||
QueryString = string.Empty;
|
||||
}
|
||||
|
||||
private void OnAbsoluteFormTarget(Span<byte> target, Span<byte> query, Span<byte> line)
|
||||
{
|
||||
// absolute-form
|
||||
// https://tools.ietf.org/html/rfc7230#section-5.3.2
|
||||
|
||||
// This code should be the edge-case.
|
||||
|
||||
// From the spec:
|
||||
// a server MUST accept the absolute-form in requests, even though
|
||||
// HTTP/1.1 clients will only send them in requests to proxies.
|
||||
|
||||
RawTarget = target.GetAsciiStringNonNullCharacters();
|
||||
|
||||
// Validation of absolute URIs is slow, but clients
|
||||
// should not be sending this form anyways, so perf optimization
|
||||
// not high priority
|
||||
|
||||
if (!Uri.TryCreate(RawTarget, UriKind.Absolute, out var uri))
|
||||
{
|
||||
if (Log.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
RejectRequestLine(line);
|
||||
}
|
||||
|
||||
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine);
|
||||
}
|
||||
|
||||
SetNormalizedPath(uri.LocalPath);
|
||||
// don't use uri.Query because we need the unescaped version
|
||||
QueryString = query.GetAsciiStringNonNullCharacters();
|
||||
}
|
||||
|
||||
private void SetNormalizedPath(string requestPath)
|
||||
{
|
||||
var normalizedTarget = PathNormalizer.RemoveDotSegments(requestPath);
|
||||
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
|
||||
else
|
||||
{
|
||||
Path = normalizedTarget;
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
private unsafe static string GetUtf8String(Span<byte> path)
|
||||
{
|
||||
// .NET 451 doesn't have pointer overloads for Encoding.GetString so we
|
||||
// copy to an array
|
||||
#if NET451
|
||||
return Encoding.UTF8.GetString(path.ToArray());
|
||||
#else
|
||||
fixed (byte* pointer = &path.DangerousGetPinnableReference())
|
||||
{
|
||||
Path = string.Empty;
|
||||
PathBase = string.Empty;
|
||||
QueryString = string.Empty;
|
||||
return Encoding.UTF8.GetString(pointer, path.Length);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||
{
|
||||
public enum HttpScheme
|
||||
{
|
||||
Unknown = -1,
|
||||
Http = 0,
|
||||
Https = 1
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,6 @@ 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);
|
||||
void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, Span<byte> line, bool pathEncoded);
|
||||
}
|
||||
}
|
||||
|
|
@ -34,219 +34,132 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
consumed = buffer.Start;
|
||||
examined = buffer.End;
|
||||
|
||||
ReadCursor end;
|
||||
Span<byte> span;
|
||||
|
||||
// If the buffer is a single span then use it to find the LF
|
||||
if (buffer.IsSingleSpan)
|
||||
// Prepare the first span
|
||||
var span = buffer.First.Span;
|
||||
var lineIndex = span.IndexOfVectorized(ByteLF);
|
||||
if (lineIndex >= 0)
|
||||
{
|
||||
var startLineSpan = buffer.First.Span;
|
||||
var lineIndex = startLineSpan.IndexOfVectorized(ByteLF);
|
||||
|
||||
if (lineIndex == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
end = buffer.Move(consumed, lineIndex + 1);
|
||||
span = startLineSpan.Slice(0, lineIndex + 1);
|
||||
consumed = buffer.Move(consumed, lineIndex + 1);
|
||||
span = span.Slice(0, lineIndex + 1);
|
||||
}
|
||||
else
|
||||
else if (buffer.IsSingleSpan || !TryGetNewLineSpan(ref buffer, ref span, out consumed))
|
||||
{
|
||||
var start = buffer.Start;
|
||||
if (ReadCursorOperations.Seek(start, buffer.End, out end, ByteLF) == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Move 1 byte past the \n
|
||||
end = buffer.Move(end, 1);
|
||||
var startLineBuffer = buffer.Slice(start, end);
|
||||
|
||||
span = startLineBuffer.ToSpan();
|
||||
// No request line end
|
||||
return false;
|
||||
}
|
||||
|
||||
var pathStart = -1;
|
||||
var queryStart = -1;
|
||||
var queryEnd = -1;
|
||||
var pathEnd = -1;
|
||||
var versionStart = -1;
|
||||
|
||||
var httpVersion = HttpVersion.Unknown;
|
||||
HttpMethod method;
|
||||
Span<byte> customMethod;
|
||||
var i = 0;
|
||||
var length = span.Length;
|
||||
var done = false;
|
||||
|
||||
// Fix and parse the span
|
||||
fixed (byte* data = &span.DangerousGetPinnableReference())
|
||||
{
|
||||
switch (StartLineState.KnownMethod)
|
||||
ParseRequestLine(handler, data, span.Length);
|
||||
}
|
||||
|
||||
examined = consumed;
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe void ParseRequestLine<T>(T handler, byte* data, int length) where T : IHttpRequestLineHandler
|
||||
{
|
||||
int offset;
|
||||
Span<byte> customMethod;
|
||||
// Get Method and set the offset
|
||||
var method = HttpUtilities.GetKnownMethod(data, length, out offset);
|
||||
if (method == HttpMethod.Custom)
|
||||
{
|
||||
customMethod = GetUnknownMethod(data, length, out offset);
|
||||
}
|
||||
|
||||
// Skip space
|
||||
offset++;
|
||||
|
||||
byte ch = 0;
|
||||
// Target = Path and Query
|
||||
var pathEncoded = false;
|
||||
var pathStart = -1;
|
||||
for (; offset < length; offset++)
|
||||
{
|
||||
ch = data[offset];
|
||||
if (ch == ByteSpace)
|
||||
{
|
||||
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;
|
||||
if (pathStart == -1)
|
||||
{
|
||||
// Empty path is illegal
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
|
||||
goto case StartLineState.Path;
|
||||
}
|
||||
goto case StartLineState.UnknownMethod;
|
||||
break;
|
||||
}
|
||||
else if (ch == ByteQuestionMark)
|
||||
{
|
||||
if (pathStart == -1)
|
||||
{
|
||||
// Empty path is illegal
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
|
||||
case StartLineState.UnknownMethod:
|
||||
for (; i < length; i++)
|
||||
{
|
||||
var ch = data[i];
|
||||
break;
|
||||
}
|
||||
else if (ch == BytePercentage)
|
||||
{
|
||||
if (pathStart == -1)
|
||||
{
|
||||
// Path starting with % is illegal
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
|
||||
if (ch == ByteSpace)
|
||||
{
|
||||
customMethod = span.Slice(0, i);
|
||||
|
||||
if (customMethod.Length == 0)
|
||||
{
|
||||
RejectRequestLine(span);
|
||||
}
|
||||
// Consume space
|
||||
i++;
|
||||
|
||||
goto case StartLineState.Path;
|
||||
}
|
||||
|
||||
if (!IsValidTokenChar((char)ch))
|
||||
{
|
||||
RejectRequestLine(span);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case StartLineState.Path:
|
||||
for (; i < length; i++)
|
||||
{
|
||||
var ch = data[i];
|
||||
if (ch == ByteSpace)
|
||||
{
|
||||
pathEnd = i;
|
||||
|
||||
if (pathStart == -1)
|
||||
{
|
||||
// Empty path is illegal
|
||||
RejectRequestLine(span);
|
||||
}
|
||||
|
||||
// No query string found
|
||||
queryStart = queryEnd = i;
|
||||
|
||||
// Consume space
|
||||
i++;
|
||||
|
||||
goto case StartLineState.KnownVersion;
|
||||
}
|
||||
else if (ch == ByteQuestionMark)
|
||||
{
|
||||
pathEnd = i;
|
||||
|
||||
if (pathStart == -1)
|
||||
{
|
||||
// Empty path is illegal
|
||||
RejectRequestLine(span);
|
||||
}
|
||||
|
||||
queryStart = i;
|
||||
goto case StartLineState.QueryString;
|
||||
}
|
||||
else if (ch == BytePercentage)
|
||||
{
|
||||
if (pathStart == -1)
|
||||
{
|
||||
RejectRequestLine(span);
|
||||
}
|
||||
}
|
||||
|
||||
if (pathStart == -1)
|
||||
{
|
||||
pathStart = i;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case StartLineState.QueryString:
|
||||
for (; i < length; i++)
|
||||
{
|
||||
var ch = data[i];
|
||||
if (ch == ByteSpace)
|
||||
{
|
||||
queryEnd = i;
|
||||
|
||||
// Consume space
|
||||
i++;
|
||||
|
||||
goto case 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;
|
||||
goto case StartLineState.NewLine;
|
||||
}
|
||||
|
||||
versionStart = i;
|
||||
|
||||
goto case StartLineState.UnknownVersion;
|
||||
|
||||
case StartLineState.UnknownVersion:
|
||||
for (; i < length; i++)
|
||||
{
|
||||
var ch = data[i];
|
||||
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 (data[i] != ByteLF)
|
||||
{
|
||||
RejectRequestLine(span);
|
||||
}
|
||||
i++;
|
||||
|
||||
goto case StartLineState.Complete;
|
||||
case StartLineState.Complete:
|
||||
done = true;
|
||||
break;
|
||||
pathEncoded = true;
|
||||
}
|
||||
else if (pathStart == -1)
|
||||
{
|
||||
pathStart = offset;
|
||||
}
|
||||
}
|
||||
|
||||
if (!done)
|
||||
if (pathStart == -1)
|
||||
{
|
||||
RejectRequestLine(span);
|
||||
// End of path not found
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
|
||||
var pathBuffer = span.Slice(pathStart, pathEnd - pathStart);
|
||||
var targetBuffer = span.Slice(pathStart, queryEnd - pathStart);
|
||||
var query = span.Slice(queryStart, queryEnd - queryStart);
|
||||
var pathBuffer = new Span<byte>(data + pathStart, offset - pathStart);
|
||||
|
||||
handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod);
|
||||
var queryStart = offset;
|
||||
// Query string
|
||||
if (ch == ByteQuestionMark)
|
||||
{
|
||||
// We have a query string
|
||||
for (; offset < length; offset++)
|
||||
{
|
||||
ch = data[offset];
|
||||
if (ch == ByteSpace)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
consumed = end;
|
||||
examined = consumed;
|
||||
return true;
|
||||
var targetBuffer = new Span<byte>(data + pathStart, offset - pathStart);
|
||||
var query = new Span<byte>(data + queryStart, offset - queryStart);
|
||||
|
||||
// Consume space
|
||||
offset++;
|
||||
|
||||
// Version
|
||||
var httpVersion = HttpUtilities.GetKnownVersion(data + offset, length - offset);
|
||||
if (httpVersion == HttpVersion.Unknown)
|
||||
{
|
||||
RejectUnknownVersion(data, length, offset);
|
||||
}
|
||||
|
||||
// After version 8 bytes and cr 1 byte, expect lf
|
||||
if (data[offset + 8 + 1] != ByteLF)
|
||||
{
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
|
||||
var line = new Span<byte>(data, length);
|
||||
|
||||
handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod, line, pathEncoded);
|
||||
}
|
||||
|
||||
public unsafe bool ParseHeaders<T>(T handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes) where T : IHttpHeadersHandler
|
||||
|
|
@ -316,11 +229,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
}
|
||||
|
||||
// Headers don't end in CRLF line.
|
||||
RejectRequest(RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence);
|
||||
}
|
||||
else if(ch1 == ByteSpace || ch1 == ByteTab)
|
||||
{
|
||||
RejectRequest(RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName);
|
||||
RejectRequest(RequestRejectionReason.InvalidRequestHeadersNoCRLF);
|
||||
}
|
||||
|
||||
// We moved the reader so look ahead 2 bytes so reset both the reader
|
||||
|
|
@ -390,7 +299,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static unsafe int FindEndOfName(byte* headerLine, int length)
|
||||
private unsafe int FindEndOfName(byte* headerLine, int length)
|
||||
{
|
||||
var index = 0;
|
||||
var sawWhitespace = false;
|
||||
|
|
@ -407,14 +316,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
}
|
||||
}
|
||||
|
||||
if (index == length)
|
||||
if (index == length || sawWhitespace)
|
||||
{
|
||||
RejectRequest(RequestRejectionReason.NoColonCharacterFoundInHeaderLine);
|
||||
}
|
||||
if (sawWhitespace)
|
||||
{
|
||||
RejectRequest(RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName);
|
||||
RejectRequestHeader(headerLine, length);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
|
|
@ -427,17 +333,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
|
||||
if (headerLine[valueEnd + 2] != ByteLF)
|
||||
{
|
||||
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
|
||||
RejectRequestHeader(headerLine, length);
|
||||
}
|
||||
if (headerLine[valueEnd + 1] != ByteCR)
|
||||
{
|
||||
RejectRequest(RequestRejectionReason.MissingCRInHeaderLine);
|
||||
RejectRequestHeader(headerLine, length);
|
||||
}
|
||||
|
||||
// Skip colon from value start
|
||||
var valueStart = nameEnd + 1;
|
||||
// Ignore start whitespace
|
||||
for(; valueStart < valueEnd; valueStart++)
|
||||
for (; valueStart < valueEnd; valueStart++)
|
||||
{
|
||||
var ch = headerLine[valueStart];
|
||||
if (ch != ByteTab && ch != ByteSpace && ch != ByteCR)
|
||||
|
|
@ -446,16 +352,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
}
|
||||
else if (ch == ByteCR)
|
||||
{
|
||||
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
|
||||
RejectRequestHeader(headerLine, length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check for CR in value
|
||||
var i = valueStart + 1;
|
||||
if (Contains(headerLine + i, valueEnd - i, ByteCR))
|
||||
{
|
||||
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
|
||||
RejectRequestHeader(headerLine, length);
|
||||
}
|
||||
|
||||
// Ignore end whitespace
|
||||
|
|
@ -506,10 +411,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
}
|
||||
}
|
||||
return false;
|
||||
found:
|
||||
found:
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static bool TryGetNewLineSpan(ref ReadableBuffer buffer, ref Span<byte> span, out ReadCursor end)
|
||||
{
|
||||
var start = buffer.Start;
|
||||
if (ReadCursorOperations.Seek(start, buffer.End, out end, ByteLF) != -1)
|
||||
{
|
||||
// Move 1 byte past the \n
|
||||
end = buffer.Move(end, 1);
|
||||
span = buffer.Slice(start, end).ToSpan();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe Span<byte> GetUnknownMethod(byte* data, int length, out int methodLength)
|
||||
{
|
||||
methodLength = 0;
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
var ch = data[i];
|
||||
|
||||
if (ch == ByteSpace)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
|
||||
methodLength = i;
|
||||
break;
|
||||
}
|
||||
else if (!IsValidTokenChar((char)ch))
|
||||
{
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
}
|
||||
|
||||
return new Span<byte>(data, methodLength);
|
||||
}
|
||||
|
||||
private static bool IsValidTokenChar(char c)
|
||||
{
|
||||
// Determines if a character is valid as a 'token' as defined in the
|
||||
|
|
@ -540,9 +487,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
throw BadHttpRequestException.GetException(reason);
|
||||
}
|
||||
|
||||
public static void RejectRequest(RequestRejectionReason reason, string value)
|
||||
private unsafe void RejectUnknownVersion(byte* data, int length, int versionStart)
|
||||
{
|
||||
throw BadHttpRequestException.GetException(reason, value);
|
||||
throw GetRejectUnknownVersion(data, length, versionStart);
|
||||
}
|
||||
|
||||
private unsafe void RejectRequestLine(byte* data, int length)
|
||||
{
|
||||
throw GetRejectRequestLineException(new Span<byte>(data, length));
|
||||
}
|
||||
|
||||
private void RejectRequestLine(Span<byte> span)
|
||||
|
|
@ -557,9 +509,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
Log.IsEnabled(LogLevel.Information) ? span.GetAsciiStringEscaped(MaxRequestLineError) : string.Empty);
|
||||
}
|
||||
|
||||
private unsafe BadHttpRequestException GetRejectUnknownVersion(byte* data, int length, int versionStart)
|
||||
{
|
||||
var span = new Span<byte>(data, length);
|
||||
length -= versionStart;
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
var ch = span[i + versionStart];
|
||||
if (ch == ByteCR)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
return GetRejectRequestLineException(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
return BadHttpRequestException.GetException(RequestRejectionReason.UnrecognizedHTTPVersion,
|
||||
span.Slice(versionStart, i).GetAsciiStringEscaped(32));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GetRejectRequestLineException(span);
|
||||
}
|
||||
|
||||
private unsafe void RejectRequestHeader(byte* headerLine, int length)
|
||||
{
|
||||
RejectRequestHeader(new Span<byte>(headerLine, length));
|
||||
}
|
||||
|
||||
private void RejectRequestHeader(Span<byte> span)
|
||||
{
|
||||
throw GetRejectRequestHeaderException(span);
|
||||
}
|
||||
|
||||
private BadHttpRequestException GetRejectRequestHeaderException(Span<byte> span)
|
||||
{
|
||||
const int MaxRequestHeaderError = 128;
|
||||
return BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestHeader,
|
||||
Log.IsEnabled(LogLevel.Information) ? span.GetAsciiStringEscaped(MaxRequestHeaderError) : string.Empty);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
@ -570,26 +562,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
// https://github.com/dotnet/coreclr/issues/7459#issuecomment-253965670
|
||||
return Vector.AsVectorByte(new Vector<uint>(vectorByte * 0x01010101u));
|
||||
}
|
||||
|
||||
private enum HeaderState
|
||||
{
|
||||
Name,
|
||||
Whitespace,
|
||||
ExpectValue,
|
||||
ExpectNewLine,
|
||||
Complete
|
||||
}
|
||||
|
||||
private enum StartLineState
|
||||
{
|
||||
KnownMethod,
|
||||
UnknownMethod,
|
||||
Path,
|
||||
QueryString,
|
||||
KnownVersion,
|
||||
UnknownVersion,
|
||||
NewLine,
|
||||
Complete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
public enum RequestRejectionReason
|
||||
{
|
||||
UnrecognizedHTTPVersion,
|
||||
HeadersCorruptedInvalidHeaderSequence,
|
||||
NoColonCharacterFoundInHeaderLine,
|
||||
WhitespaceIsNotAllowedInHeaderName,
|
||||
HeaderValueMustNotContainCR,
|
||||
HeaderValueLineFoldingNotSupported,
|
||||
InvalidRequestLine,
|
||||
InvalidRequestHeader,
|
||||
InvalidRequestHeadersNoCRLF,
|
||||
MalformedRequestInvalidHeaders,
|
||||
InvalidContentLength,
|
||||
MultipleContentLengths,
|
||||
|
|
@ -19,16 +16,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
BadChunkSuffix,
|
||||
BadChunkSizeData,
|
||||
ChunkedRequestIncomplete,
|
||||
PathContainsNullCharacters,
|
||||
InvalidCharactersInHeaderName,
|
||||
NonAsciiOrNullCharactersInInputString,
|
||||
RequestLineTooLong,
|
||||
HeadersExceedMaxTotalSize,
|
||||
MissingCRInHeaderLine,
|
||||
TooManyHeaders,
|
||||
RequestTimeout,
|
||||
FinalTransferCodingNotChunked,
|
||||
LengthRequired,
|
||||
LengthRequiredHttp10
|
||||
LengthRequiredHttp10,
|
||||
OptionsMethodRequired,
|
||||
ConnectMethodRequired,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
|||
private readonly static ulong _mask4Chars = GetMaskAsLong(new byte[]
|
||||
{0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00});
|
||||
|
||||
private readonly static Tuple<ulong, ulong, HttpMethod, int, bool>[] _knownMethods =
|
||||
new Tuple<ulong, ulong, HttpMethod, int, bool>[17];
|
||||
private readonly static Tuple<ulong, ulong, HttpMethod, int>[] _knownMethods =
|
||||
new Tuple<ulong, ulong, HttpMethod, int>[17];
|
||||
|
||||
private readonly static string[] _methodNames = new string[9];
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Diagnostics;
|
||||
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
|
||||
|
|
@ -13,34 +14,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
|||
{
|
||||
public const string Http10Version = "HTTP/1.0";
|
||||
public const string Http11Version = "HTTP/1.1";
|
||||
private const uint _httpGetMethodInt = 542393671; // retun of GetAsciiStringAsInt("GET "); const results in better codegen
|
||||
|
||||
public const string HttpUriScheme = "http://";
|
||||
public const string HttpsUriScheme = "https://";
|
||||
|
||||
// readonly primitive statics can be Jit'd to consts https://github.com/dotnet/coreclr/issues/1079
|
||||
private readonly static ulong _httpSchemeLong = GetAsciiStringAsLong(HttpUriScheme + "\0");
|
||||
private readonly static ulong _httpsSchemeLong = GetAsciiStringAsLong(HttpsUriScheme);
|
||||
|
||||
private const uint _httpGetMethodInt = 542393671; // retun of GetAsciiStringAsInt("GET "); const results in better codegen
|
||||
|
||||
private const ulong _http10VersionLong = 3471766442030158920; // GetAsciiStringAsLong("HTTP/1.0"); const results in better codegen
|
||||
private const ulong _http11VersionLong = 3543824036068086856; // GetAsciiStringAsLong("HTTP/1.1"); const results in better codegen
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void SetKnownMethod(ulong mask, ulong knownMethodUlong, HttpMethod knownMethod, int length)
|
||||
{
|
||||
_knownMethods[GetKnownMethodIndex(knownMethodUlong)] = new Tuple<ulong, ulong, HttpMethod, int, bool>(mask, knownMethodUlong, knownMethod, length, true);
|
||||
}
|
||||
|
||||
private unsafe static ulong GetMaskAsLong(byte[] bytes)
|
||||
{
|
||||
Debug.Assert(bytes.Length == 8, "Mask must be exactly 8 bytes long.");
|
||||
|
||||
fixed (byte* ptr = bytes)
|
||||
{
|
||||
return *(ulong*)ptr;
|
||||
}
|
||||
_knownMethods[GetKnownMethodIndex(knownMethodUlong)] = new Tuple<ulong, ulong, HttpMethod, int>(mask, knownMethodUlong, knownMethod, length);
|
||||
}
|
||||
|
||||
private static void FillKnownMethodsGaps()
|
||||
{
|
||||
var knownMethods = _knownMethods;
|
||||
var length = knownMethods.Length;
|
||||
var invalidHttpMethod = new Tuple<ulong, ulong, HttpMethod, int, bool>(_mask8Chars, 0ul, HttpMethod.Custom, 0, false);
|
||||
var invalidHttpMethod = new Tuple<ulong, ulong, HttpMethod, int>(_mask8Chars, 0ul, HttpMethod.Custom, 0);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
if (knownMethods[i] == null)
|
||||
|
|
@ -74,7 +71,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
|||
}
|
||||
}
|
||||
|
||||
private unsafe static ulong GetMaskAsLong(byte[] bytes)
|
||||
{
|
||||
Debug.Assert(bytes.Length == 8, "Mask must be exactly 8 bytes long.");
|
||||
|
||||
fixed (byte* ptr = bytes)
|
||||
{
|
||||
return *(ulong*)ptr;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe static string GetAsciiStringNonNullCharacters(this Span<byte> span)
|
||||
{
|
||||
|
|
@ -106,7 +111,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
|||
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());
|
||||
sb.Append(ch < 0x20 || ch >= 0x7F ? $"\\x{ch:X2}" : ((char)ch).ToString());
|
||||
}
|
||||
|
||||
if (span.Length > maxChars)
|
||||
|
|
@ -129,35 +134,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
|||
/// </remarks>
|
||||
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool GetKnownMethod(this Span<byte> span, out HttpMethod method, out int length)
|
||||
public static unsafe bool GetKnownMethod(this Span<byte> span, out HttpMethod method, out int length)
|
||||
{
|
||||
if (span.TryRead<uint>(out var possiblyGet))
|
||||
fixed (byte* data = &span.DangerousGetPinnableReference())
|
||||
{
|
||||
if (possiblyGet == _httpGetMethodInt)
|
||||
{
|
||||
length = 3;
|
||||
method = HttpMethod.Get;
|
||||
return true;
|
||||
}
|
||||
method = GetKnownMethod(data, span.Length, out length);
|
||||
return method != HttpMethod.Custom;
|
||||
}
|
||||
}
|
||||
|
||||
if (span.TryRead<ulong>(out var value))
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal unsafe static HttpMethod GetKnownMethod(byte* data, int length, out int methodLength)
|
||||
{
|
||||
methodLength = 0;
|
||||
if (length < sizeof(uint))
|
||||
{
|
||||
return HttpMethod.Custom;
|
||||
}
|
||||
else if (*(uint*)data == _httpGetMethodInt)
|
||||
{
|
||||
methodLength = 3;
|
||||
return HttpMethod.Get;
|
||||
}
|
||||
else if (length < sizeof(ulong))
|
||||
{
|
||||
return HttpMethod.Custom;
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = *(ulong*)data;
|
||||
var key = GetKnownMethodIndex(value);
|
||||
|
||||
var x = _knownMethods[key];
|
||||
|
||||
if (x != null && (value & x.Item1) == x.Item2)
|
||||
{
|
||||
method = x.Item3;
|
||||
length = x.Item4;
|
||||
return x.Item5;
|
||||
methodLength = x.Item4;
|
||||
return x.Item3;
|
||||
}
|
||||
}
|
||||
|
||||
method = HttpMethod.Custom;
|
||||
length = 0;
|
||||
return false;
|
||||
return HttpMethod.Custom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -172,35 +188,83 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
|||
/// </remarks>
|
||||
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool GetKnownVersion(this Span<byte> span, out HttpVersion knownVersion, out byte length)
|
||||
public static unsafe bool GetKnownVersion(this Span<byte> span, out HttpVersion knownVersion, out byte length)
|
||||
{
|
||||
if (span.TryRead<ulong>(out var version))
|
||||
fixed (byte* data = &span.DangerousGetPinnableReference())
|
||||
{
|
||||
if (version == _http11VersionLong)
|
||||
knownVersion = GetKnownVersion(data, span.Length);
|
||||
if (knownVersion != HttpVersion.Unknown)
|
||||
{
|
||||
length = sizeof(ulong);
|
||||
knownVersion = HttpVersion.Http11;
|
||||
}
|
||||
else if (version == _http10VersionLong)
|
||||
{
|
||||
length = sizeof(ulong);
|
||||
knownVersion = HttpVersion.Http10;
|
||||
}
|
||||
else
|
||||
{
|
||||
length = 0;
|
||||
knownVersion = HttpVersion.Unknown;
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (span[sizeof(ulong)] == (byte)'\r')
|
||||
length = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks 9 bytes from <paramref name="location"/> correspond to a known HTTP version.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A "known HTTP version" Is is either HTTP/1.0 or HTTP/1.1.
|
||||
/// Since those fit in 8 bytes, they can be optimally looked up by reading those bytes as a long. Once
|
||||
/// in that format, it can be checked against the known versions.
|
||||
/// The Known versions will be checked with the required '\r'.
|
||||
/// To optimize performance the HTTP/1.1 will be checked first.
|
||||
/// </remarks>
|
||||
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal unsafe static HttpVersion GetKnownVersion(byte* location, int length)
|
||||
{
|
||||
HttpVersion knownVersion;
|
||||
var version = *(ulong*)location;
|
||||
if (length < sizeof(ulong) + 1 || location[sizeof(ulong)] != (byte)'\r')
|
||||
{
|
||||
knownVersion = HttpVersion.Unknown;
|
||||
}
|
||||
else if (version == _http11VersionLong)
|
||||
{
|
||||
knownVersion = HttpVersion.Http11;
|
||||
}
|
||||
else if (version == _http10VersionLong)
|
||||
{
|
||||
knownVersion = HttpVersion.Http10;
|
||||
}
|
||||
else
|
||||
{
|
||||
knownVersion = HttpVersion.Unknown;
|
||||
}
|
||||
|
||||
return knownVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks 8 bytes from <paramref name="span"/> that correspond to 'http://' or 'https://'
|
||||
/// </summary>
|
||||
/// <param name="span">The span</param>
|
||||
/// <param name="knownScheme">A reference to the known scheme, if the input matches any</param>
|
||||
/// <returns>True when memory starts with known http or https schema</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool GetKnownHttpScheme(this Span<byte> span, out HttpScheme knownScheme)
|
||||
{
|
||||
if (span.TryRead<ulong>(out var value))
|
||||
{
|
||||
if ((value & _mask7Chars) == _httpSchemeLong)
|
||||
{
|
||||
knownScheme = HttpScheme.Http;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value == _httpsSchemeLong)
|
||||
{
|
||||
knownScheme = HttpScheme.Https;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
knownVersion = HttpVersion.Unknown;
|
||||
length = 0;
|
||||
knownScheme = HttpScheme.Unknown;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -225,5 +289,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string SchemeToString(HttpScheme scheme)
|
||||
{
|
||||
switch (scheme)
|
||||
{
|
||||
case HttpScheme.Http:
|
||||
return HttpUriScheme;
|
||||
case HttpScheme.Https:
|
||||
return HttpsUriScheme;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
// 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.Infrastructure
|
||||
{
|
||||
public class UriUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if character is valid in the 'authority' section of a URI.
|
||||
/// <see href="https://tools.ietf.org/html/rfc3986#section-3.2"/>
|
||||
/// </summary>
|
||||
/// <param name="ch">The character</param>
|
||||
/// <returns></returns>
|
||||
public static bool IsValidAuthorityCharacter(byte ch)
|
||||
{
|
||||
// Examples:
|
||||
// microsoft.com
|
||||
// hostname:8080
|
||||
// [::]:8080
|
||||
// [fe80::]
|
||||
// 127.0.0.1
|
||||
// user@host.com
|
||||
// user:password@host.com
|
||||
return
|
||||
(ch >= '0' && ch <= '9') ||
|
||||
(ch >= 'A' && ch <= 'Z') ||
|
||||
(ch >= 'a' && ch <= 'z') ||
|
||||
ch == ':' ||
|
||||
ch == '.' ||
|
||||
ch == '[' ||
|
||||
ch == ']' ||
|
||||
ch == '@';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,6 @@
|
|||
<PackageReference Include="System.Numerics.Vectors" Version="$(CoreFxVersion)" />
|
||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="$(CoreFxVersion)" />
|
||||
<PackageReference Include="System.IO.Pipelines" Version="$(CoreFxLabsPipelinesVersion)" />
|
||||
<PackageReference Include="System.IO.Pipelines.Text.Primitives" Version="$(CoreFxLabsVersion)" />
|
||||
<PackageReference Include="System.Text.Encodings.Web.Utf8" Version="$(CoreFxLabsVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
|
|
@ -90,7 +91,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
$"Invalid content length: {contentLength}");
|
||||
}
|
||||
|
||||
private async Task TestBadRequest(string request, string expectedResponseStatusCode, string expectedExceptionMessage)
|
||||
[Theory]
|
||||
[InlineData("GET *", "OPTIONS")]
|
||||
[InlineData("GET www.host.com", "CONNECT")]
|
||||
public Task RejectsIncorrectMethods(string request, string allowedMethod)
|
||||
{
|
||||
return TestBadRequest(
|
||||
$"{request} HTTP/1.1\r\n",
|
||||
"405 Method Not Allowed",
|
||||
"Method not allowed.",
|
||||
$"Allow: {allowedMethod}");
|
||||
}
|
||||
|
||||
private async Task TestBadRequest(string request, string expectedResponseStatusCode, string expectedExceptionMessage, string expectedAllowHeader = null)
|
||||
{
|
||||
BadHttpRequestException loggedException = null;
|
||||
var mockKestrelTrace = new Mock<IKestrelTrace>();
|
||||
|
|
@ -106,7 +119,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
await connection.SendAll(request);
|
||||
await ReceiveBadRequestResponse(connection, expectedResponseStatusCode, server.Context.DateHeaderValue);
|
||||
await ReceiveBadRequestResponse(connection, expectedResponseStatusCode, server.Context.DateHeaderValue, expectedAllowHeader);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,15 +127,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
Assert.Equal(expectedExceptionMessage, loggedException.Message);
|
||||
}
|
||||
|
||||
private async Task ReceiveBadRequestResponse(TestConnection connection, string expectedResponseStatusCode, string expectedDateHeaderValue)
|
||||
private async Task ReceiveBadRequestResponse(TestConnection connection, string expectedResponseStatusCode, string expectedDateHeaderValue, string expectedAllowHeader = null)
|
||||
{
|
||||
await connection.ReceiveForcedEnd(
|
||||
var lines = new[]
|
||||
{
|
||||
$"HTTP/1.1 {expectedResponseStatusCode}",
|
||||
"Connection: close",
|
||||
$"Date: {expectedDateHeaderValue}",
|
||||
"Content-Length: 0",
|
||||
expectedAllowHeader,
|
||||
"",
|
||||
"");
|
||||
""
|
||||
};
|
||||
|
||||
await connection.ReceiveForcedEnd(lines.Where(f => f != null).ToArray());
|
||||
}
|
||||
|
||||
public static TheoryData<string, string> InvalidRequestLineData
|
||||
|
|
@ -131,9 +149,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
{
|
||||
var data = new TheoryData<string, string>();
|
||||
|
||||
string Escape(string line)
|
||||
{
|
||||
return line
|
||||
.Replace("\r", @"\x0D")
|
||||
.Replace("\n", @"\x0A")
|
||||
.Replace("\0", @"\x00");
|
||||
}
|
||||
|
||||
foreach (var requestLine in HttpParsingData.RequestLineInvalidData)
|
||||
{
|
||||
data.Add(requestLine, $"Invalid request line: {requestLine.Replace("\r", "<0x0D>").Replace("\n", "<0x0A>")}");
|
||||
data.Add(requestLine, $"Invalid request line: '{Escape(requestLine)}'");
|
||||
}
|
||||
|
||||
foreach (var requestLine in HttpParsingData.RequestLineWithEncodedNullCharInTargetData)
|
||||
|
|
@ -143,7 +169,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
|
||||
foreach (var requestLine in HttpParsingData.RequestLineWithNullCharInTargetData)
|
||||
{
|
||||
data.Add(requestLine, "Invalid request line.");
|
||||
data.Add(requestLine, $"Invalid request line.");
|
||||
}
|
||||
|
||||
return data;
|
||||
|
|
|
|||
|
|
@ -454,6 +454,72 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://localhost/abs/path", "/abs/path", null)]
|
||||
[InlineData("https://localhost/abs/path", "/abs/path", null)] // handles mismatch scheme
|
||||
[InlineData("https://localhost:22/abs/path", "/abs/path", null)] // handles mismatched ports
|
||||
[InlineData("https://differenthost/abs/path", "/abs/path", null)] // handles mismatched hostname
|
||||
[InlineData("http://localhost/", "/", null)]
|
||||
[InlineData("http://root@contoso.com/path", "/path", null)]
|
||||
[InlineData("http://root:password@contoso.com/path", "/path", null)]
|
||||
[InlineData("https://localhost/", "/", null)]
|
||||
[InlineData("http://localhost", "/", null)]
|
||||
[InlineData("http://127.0.0.1/", "/", null)]
|
||||
[InlineData("http://[::1]/", "/", null)]
|
||||
[InlineData("http://[::1]:8080/", "/", null)]
|
||||
[InlineData("http://localhost?q=123&w=xyz", "/", "123")]
|
||||
[InlineData("http://localhost/?q=123&w=xyz", "/", "123")]
|
||||
[InlineData("http://localhost/path?q=123&w=xyz", "/path", "123")]
|
||||
[InlineData("http://localhost/path%20with%20space?q=abc%20123", "/path with space", "abc 123")]
|
||||
public async Task CanHandleRequestsWithUrlInAbsoluteForm(string requestUrl, string expectedPath, string queryValue)
|
||||
{
|
||||
var pathTcs = new TaskCompletionSource<PathString>();
|
||||
var rawTargetTcs = new TaskCompletionSource<string>();
|
||||
var hostTcs = new TaskCompletionSource<HostString>();
|
||||
var queryTcs = new TaskCompletionSource<IQueryCollection>();
|
||||
|
||||
using (var server = new TestServer(async context =>
|
||||
{
|
||||
pathTcs.TrySetResult(context.Request.Path);
|
||||
hostTcs.TrySetResult(context.Request.Host);
|
||||
queryTcs.TrySetResult(context.Request.Query);
|
||||
rawTargetTcs.TrySetResult(context.Features.Get<IHttpRequestFeature>().RawTarget);
|
||||
await context.Response.WriteAsync("Done");
|
||||
}))
|
||||
{
|
||||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
await connection.Send(
|
||||
$"GET {requestUrl} HTTP/1.1",
|
||||
"Content-Length: 0",
|
||||
"Host: localhost",
|
||||
"",
|
||||
"");
|
||||
|
||||
await connection.Receive($"HTTP/1.1 200 OK",
|
||||
$"Date: {server.Context.DateHeaderValue}",
|
||||
"Transfer-Encoding: chunked",
|
||||
"",
|
||||
"4",
|
||||
"Done")
|
||||
.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
await Task.WhenAll(pathTcs.Task, rawTargetTcs.Task, hostTcs.Task, queryTcs.Task).TimeoutAfter(TimeSpan.FromSeconds(30));
|
||||
Assert.Equal(new PathString(expectedPath), pathTcs.Task.Result);
|
||||
Assert.Equal(requestUrl, rawTargetTcs.Task.Result);
|
||||
Assert.Equal("localhost", hostTcs.Task.Result.ToString());
|
||||
if (queryValue == null)
|
||||
{
|
||||
Assert.False(queryTcs.Task.Result.ContainsKey("q"));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(queryValue, queryTcs.Task.Result["q"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
|
||||
private class NullParser : IHttpParser
|
||||
{
|
||||
private readonly byte[] _startLine = Encoding.ASCII.GetBytes("GET /plaintext HTTP/1.1\r\n");
|
||||
private readonly byte[] _target = Encoding.ASCII.GetBytes("/plaintext");
|
||||
private readonly byte[] _hostHeaderName = Encoding.ASCII.GetBytes("Host");
|
||||
private readonly byte[] _hostHeaderValue = Encoding.ASCII.GetBytes("www.example.com");
|
||||
|
|
@ -119,7 +120,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
|
||||
public bool ParseRequestLine<T>(T handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined) where T : IHttpRequestLineHandler
|
||||
{
|
||||
handler.OnStartLine(HttpMethod.Get, HttpVersion.Http11, new Span<byte>(_target), new Span<byte>(_target), Span<byte>.Empty, Span<byte>.Empty);
|
||||
handler.OnStartLine(HttpMethod.Get,
|
||||
HttpVersion.Http11,
|
||||
new Span<byte>(_target),
|
||||
new Span<byte>(_target),
|
||||
Span<byte>.Empty,
|
||||
Span<byte>.Empty,
|
||||
new Span<byte>(_startLine),
|
||||
false);
|
||||
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.End;
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
}
|
||||
}
|
||||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod)
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, Span<byte> line, bool pathEncoded)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
}
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
|
||||
public void PlaintextAbsoluteUri()
|
||||
{
|
||||
for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
|
||||
{
|
||||
InsertData(RequestParsingData.PlaintextAbsoluteUriRequest);
|
||||
ParseData();
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount * RequestParsingData.Pipelining)]
|
||||
public void PipelinedPlaintextTechEmpower()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -21,8 +21,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
"Connection: keep-alive\r\n" +
|
||||
"\r\n";
|
||||
|
||||
// edge-casey - client's don't normally send this
|
||||
private const string _plaintextAbsoluteUriRequest =
|
||||
"GET http://localhost/plaintext HTTP/1.1\r\n" +
|
||||
"Host: localhost\r\n" +
|
||||
"Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7\r\n" +
|
||||
"Connection: keep-alive\r\n" +
|
||||
"\r\n";
|
||||
|
||||
private const string _liveaspnetRequest =
|
||||
"GET https://live.asp.net/ HTTP/1.1\r\n" +
|
||||
"GET / HTTP/1.1\r\n" +
|
||||
"Host: live.asp.net\r\n" +
|
||||
"Connection: keep-alive\r\n" +
|
||||
"Upgrade-Insecure-Requests: 1\r\n" +
|
||||
|
|
@ -35,7 +43,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
"\r\n";
|
||||
|
||||
private const string _unicodeRequest =
|
||||
"GET http://stackoverflow.com/questions/40148683/why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric HTTP/1.1\r\n" +
|
||||
"GET /questions/40148683/why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric HTTP/1.1\r\n" +
|
||||
"Accept: text/html, application/xhtml+xml, image/jxr, */*\r\n" +
|
||||
"Accept-Language: en-US,en-GB;q=0.7,en;q=0.3\r\n" +
|
||||
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.14965\r\n" +
|
||||
|
|
@ -53,6 +61,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
public static readonly byte[] PlaintextTechEmpowerPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_plaintextTechEmpowerRequest, Pipelining)));
|
||||
public static readonly byte[] PlaintextTechEmpowerRequest = Encoding.ASCII.GetBytes(_plaintextTechEmpowerRequest);
|
||||
|
||||
public static readonly byte[] PlaintextAbsoluteUriRequest = Encoding.ASCII.GetBytes(_plaintextAbsoluteUriRequest);
|
||||
|
||||
public static readonly byte[] LiveaspnetPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_liveaspnetRequest, Pipelining)));
|
||||
public static readonly byte[] LiveaspnetRequest = Encoding.ASCII.GetBytes(_liveaspnetRequest);
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
Output = new MockSocketOutput(),
|
||||
ConnectionControl = Mock.Of<IConnectionControl>()
|
||||
};
|
||||
connectionContext.ListenerContext.ServiceContext.HttpParserFactory = f => new Internal.Http.KestrelHttpParser(log: null);
|
||||
|
||||
var frame = new TestFrame<object>(application: null, context: connectionContext);
|
||||
frame.Reset();
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Http.Features;
|
|||
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 Moq;
|
||||
|
|
@ -81,25 +82,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
_pipelineFactory.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TakeMessageHeadersThrowsOnHeaderValueWithLineFolding_CharacterNotAvailableOnFirstAttempt()
|
||||
{
|
||||
await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes("Header-1: value1\r\n"));
|
||||
|
||||
var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
|
||||
Assert.False(_frame.TakeMessageHeaders(readableBuffer, out _consumed, out _examined));
|
||||
_input.Reader.Advance(_consumed, _examined);
|
||||
|
||||
await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes(" "));
|
||||
|
||||
readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
|
||||
var exception = Assert.Throws<BadHttpRequestException>(() => _frame.TakeMessageHeaders(readableBuffer, out _consumed, out _examined));
|
||||
_input.Reader.Advance(_consumed, _examined);
|
||||
|
||||
Assert.Equal("Whitespace is not allowed in header name.", exception.Message);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TakeMessageHeadersThrowsWhenHeadersExceedTotalSizeLimit()
|
||||
{
|
||||
|
|
@ -347,6 +329,37 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
Assert.Equal(new InvalidOperationException().Message, exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RequestLineWithInvalidRequestTargetData))]
|
||||
public async Task TakeStartLineThrowsWhenRequestTargetIsInvalid(string requestLine)
|
||||
{
|
||||
await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes(requestLine));
|
||||
var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
|
||||
|
||||
var exception = Assert.Throws<BadHttpRequestException>(() =>
|
||||
_frame.TakeStartLine(readableBuffer, out _consumed, out _examined));
|
||||
_input.Reader.Advance(_consumed, _examined);
|
||||
|
||||
Assert.Equal($"Invalid request line: '{Escape(requestLine)}'", exception.Message);
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MethodNotAllowedTargetData))]
|
||||
public async Task TakeStartLineThrowsWhenMethodNotAllowed(string requestLine, HttpMethod allowedMethod)
|
||||
{
|
||||
await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes(requestLine));
|
||||
var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
|
||||
|
||||
var exception = Assert.Throws<BadHttpRequestException>(() =>
|
||||
_frame.TakeStartLine(readableBuffer, out _consumed, out _examined));
|
||||
_input.Reader.Advance(_consumed, _examined);
|
||||
|
||||
Assert.Equal(405, exception.StatusCode);
|
||||
Assert.Equal("Method not allowed.", exception.Message);
|
||||
Assert.Equal(HttpUtilities.MethodToString(allowedMethod), exception.AllowedHeader);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequestProcessingAsyncEnablesKeepAliveTimeout()
|
||||
{
|
||||
|
|
@ -535,6 +548,25 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
}
|
||||
}
|
||||
|
||||
private string Escape(string requestLine)
|
||||
{
|
||||
var ellipsis = requestLine.Length > 32
|
||||
? "..."
|
||||
: string.Empty;
|
||||
return requestLine
|
||||
.Substring(0, Math.Min(32, requestLine.Length))
|
||||
.Replace("\r", @"\x0D")
|
||||
.Replace("\n", @"\x0A")
|
||||
.Replace("\0", @"\x00")
|
||||
+ ellipsis;
|
||||
}
|
||||
|
||||
public static TheoryData<string> RequestLineWithInvalidRequestTargetData
|
||||
=> HttpParsingData.RequestLineWithInvalidRequestTarget;
|
||||
|
||||
public static TheoryData<string, HttpMethod> MethodNotAllowedTargetData
|
||||
=> HttpParsingData.MethodNotAllowedRequestLine;
|
||||
|
||||
public static TheoryData<string> RequestLineWithNullCharInTargetData
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -49,14 +49,17 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
It.IsAny<Span<byte>>(),
|
||||
It.IsAny<Span<byte>>(),
|
||||
It.IsAny<Span<byte>>(),
|
||||
It.IsAny<Span<byte>>()))
|
||||
.Callback<HttpMethod, HttpVersion, Span<byte>, Span<byte>, Span<byte>, Span<byte>>((method, version, target, path, query, customMethod) =>
|
||||
It.IsAny<Span<byte>>(),
|
||||
It.IsAny<Span<byte>>(),
|
||||
It.IsAny<bool>()))
|
||||
.Callback<HttpMethod, HttpVersion, Span<byte>, Span<byte>, Span<byte>, Span<byte>, Span<byte>, bool>((method, version, target, path, query, customMethod, line, pathEncoded) =>
|
||||
{
|
||||
parsedMethod = method != HttpMethod.Custom ? HttpUtilities.MethodToString(method) : customMethod.GetAsciiStringNonNullCharacters();
|
||||
parsedVersion = HttpUtilities.VersionToString(version);
|
||||
parsedRawTarget = target.GetAsciiStringNonNullCharacters();
|
||||
parsedRawPath = path.GetAsciiStringNonNullCharacters();
|
||||
parsedQuery = query.GetAsciiStringNonNullCharacters();
|
||||
pathEncoded = false;
|
||||
});
|
||||
|
||||
Assert.True(parser.ParseRequestLine(requestLineHandler.Object, buffer, out var consumed, out var examined));
|
||||
|
|
@ -108,7 +111,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
var exception = Assert.Throws<BadHttpRequestException>(() =>
|
||||
parser.ParseRequestLine(Mock.Of<IHttpRequestLineHandler>(), buffer, out var consumed, out var examined));
|
||||
|
||||
Assert.Equal($"Invalid request line: {requestLine.Replace("\r", "<0x0D>").Replace("\n", "<0x0A>")}", exception.Message);
|
||||
Assert.Equal($"Invalid request line: '{requestLine.Replace("\r", "\\x0D").Replace("\n", "\\x0A")}'", exception.Message);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, (exception as BadHttpRequestException).StatusCode);
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +309,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
[MemberData(nameof(RequestHeaderInvalidData))]
|
||||
public void ParseHeadersThrowsOnInvalidRequestHeaders(string rawHeaders, string expectedExceptionMessage)
|
||||
{
|
||||
var parser = CreateParser(Mock.Of<IKestrelTrace>());
|
||||
var mockTrace = new Mock<IKestrelTrace>();
|
||||
mockTrace
|
||||
.Setup(trace => trace.IsEnabled(LogLevel.Information))
|
||||
.Returns(true);
|
||||
|
||||
var parser = CreateParser(mockTrace.Object);
|
||||
var buffer = ReadableBuffer.Create(Encoding.ASCII.GetBytes(rawHeaders));
|
||||
|
||||
var exception = Assert.Throws<BadHttpRequestException>(() =>
|
||||
|
|
|
|||
|
|
@ -84,11 +84,23 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
{
|
||||
TestKnownStringsInterning(input, expected, span =>
|
||||
{
|
||||
HttpUtilities.GetKnownVersion(span, out var version, out var lenght);
|
||||
HttpUtilities.GetKnownVersion(span, out var version, out var _);
|
||||
return HttpUtilities.VersionToString(version);
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("https://host/", "https://")]
|
||||
[InlineData("http://host/", "http://")]
|
||||
public void KnownSchemesAreInterned(string input, string expected)
|
||||
{
|
||||
TestKnownStringsInterning(input, expected, span =>
|
||||
{
|
||||
HttpUtilities.GetKnownHttpScheme(span, out var scheme);
|
||||
return HttpUtilities.SchemeToString(scheme);
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("CONNECT / HTTP/1.1", "CONNECT")]
|
||||
[InlineData("DELETE / HTTP/1.1", "DELETE")]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -93,19 +95,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("*")]
|
||||
[InlineData("*/?arg=value")]
|
||||
[InlineData("*?arg=value")]
|
||||
[InlineData("DoesNotStartWith/")]
|
||||
[InlineData("DoesNotStartWith/?arg=value")]
|
||||
[InlineData("DoesNotStartWithSlash?arg=value")]
|
||||
[InlineData("./")]
|
||||
[InlineData("../")]
|
||||
[InlineData("../.")]
|
||||
[InlineData(".././")]
|
||||
[InlineData("../..")]
|
||||
[InlineData("../../")]
|
||||
public async Task NonPathRequestTargetSetInRawTarget(string requestTarget)
|
||||
[InlineData(HttpMethod.Options, "*")]
|
||||
[InlineData(HttpMethod.Connect, "host")]
|
||||
public async Task NonPathRequestTargetSetInRawTarget(HttpMethod method, string requestTarget)
|
||||
{
|
||||
var testContext = new TestServiceContext();
|
||||
|
||||
|
|
@ -123,7 +115,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
await connection.Send(
|
||||
$"GET {requestTarget} HTTP/1.1",
|
||||
$"{HttpUtilities.MethodToString(method)} {requestTarget} HTTP/1.1",
|
||||
"",
|
||||
"");
|
||||
await connection.ReceiveEnd(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 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 Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
|
@ -33,6 +34,26 @@ namespace Microsoft.AspNetCore.Testing
|
|||
Tuple.Create("/%C3%A5/bc", "/\u00E5/bc"),
|
||||
Tuple.Create("/%25", "/%"),
|
||||
Tuple.Create("/%2F", "/%2F"),
|
||||
Tuple.Create("http://host/abs/path", "/abs/path"),
|
||||
Tuple.Create("http://host/abs/path/", "/abs/path/"),
|
||||
Tuple.Create("http://host/a%20b%20c/", "/a b c/"),
|
||||
Tuple.Create("https://host/abs/path", "/abs/path"),
|
||||
Tuple.Create("https://host/abs/path/", "/abs/path/"),
|
||||
Tuple.Create("https://host:22/abs/path", "/abs/path"),
|
||||
Tuple.Create("https://user@host:9080/abs/path", "/abs/path"),
|
||||
Tuple.Create("http://host/", "/"),
|
||||
Tuple.Create("http://host", "/"),
|
||||
Tuple.Create("https://host/", "/"),
|
||||
Tuple.Create("https://host", "/"),
|
||||
Tuple.Create("http://user@host/", "/"),
|
||||
Tuple.Create("http://127.0.0.1/", "/"),
|
||||
Tuple.Create("http://user@127.0.0.1/", "/"),
|
||||
Tuple.Create("http://user@127.0.0.1:8080/", "/"),
|
||||
Tuple.Create("http://127.0.0.1:8080/", "/"),
|
||||
Tuple.Create("http://[::1]", "/"),
|
||||
Tuple.Create("http://[::1]/path", "/path"),
|
||||
Tuple.Create("http://[::1]:8080/", "/"),
|
||||
Tuple.Create("http://user@[::1]:8080/", "/"),
|
||||
};
|
||||
var queryStrings = new[]
|
||||
{
|
||||
|
|
@ -173,9 +194,73 @@ namespace Microsoft.AspNetCore.Testing
|
|||
"GET /%E8%01%00 HTTP/1.1\r\n",
|
||||
};
|
||||
|
||||
public static TheoryData<string> RequestLineWithInvalidRequestTarget => new TheoryData<string>
|
||||
{
|
||||
// Invalid absolute-form requests
|
||||
"GET http:// HTTP/1.1\r\n",
|
||||
"GET http:/ HTTP/1.1\r\n",
|
||||
"GET https:/ HTTP/1.1\r\n",
|
||||
"GET http:/// HTTP/1.1\r\n",
|
||||
"GET https:// HTTP/1.1\r\n",
|
||||
"GET http://// HTTP/1.1\r\n",
|
||||
"GET http://:80 HTTP/1.1\r\n",
|
||||
"GET http://:80/abc HTTP/1.1\r\n",
|
||||
"GET http://user@ HTTP/1.1\r\n",
|
||||
"GET http://user@/abc HTTP/1.1\r\n",
|
||||
"GET http://abc%20xyz/abc HTTP/1.1\r\n",
|
||||
"GET http://%20/abc?query=%0A HTTP/1.1\r\n",
|
||||
// Valid absolute-form but with unsupported schemes
|
||||
"GET otherscheme://host/ HTTP/1.1\r\n",
|
||||
"GET ws://host/ HTTP/1.1\r\n",
|
||||
"GET wss://host/ HTTP/1.1\r\n",
|
||||
// Must only have one asterisk
|
||||
"OPTIONS ** HTTP/1.1\r\n",
|
||||
// Relative form
|
||||
"GET ../../ HTTP/1.1\r\n",
|
||||
"GET ..\\. HTTP/1.1\r\n",
|
||||
};
|
||||
|
||||
public static TheoryData<string, HttpMethod> MethodNotAllowedRequestLine
|
||||
{
|
||||
get
|
||||
{
|
||||
var methods = new[]
|
||||
{
|
||||
"GET",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
"POST",
|
||||
"HEAD",
|
||||
"TRACE",
|
||||
"PATCH",
|
||||
"CONNECT",
|
||||
//"OPTIONS",
|
||||
"CUSTOM",
|
||||
};
|
||||
|
||||
var theoryData = new TheoryData<string, HttpMethod>();
|
||||
foreach (var line in methods
|
||||
.Select(m => Tuple.Create($"{m} * HTTP/1.1\r\n", HttpMethod.Options))
|
||||
.Concat(new[]
|
||||
{
|
||||
// CONNECT required for authority-form targets
|
||||
Tuple.Create("GET http:80 HTTP/1.1\r\n", HttpMethod.Connect),
|
||||
Tuple.Create("GET http: HTTP/1.1\r\n", HttpMethod.Connect),
|
||||
Tuple.Create("GET https: HTTP/1.1\r\n", HttpMethod.Connect),
|
||||
Tuple.Create("GET . HTTP/1.1\r\n", HttpMethod.Connect),
|
||||
}))
|
||||
{
|
||||
theoryData.Add(line.Item1, line.Item2);
|
||||
}
|
||||
|
||||
return theoryData;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<string> RequestLineWithNullCharInTargetData => new[]
|
||||
{
|
||||
"GET \0 HTTP/1.1\r\n",
|
||||
// TODO re-enable after we get both #1469 and #1470 merged
|
||||
// "GET \0 HTTP/1.1\r\n",
|
||||
"GET /\0 HTTP/1.1\r\n",
|
||||
"GET /\0\0 HTTP/1.1\r\n",
|
||||
"GET /%C8\0 HTTP/1.1\r\n",
|
||||
|
|
@ -183,6 +268,8 @@ namespace Microsoft.AspNetCore.Testing
|
|||
|
||||
public static TheoryData<string> UnrecognizedHttpVersionData => new TheoryData<string>
|
||||
{
|
||||
" ",
|
||||
"/",
|
||||
"H",
|
||||
"HT",
|
||||
"HTT",
|
||||
|
|
@ -203,106 +290,79 @@ namespace Microsoft.AspNetCore.Testing
|
|||
"8charact",
|
||||
};
|
||||
|
||||
public static IEnumerable<object[]> RequestHeaderInvalidData
|
||||
public static IEnumerable<object[]> RequestHeaderInvalidData => new[]
|
||||
{
|
||||
get
|
||||
{
|
||||
// Line folding
|
||||
var headersWithLineFolding = new[]
|
||||
{
|
||||
"Header: line1\r\n line2\r\n\r\n",
|
||||
"Header: line1\r\n\tline2\r\n\r\n",
|
||||
"Header: line1\r\n line2\r\n\r\n",
|
||||
"Header: line1\r\n \tline2\r\n\r\n",
|
||||
"Header: line1\r\n\t line2\r\n\r\n",
|
||||
"Header: line1\r\n\t\tline2\r\n\r\n",
|
||||
"Header: line1\r\n \t\t line2\r\n\r\n",
|
||||
"Header: line1\r\n \t \t line2\r\n\r\n",
|
||||
"Header-1: multi\r\n line\r\nHeader-2: value2\r\n\r\n",
|
||||
"Header-1: value1\r\nHeader-2: multi\r\n line\r\n\r\n",
|
||||
"Header-1: value1\r\n Header-2: value2\r\n\r\n",
|
||||
"Header-1: value1\r\n\tHeader-2: value2\r\n\r\n",
|
||||
};
|
||||
// Missing CR
|
||||
new[] { "Header: value\n\r\n", @"Invalid request header: 'Header: value\x0A'" },
|
||||
new[] { "Header-1: value1\nHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header-1: value1\x0A'" },
|
||||
new[] { "Header-1: value1\r\nHeader-2: value2\n\r\n", @"Invalid request header: 'Header-2: value2\x0A'" },
|
||||
|
||||
// CR in value
|
||||
var headersWithCRInValue = new[]
|
||||
{
|
||||
"Header-1: value1\r\r\n",
|
||||
"Header-1: val\rue1\r\n",
|
||||
"Header-1: value1\rHeader-2: value2\r\n\r\n",
|
||||
"Header-1: value1\r\nHeader-2: value2\r\r\n",
|
||||
"Header-1: value1\r\nHeader-2: v\ralue2\r\n",
|
||||
"Header-1: Value__\rVector16________Vector32\r\n",
|
||||
"Header-1: Value___Vector16\r________Vector32\r\n",
|
||||
"Header-1: Value___Vector16_______\rVector32\r\n",
|
||||
"Header-1: Value___Vector16________Vector32\r\r\n",
|
||||
"Header-1: Value___Vector16________Vector32_\r\r\n",
|
||||
"Header-1: Value___Vector16________Vector32Value___Vector16_______\rVector32\r\n",
|
||||
"Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\r\r\n",
|
||||
"Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\r\r\n",
|
||||
};
|
||||
// Line folding
|
||||
new[] { "Header: line1\r\n line2\r\n\r\n", @"Invalid request header: ' line2\x0D\x0A'" },
|
||||
new[] { "Header: line1\r\n\tline2\r\n\r\n", @"Invalid request header: '\x09line2\x0D\x0A'" },
|
||||
new[] { "Header: line1\r\n line2\r\n\r\n", @"Invalid request header: ' line2\x0D\x0A'" },
|
||||
new[] { "Header: line1\r\n \tline2\r\n\r\n", @"Invalid request header: ' \x09line2\x0D\x0A'" },
|
||||
new[] { "Header: line1\r\n\t line2\r\n\r\n", @"Invalid request header: '\x09 line2\x0D\x0A'" },
|
||||
new[] { "Header: line1\r\n\t\tline2\r\n\r\n", @"Invalid request header: '\x09\x09line2\x0D\x0A'" },
|
||||
new[] { "Header: line1\r\n \t\t line2\r\n\r\n", @"Invalid request header: ' \x09\x09 line2\x0D\x0A'" },
|
||||
new[] { "Header: line1\r\n \t \t line2\r\n\r\n", @"Invalid request header: ' \x09 \x09 line2\x0D\x0A'" },
|
||||
new[] { "Header-1: multi\r\n line\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: ' line\x0D\x0A'" },
|
||||
new[] { "Header-1: value1\r\nHeader-2: multi\r\n line\r\n\r\n", @"Invalid request header: ' line\x0D\x0A'" },
|
||||
new[] { "Header-1: value1\r\n Header-2: value2\r\n\r\n", @"Invalid request header: ' Header-2: value2\x0D\x0A'" },
|
||||
new[] { "Header-1: value1\r\n\tHeader-2: value2\r\n\r\n", @"Invalid request header: '\x09Header-2: value2\x0D\x0A'" },
|
||||
|
||||
// Missing colon
|
||||
var headersWithMissingColon = new[]
|
||||
{
|
||||
"Header-1 value1\r\n\r\n",
|
||||
"Header-1 value1\r\nHeader-2: value2\r\n\r\n",
|
||||
"Header-1: value1\r\nHeader-2 value2\r\n\r\n",
|
||||
"\n"
|
||||
};
|
||||
// CR in value
|
||||
new[] { "Header-1: value1\r\r\n", @"Invalid request header: 'Header-1: value1\x0D\x0D\x0A'" },
|
||||
new[] { "Header-1: val\rue1\r\n", @"Invalid request header: 'Header-1: val\x0Due1\x0D\x0A'" },
|
||||
new[] { "Header-1: value1\rHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header-1: value1\x0DHeader-2: value2\x0D\x0A'" },
|
||||
new[] { "Header-1: value1\r\nHeader-2: value2\r\r\n", @"Invalid request header: 'Header-2: value2\x0D\x0D\x0A'" },
|
||||
new[] { "Header-1: value1\r\nHeader-2: v\ralue2\r\n", @"Invalid request header: 'Header-2: v\x0Dalue2\x0D\x0A'" },
|
||||
new[] { "Header-1: Value__\rVector16________Vector32\r\n", @"Invalid request header: 'Header-1: Value__\x0DVector16________Vector32\x0D\x0A'" },
|
||||
new[] { "Header-1: Value___Vector16\r________Vector32\r\n", @"Invalid request header: 'Header-1: Value___Vector16\x0D________Vector32\x0D\x0A'" },
|
||||
new[] { "Header-1: Value___Vector16_______\rVector32\r\n", @"Invalid request header: 'Header-1: Value___Vector16_______\x0DVector32\x0D\x0A'" },
|
||||
new[] { "Header-1: Value___Vector16________Vector32\r\r\n", @"Invalid request header: 'Header-1: Value___Vector16________Vector32\x0D\x0D\x0A'" },
|
||||
new[] { "Header-1: Value___Vector16________Vector32_\r\r\n", @"Invalid request header: 'Header-1: Value___Vector16________Vector32_\x0D\x0D\x0A'" },
|
||||
new[] { "Header-1: Value___Vector16________Vector32Value___Vector16_______\rVector32\r\n", @"Invalid request header: 'Header-1: Value___Vector16________Vector32Value___Vector16_______\x0DVector32\x0D\x0A'" },
|
||||
new[] { "Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\r\r\n", @"Invalid request header: 'Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\x0D\x0D\x0A'" },
|
||||
new[] { "Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\r\r\n", @"Invalid request header: 'Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\x0D\x0D\x0A'" },
|
||||
|
||||
// Starting with whitespace
|
||||
var headersStartingWithWhitespace = new[]
|
||||
{
|
||||
" Header: value\r\n\r\n",
|
||||
"\tHeader: value\r\n\r\n",
|
||||
" Header-1: value1\r\nHeader-2: value2\r\n\r\n",
|
||||
"\tHeader-1: value1\r\nHeader-2: value2\r\n\r\n",
|
||||
};
|
||||
// Missing colon
|
||||
new[] { "Header-1 value1\r\n\r\n", @"Invalid request header: 'Header-1 value1\x0D\x0A'" },
|
||||
new[] { "Header-1 value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header-1 value1\x0D\x0A'" },
|
||||
new[] { "Header-1: value1\r\nHeader-2 value2\r\n\r\n", @"Invalid request header: 'Header-2 value2\x0D\x0A'" },
|
||||
new[] { "\n", @"Invalid request header: '\x0A'" },
|
||||
|
||||
// Whitespace in header name
|
||||
var headersWithWithspaceInName = new[]
|
||||
{
|
||||
"Header : value\r\n\r\n",
|
||||
"Header\t: value\r\n\r\n",
|
||||
"Header\r: value\r\n\r\n",
|
||||
"Header_\rVector16: value\r\n\r\n",
|
||||
"Header__Vector16\r: value\r\n\r\n",
|
||||
"Header__Vector16_\r: value\r\n\r\n",
|
||||
"Header_\rVector16________Vector32: value\r\n\r\n",
|
||||
"Header__Vector16________Vector32\r: value\r\n\r\n",
|
||||
"Header__Vector16________Vector32_\r: value\r\n\r\n",
|
||||
"Header__Vector16________Vector32Header_\rVector16________Vector32: value\r\n\r\n",
|
||||
"Header__Vector16________Vector32Header__Vector16________Vector32\r: value\r\n\r\n",
|
||||
"Header__Vector16________Vector32Header__Vector16________Vector32_\r: value\r\n\r\n",
|
||||
"Header 1: value1\r\nHeader-2: value2\r\n\r\n",
|
||||
"Header 1 : value1\r\nHeader-2: value2\r\n\r\n",
|
||||
"Header 1\t: value1\r\nHeader-2: value2\r\n\r\n",
|
||||
"Header 1\r: value1\r\nHeader-2: value2\r\n\r\n",
|
||||
"Header-1: value1\r\nHeader 2: value2\r\n\r\n",
|
||||
"Header-1: value1\r\nHeader-2 : value2\r\n\r\n",
|
||||
"Header-1: value1\r\nHeader-2\t: value2\r\n\r\n",
|
||||
};
|
||||
// Starting with whitespace
|
||||
new[] { " Header: value\r\n\r\n", @"Invalid request header: ' Header: value\x0D\x0A'" },
|
||||
new[] { "\tHeader: value\r\n\r\n", @"Invalid request header: '\x09Header: value\x0D\x0A'" },
|
||||
new[] { " Header-1: value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: ' Header-1: value1\x0D\x0A'" },
|
||||
new[] { "\tHeader-1: value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: '\x09Header-1: value1\x0D\x0A'" },
|
||||
|
||||
// Headers not ending in CRLF line
|
||||
var headersNotEndingInCrLfLine = new[]
|
||||
{
|
||||
"Header-1: value1\r\nHeader-2: value2\r\n\r\r",
|
||||
"Header-1: value1\r\nHeader-2: value2\r\n\r ",
|
||||
"Header-1: value1\r\nHeader-2: value2\r\n\r \n",
|
||||
};
|
||||
// Whitespace in header name
|
||||
new[] { "Header : value\r\n\r\n", @"Invalid request header: 'Header : value\x0D\x0A'" },
|
||||
new[] { "Header\t: value\r\n\r\n", @"Invalid request header: 'Header\x09: value\x0D\x0A'" },
|
||||
new[] { "Header\r: value\r\n\r\n", @"Invalid request header: 'Header\x0D: value\x0D\x0A'" },
|
||||
new[] { "Header_\rVector16: value\r\n\r\n", @"Invalid request header: 'Header_\x0DVector16: value\x0D\x0A'" },
|
||||
new[] { "Header__Vector16\r: value\r\n\r\n", @"Invalid request header: 'Header__Vector16\x0D: value\x0D\x0A'" },
|
||||
new[] { "Header__Vector16_\r: value\r\n\r\n", @"Invalid request header: 'Header__Vector16_\x0D: value\x0D\x0A'" },
|
||||
new[] { "Header_\rVector16________Vector32: value\r\n\r\n", @"Invalid request header: 'Header_\x0DVector16________Vector32: value\x0D\x0A'" },
|
||||
new[] { "Header__Vector16________Vector32\r: value\r\n\r\n", @"Invalid request header: 'Header__Vector16________Vector32\x0D: value\x0D\x0A'" },
|
||||
new[] { "Header__Vector16________Vector32_\r: value\r\n\r\n", @"Invalid request header: 'Header__Vector16________Vector32_\x0D: value\x0D\x0A'" },
|
||||
new[] { "Header__Vector16________Vector32Header_\rVector16________Vector32: value\r\n\r\n", @"Invalid request header: 'Header__Vector16________Vector32Header_\x0DVector16________Vector32: value\x0D\x0A'" },
|
||||
new[] { "Header__Vector16________Vector32Header__Vector16________Vector32\r: value\r\n\r\n", @"Invalid request header: 'Header__Vector16________Vector32Header__Vector16________Vector32\x0D: value\x0D\x0A'" },
|
||||
new[] { "Header__Vector16________Vector32Header__Vector16________Vector32_\r: value\r\n\r\n", @"Invalid request header: 'Header__Vector16________Vector32Header__Vector16________Vector32_\x0D: value\x0D\x0A'" },
|
||||
new[] { "Header 1: value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header 1: value1\x0D\x0A'" },
|
||||
new[] { "Header 1 : value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header 1 : value1\x0D\x0A'" },
|
||||
new[] { "Header 1\t: value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header 1\x09: value1\x0D\x0A'" },
|
||||
new[] { "Header 1\r: value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header 1\x0D: value1\x0D\x0A'" },
|
||||
new[] { "Header-1: value1\r\nHeader 2: value2\r\n\r\n", @"Invalid request header: 'Header 2: value2\x0D\x0A'" },
|
||||
new[] { "Header-1: value1\r\nHeader-2 : value2\r\n\r\n", @"Invalid request header: 'Header-2 : value2\x0D\x0A'" },
|
||||
new[] { "Header-1: value1\r\nHeader-2\t: value2\r\n\r\n", @"Invalid request header: 'Header-2\x09: value2\x0D\x0A'" },
|
||||
|
||||
return new[]
|
||||
{
|
||||
Tuple.Create(headersWithLineFolding, "Whitespace is not allowed in header name."),
|
||||
Tuple.Create(headersWithCRInValue, "Header value must not contain CR characters."),
|
||||
Tuple.Create(headersWithMissingColon, "No ':' character found in header line."),
|
||||
Tuple.Create(headersStartingWithWhitespace, "Whitespace is not allowed in header name."),
|
||||
Tuple.Create(headersWithWithspaceInName, "Whitespace is not allowed in header name."),
|
||||
Tuple.Create(headersNotEndingInCrLfLine, "Headers corrupted, invalid header sequence.")
|
||||
}
|
||||
.SelectMany(t => t.Item1.Select(headers => new[] { headers, t.Item2 }));
|
||||
}
|
||||
}
|
||||
// Headers not ending in CRLF line
|
||||
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r\r", @"Invalid request headers: missing final CRLF in header fields." },
|
||||
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r ", @"Invalid request headers: missing final CRLF in header fields." },
|
||||
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r \n", @"Invalid request headers: missing final CRLF in header fields." },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
|||
{0}
|
||||
|
||||
{1}
|
||||
private readonly static Tuple<ulong, ulong, HttpMethod, int, bool>[] _knownMethods =
|
||||
new Tuple<ulong, ulong, HttpMethod, int, bool>[{2}];
|
||||
private readonly static Tuple<ulong, ulong, HttpMethod, int>[] _knownMethods =
|
||||
new Tuple<ulong, ulong, HttpMethod, int>[{2}];
|
||||
|
||||
private readonly static string[] _methodNames = new string[{3}];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue