Merge branch 'dev' into knownmethods-optimizations
This commit is contained in:
commit
d755f6183a
|
|
@ -16,8 +16,7 @@ env:
|
||||||
global:
|
global:
|
||||||
- DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
- DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||||
- DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
- DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||||
mono:
|
mono: none
|
||||||
- 4.0.5
|
|
||||||
os:
|
os:
|
||||||
- linux
|
- linux
|
||||||
- osx
|
- osx
|
||||||
|
|
|
||||||
|
|
@ -4,38 +4,39 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel
|
namespace Microsoft.AspNetCore.Server.Kestrel
|
||||||
{
|
{
|
||||||
public sealed class BadHttpRequestException : IOException
|
public sealed class BadHttpRequestException : IOException
|
||||||
{
|
{
|
||||||
private BadHttpRequestException(string message, int statusCode)
|
private BadHttpRequestException(string message, int statusCode)
|
||||||
|
: this(message, statusCode, null)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
private BadHttpRequestException(string message, int statusCode, HttpMethod? requiredMethod)
|
||||||
: base(message)
|
: base(message)
|
||||||
{
|
{
|
||||||
StatusCode = statusCode;
|
StatusCode = statusCode;
|
||||||
|
|
||||||
|
if (requiredMethod.HasValue)
|
||||||
|
{
|
||||||
|
AllowedHeader = HttpUtilities.MethodToString(requiredMethod.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal int StatusCode { get; }
|
internal int StatusCode { get; }
|
||||||
|
|
||||||
|
internal StringValues AllowedHeader { get; }
|
||||||
|
|
||||||
internal static BadHttpRequestException GetException(RequestRejectionReason reason)
|
internal static BadHttpRequestException GetException(RequestRejectionReason reason)
|
||||||
{
|
{
|
||||||
BadHttpRequestException ex;
|
BadHttpRequestException ex;
|
||||||
switch (reason)
|
switch (reason)
|
||||||
{
|
{
|
||||||
case RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence:
|
case RequestRejectionReason.InvalidRequestHeadersNoCRLF:
|
||||||
ex = new BadHttpRequestException("Headers corrupted, invalid header sequence.", StatusCodes.Status400BadRequest);
|
ex = new BadHttpRequestException("Invalid request headers: missing final CRLF in header fields.", 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);
|
|
||||||
break;
|
break;
|
||||||
case RequestRejectionReason.InvalidRequestLine:
|
case RequestRejectionReason.InvalidRequestLine:
|
||||||
ex = new BadHttpRequestException("Invalid request line.", StatusCodes.Status400BadRequest);
|
ex = new BadHttpRequestException("Invalid request line.", StatusCodes.Status400BadRequest);
|
||||||
|
|
@ -58,30 +59,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
||||||
case RequestRejectionReason.ChunkedRequestIncomplete:
|
case RequestRejectionReason.ChunkedRequestIncomplete:
|
||||||
ex = new BadHttpRequestException("Chunked request incomplete.", StatusCodes.Status400BadRequest);
|
ex = new BadHttpRequestException("Chunked request incomplete.", StatusCodes.Status400BadRequest);
|
||||||
break;
|
break;
|
||||||
case RequestRejectionReason.PathContainsNullCharacters:
|
|
||||||
ex = new BadHttpRequestException("The path contains null characters.", StatusCodes.Status400BadRequest);
|
|
||||||
break;
|
|
||||||
case RequestRejectionReason.InvalidCharactersInHeaderName:
|
case RequestRejectionReason.InvalidCharactersInHeaderName:
|
||||||
ex = new BadHttpRequestException("Invalid characters in header name.", StatusCodes.Status400BadRequest);
|
ex = new BadHttpRequestException("Invalid characters in header name.", StatusCodes.Status400BadRequest);
|
||||||
break;
|
break;
|
||||||
case RequestRejectionReason.NonAsciiOrNullCharactersInInputString:
|
|
||||||
ex = new BadHttpRequestException("The input string contains non-ASCII or null characters.", StatusCodes.Status400BadRequest);
|
|
||||||
break;
|
|
||||||
case RequestRejectionReason.RequestLineTooLong:
|
case RequestRejectionReason.RequestLineTooLong:
|
||||||
ex = new BadHttpRequestException("Request line too long.", StatusCodes.Status414UriTooLong);
|
ex = new BadHttpRequestException("Request line too long.", StatusCodes.Status414UriTooLong);
|
||||||
break;
|
break;
|
||||||
case RequestRejectionReason.HeadersExceedMaxTotalSize:
|
case RequestRejectionReason.HeadersExceedMaxTotalSize:
|
||||||
ex = new BadHttpRequestException("Request headers too long.", StatusCodes.Status431RequestHeaderFieldsTooLarge);
|
ex = new BadHttpRequestException("Request headers too long.", StatusCodes.Status431RequestHeaderFieldsTooLarge);
|
||||||
break;
|
break;
|
||||||
case RequestRejectionReason.MissingCRInHeaderLine:
|
|
||||||
ex = new BadHttpRequestException("No CR character found in header line.", StatusCodes.Status400BadRequest);
|
|
||||||
break;
|
|
||||||
case RequestRejectionReason.TooManyHeaders:
|
case RequestRejectionReason.TooManyHeaders:
|
||||||
ex = new BadHttpRequestException("Request contains too many headers.", StatusCodes.Status431RequestHeaderFieldsTooLarge);
|
ex = new BadHttpRequestException("Request contains too many headers.", StatusCodes.Status431RequestHeaderFieldsTooLarge);
|
||||||
break;
|
break;
|
||||||
case RequestRejectionReason.RequestTimeout:
|
case RequestRejectionReason.RequestTimeout:
|
||||||
ex = new BadHttpRequestException("Request timed out.", StatusCodes.Status408RequestTimeout);
|
ex = new BadHttpRequestException("Request timed out.", StatusCodes.Status408RequestTimeout);
|
||||||
break;
|
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:
|
default:
|
||||||
ex = new BadHttpRequestException("Bad request.", StatusCodes.Status400BadRequest);
|
ex = new BadHttpRequestException("Bad request.", StatusCodes.Status400BadRequest);
|
||||||
break;
|
break;
|
||||||
|
|
@ -95,7 +93,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
||||||
switch (reason)
|
switch (reason)
|
||||||
{
|
{
|
||||||
case RequestRejectionReason.InvalidRequestLine:
|
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;
|
break;
|
||||||
case RequestRejectionReason.InvalidContentLength:
|
case RequestRejectionReason.InvalidContentLength:
|
||||||
ex = new BadHttpRequestException($"Invalid content length: {value}", StatusCodes.Status400BadRequest);
|
ex = new BadHttpRequestException($"Invalid content length: {value}", StatusCodes.Status400BadRequest);
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,11 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.IO.Pipelines.Text.Primitives;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Encodings.Web.Utf8;
|
using System.Text.Encodings.Web.Utf8;
|
||||||
using System.Text.Utf8;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -28,6 +26,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
{
|
{
|
||||||
public abstract partial class Frame : IFrameControl, IHttpRequestLineHandler, IHttpHeadersHandler
|
public abstract partial class Frame : IFrameControl, IHttpRequestLineHandler, IHttpHeadersHandler
|
||||||
{
|
{
|
||||||
|
private const byte ByteAsterisk = (byte)'*';
|
||||||
|
private const byte ByteForwardSlash = (byte)'/';
|
||||||
private const byte BytePercentage = (byte)'%';
|
private const byte BytePercentage = (byte)'%';
|
||||||
|
|
||||||
private static readonly ArraySegment<byte> _endChunkedResponseBytes = CreateAsciiByteArraySegment("0\r\n\r\n");
|
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[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n");
|
||||||
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: Kestrel");
|
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 _onStartingSync = new Object();
|
||||||
private readonly object _onCompletedSync = new Object();
|
private readonly object _onCompletedSync = new Object();
|
||||||
|
|
||||||
|
|
@ -819,7 +822,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
// that should take precedence.
|
// that should take precedence.
|
||||||
if (_requestRejectedException != null)
|
if (_requestRejectedException != null)
|
||||||
{
|
{
|
||||||
SetErrorResponseHeaders(statusCode: _requestRejectedException.StatusCode);
|
SetErrorResponseException(_requestRejectedException);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -1078,7 +1081,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
{
|
{
|
||||||
buffer = buffer.Slice(buffer.Start, _remainingRequestHeadersBytesAllowed);
|
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
|
// allowed to look at
|
||||||
overLength = true;
|
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)
|
private void SetErrorResponseHeaders(int statusCode)
|
||||||
{
|
{
|
||||||
Debug.Assert(!HasResponseStarted, $"{nameof(SetErrorResponseHeaders)} called after response had already started.");
|
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);
|
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)
|
public void SetBadRequestState(RequestRejectionReason reason)
|
||||||
{
|
{
|
||||||
SetBadRequestState(BadHttpRequestException.GetException(reason));
|
SetBadRequestState(BadHttpRequestException.GetException(reason));
|
||||||
|
|
@ -1191,7 +1213,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
|
|
||||||
if (!HasResponseStarted)
|
if (!HasResponseStarted)
|
||||||
{
|
{
|
||||||
SetErrorResponseHeaders(ex.StatusCode);
|
SetErrorResponseException(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
_keepAlive = false;
|
_keepAlive = false;
|
||||||
|
|
@ -1217,22 +1239,64 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
Log.ApplicationError(ConnectionId, ex);
|
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
|
// 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;
|
// 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"
|
// then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
|
||||||
string requestUrlPath;
|
string requestUrlPath;
|
||||||
string rawTarget;
|
string rawTarget;
|
||||||
var needDecode = path.IndexOf(BytePercentage) >= 0;
|
if (pathEncoded)
|
||||||
if (needDecode)
|
|
||||||
{
|
{
|
||||||
// Read raw target before mutating memory.
|
// Read raw target before mutating memory.
|
||||||
rawTarget = target.GetAsciiStringNonNullCharacters();
|
rawTarget = target.GetAsciiStringNonNullCharacters();
|
||||||
|
|
||||||
// URI was encoded, unescape and then parse as utf8
|
// URI was encoded, unescape and then parse as utf8
|
||||||
int pathLength = UrlEncoder.Decode(path, path);
|
int pathLength = UrlEncoder.Decode(path, path);
|
||||||
requestUrlPath = new Utf8String(path.Slice(0, pathLength)).ToString();
|
requestUrlPath = GetUtf8String(path.Slice(0, pathLength));
|
||||||
}
|
}
|
||||||
else
|
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();
|
QueryString = query.GetAsciiStringNonNullCharacters();
|
||||||
RawTarget = rawTarget;
|
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))
|
if (RequestUrlStartsWithPathBase(normalizedTarget, out bool caseMatches))
|
||||||
{
|
{
|
||||||
PathBase = caseMatches ? _pathBase : normalizedTarget.Substring(0, _pathBase.Length);
|
PathBase = caseMatches ? _pathBase : normalizedTarget.Substring(0, _pathBase.Length);
|
||||||
Path = normalizedTarget.Substring(_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;
|
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;
|
return Encoding.UTF8.GetString(pointer, path.Length);
|
||||||
PathBase = string.Empty;
|
|
||||||
QueryString = string.Empty;
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
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
|
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;
|
consumed = buffer.Start;
|
||||||
examined = buffer.End;
|
examined = buffer.End;
|
||||||
|
|
||||||
ReadCursor end;
|
// Prepare the first span
|
||||||
Span<byte> span;
|
var span = buffer.First.Span;
|
||||||
|
var lineIndex = span.IndexOfVectorized(ByteLF);
|
||||||
// If the buffer is a single span then use it to find the LF
|
if (lineIndex >= 0)
|
||||||
if (buffer.IsSingleSpan)
|
|
||||||
{
|
{
|
||||||
var startLineSpan = buffer.First.Span;
|
consumed = buffer.Move(consumed, lineIndex + 1);
|
||||||
var lineIndex = startLineSpan.IndexOfVectorized(ByteLF);
|
span = span.Slice(0, lineIndex + 1);
|
||||||
|
|
||||||
if (lineIndex == -1)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
end = buffer.Move(consumed, lineIndex + 1);
|
|
||||||
span = startLineSpan.Slice(0, lineIndex + 1);
|
|
||||||
}
|
}
|
||||||
else
|
else if (buffer.IsSingleSpan || !TryGetNewLineSpan(ref buffer, ref span, out consumed))
|
||||||
{
|
{
|
||||||
var start = buffer.Start;
|
// No request line end
|
||||||
if (ReadCursorOperations.Seek(start, buffer.End, out end, ByteLF) == -1)
|
return false;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move 1 byte past the \n
|
|
||||||
end = buffer.Move(end, 1);
|
|
||||||
var startLineBuffer = buffer.Slice(start, end);
|
|
||||||
|
|
||||||
span = startLineBuffer.ToSpan();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var pathStart = -1;
|
// Fix and parse the span
|
||||||
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;
|
|
||||||
|
|
||||||
fixed (byte* data = &span.DangerousGetPinnableReference())
|
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 (pathStart == -1)
|
||||||
if (span.GetKnownMethod(out method, out var methodLength))
|
{
|
||||||
{
|
// Empty path is illegal
|
||||||
// Update the index, current char, state and jump directly
|
RejectRequestLine(data, length);
|
||||||
// to the next state
|
}
|
||||||
i += methodLength + 1;
|
|
||||||
|
|
||||||
goto case StartLineState.Path;
|
break;
|
||||||
}
|
}
|
||||||
goto case StartLineState.UnknownMethod;
|
else if (ch == ByteQuestionMark)
|
||||||
|
{
|
||||||
|
if (pathStart == -1)
|
||||||
|
{
|
||||||
|
// Empty path is illegal
|
||||||
|
RejectRequestLine(data, length);
|
||||||
|
}
|
||||||
|
|
||||||
case StartLineState.UnknownMethod:
|
break;
|
||||||
for (; i < length; i++)
|
}
|
||||||
{
|
else if (ch == BytePercentage)
|
||||||
var ch = data[i];
|
{
|
||||||
|
if (pathStart == -1)
|
||||||
|
{
|
||||||
|
// Path starting with % is illegal
|
||||||
|
RejectRequestLine(data, length);
|
||||||
|
}
|
||||||
|
|
||||||
if (ch == ByteSpace)
|
pathEncoded = true;
|
||||||
{
|
}
|
||||||
customMethod = span.Slice(0, i);
|
else if (pathStart == -1)
|
||||||
|
{
|
||||||
if (customMethod.Length == 0)
|
pathStart = offset;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!done)
|
if (pathStart == -1)
|
||||||
{
|
{
|
||||||
RejectRequestLine(span);
|
// End of path not found
|
||||||
|
RejectRequestLine(data, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
var pathBuffer = span.Slice(pathStart, pathEnd - pathStart);
|
var pathBuffer = new Span<byte>(data + pathStart, offset - pathStart);
|
||||||
var targetBuffer = span.Slice(pathStart, queryEnd - pathStart);
|
|
||||||
var query = span.Slice(queryStart, queryEnd - queryStart);
|
|
||||||
|
|
||||||
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;
|
var targetBuffer = new Span<byte>(data + pathStart, offset - pathStart);
|
||||||
examined = consumed;
|
var query = new Span<byte>(data + queryStart, offset - queryStart);
|
||||||
return true;
|
|
||||||
|
// 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
|
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.
|
// Headers don't end in CRLF line.
|
||||||
RejectRequest(RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence);
|
RejectRequest(RequestRejectionReason.InvalidRequestHeadersNoCRLF);
|
||||||
}
|
|
||||||
else if(ch1 == ByteSpace || ch1 == ByteTab)
|
|
||||||
{
|
|
||||||
RejectRequest(RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We moved the reader so look ahead 2 bytes so reset both the reader
|
// 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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static unsafe int FindEndOfName(byte* headerLine, int length)
|
private unsafe int FindEndOfName(byte* headerLine, int length)
|
||||||
{
|
{
|
||||||
var index = 0;
|
var index = 0;
|
||||||
var sawWhitespace = false;
|
var sawWhitespace = false;
|
||||||
|
|
@ -407,14 +316,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index == length)
|
if (index == length || sawWhitespace)
|
||||||
{
|
{
|
||||||
RejectRequest(RequestRejectionReason.NoColonCharacterFoundInHeaderLine);
|
RejectRequestHeader(headerLine, length);
|
||||||
}
|
|
||||||
if (sawWhitespace)
|
|
||||||
{
|
|
||||||
RejectRequest(RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -427,17 +333,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
|
|
||||||
if (headerLine[valueEnd + 2] != ByteLF)
|
if (headerLine[valueEnd + 2] != ByteLF)
|
||||||
{
|
{
|
||||||
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
|
RejectRequestHeader(headerLine, length);
|
||||||
}
|
}
|
||||||
if (headerLine[valueEnd + 1] != ByteCR)
|
if (headerLine[valueEnd + 1] != ByteCR)
|
||||||
{
|
{
|
||||||
RejectRequest(RequestRejectionReason.MissingCRInHeaderLine);
|
RejectRequestHeader(headerLine, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip colon from value start
|
// Skip colon from value start
|
||||||
var valueStart = nameEnd + 1;
|
var valueStart = nameEnd + 1;
|
||||||
// Ignore start whitespace
|
// Ignore start whitespace
|
||||||
for(; valueStart < valueEnd; valueStart++)
|
for (; valueStart < valueEnd; valueStart++)
|
||||||
{
|
{
|
||||||
var ch = headerLine[valueStart];
|
var ch = headerLine[valueStart];
|
||||||
if (ch != ByteTab && ch != ByteSpace && ch != ByteCR)
|
if (ch != ByteTab && ch != ByteSpace && ch != ByteCR)
|
||||||
|
|
@ -446,16 +352,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
}
|
}
|
||||||
else if (ch == ByteCR)
|
else if (ch == ByteCR)
|
||||||
{
|
{
|
||||||
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
|
RejectRequestHeader(headerLine, length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Check for CR in value
|
// Check for CR in value
|
||||||
var i = valueStart + 1;
|
var i = valueStart + 1;
|
||||||
if (Contains(headerLine + i, valueEnd - i, ByteCR))
|
if (Contains(headerLine + i, valueEnd - i, ByteCR))
|
||||||
{
|
{
|
||||||
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
|
RejectRequestHeader(headerLine, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore end whitespace
|
// Ignore end whitespace
|
||||||
|
|
@ -506,10 +411,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
found:
|
found:
|
||||||
return true;
|
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)
|
private static bool IsValidTokenChar(char c)
|
||||||
{
|
{
|
||||||
// Determines if a character is valid as a 'token' as defined in the
|
// 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);
|
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)
|
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);
|
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()
|
public void Reset()
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|
@ -570,26 +562,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
// https://github.com/dotnet/coreclr/issues/7459#issuecomment-253965670
|
// https://github.com/dotnet/coreclr/issues/7459#issuecomment-253965670
|
||||||
return Vector.AsVectorByte(new Vector<uint>(vectorByte * 0x01010101u));
|
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
|
public enum RequestRejectionReason
|
||||||
{
|
{
|
||||||
UnrecognizedHTTPVersion,
|
UnrecognizedHTTPVersion,
|
||||||
HeadersCorruptedInvalidHeaderSequence,
|
|
||||||
NoColonCharacterFoundInHeaderLine,
|
|
||||||
WhitespaceIsNotAllowedInHeaderName,
|
|
||||||
HeaderValueMustNotContainCR,
|
|
||||||
HeaderValueLineFoldingNotSupported,
|
|
||||||
InvalidRequestLine,
|
InvalidRequestLine,
|
||||||
|
InvalidRequestHeader,
|
||||||
|
InvalidRequestHeadersNoCRLF,
|
||||||
MalformedRequestInvalidHeaders,
|
MalformedRequestInvalidHeaders,
|
||||||
InvalidContentLength,
|
InvalidContentLength,
|
||||||
MultipleContentLengths,
|
MultipleContentLengths,
|
||||||
|
|
@ -19,16 +16,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
BadChunkSuffix,
|
BadChunkSuffix,
|
||||||
BadChunkSizeData,
|
BadChunkSizeData,
|
||||||
ChunkedRequestIncomplete,
|
ChunkedRequestIncomplete,
|
||||||
PathContainsNullCharacters,
|
|
||||||
InvalidCharactersInHeaderName,
|
InvalidCharactersInHeaderName,
|
||||||
NonAsciiOrNullCharactersInInputString,
|
|
||||||
RequestLineTooLong,
|
RequestLineTooLong,
|
||||||
HeadersExceedMaxTotalSize,
|
HeadersExceedMaxTotalSize,
|
||||||
MissingCRInHeaderLine,
|
|
||||||
TooManyHeaders,
|
TooManyHeaders,
|
||||||
RequestTimeout,
|
RequestTimeout,
|
||||||
FinalTransferCodingNotChunked,
|
FinalTransferCodingNotChunked,
|
||||||
LengthRequired,
|
LengthRequired,
|
||||||
LengthRequiredHttp10
|
LengthRequiredHttp10,
|
||||||
|
OptionsMethodRequired,
|
||||||
|
ConnectMethodRequired,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
||||||
private readonly static ulong _mask4Chars = GetMaskAsLong(new byte[]
|
private readonly static ulong _mask4Chars = GetMaskAsLong(new byte[]
|
||||||
{0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00});
|
{0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00});
|
||||||
|
|
||||||
private readonly static Tuple<ulong, ulong, HttpMethod, int, bool>[] _knownMethods =
|
private readonly static Tuple<ulong, ulong, HttpMethod, int>[] _knownMethods =
|
||||||
new Tuple<ulong, ulong, HttpMethod, int, bool>[17];
|
new Tuple<ulong, ulong, HttpMethod, int>[17];
|
||||||
|
|
||||||
private readonly static string[] _methodNames = new string[9];
|
private readonly static string[] _methodNames = new string[9];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
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 Http10Version = "HTTP/1.0";
|
||||||
public const string Http11Version = "HTTP/1.1";
|
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 _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
|
private const ulong _http11VersionLong = 3543824036068086856; // GetAsciiStringAsLong("HTTP/1.1"); const results in better codegen
|
||||||
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void SetKnownMethod(ulong mask, ulong knownMethodUlong, HttpMethod knownMethod, int length)
|
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);
|
_knownMethods[GetKnownMethodIndex(knownMethodUlong)] = new Tuple<ulong, ulong, HttpMethod, int>(mask, knownMethodUlong, knownMethod, length);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void FillKnownMethodsGaps()
|
private static void FillKnownMethodsGaps()
|
||||||
{
|
{
|
||||||
var knownMethods = _knownMethods;
|
var knownMethods = _knownMethods;
|
||||||
var length = knownMethods.Length;
|
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++)
|
for (int i = 0; i < length; i++)
|
||||||
{
|
{
|
||||||
if (knownMethods[i] == null)
|
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)
|
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)
|
for (i = 0; i < Math.Min(span.Length, maxChars); ++i)
|
||||||
{
|
{
|
||||||
var ch = span[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)
|
if (span.Length > maxChars)
|
||||||
|
|
@ -129,35 +134,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
|
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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)
|
method = GetKnownMethod(data, span.Length, out length);
|
||||||
{
|
return method != HttpMethod.Custom;
|
||||||
length = 3;
|
|
||||||
method = HttpMethod.Get;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 key = GetKnownMethodIndex(value);
|
||||||
|
|
||||||
var x = _knownMethods[key];
|
var x = _knownMethods[key];
|
||||||
|
|
||||||
if (x != null && (value & x.Item1) == x.Item2)
|
if (x != null && (value & x.Item1) == x.Item2)
|
||||||
{
|
{
|
||||||
method = x.Item3;
|
methodLength = x.Item4;
|
||||||
length = x.Item4;
|
return x.Item3;
|
||||||
return x.Item5;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
method = HttpMethod.Custom;
|
return HttpMethod.Custom;
|
||||||
length = 0;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -172,35 +188,83 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
|
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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);
|
length = sizeof(ulong);
|
||||||
knownVersion = HttpVersion.Http11;
|
return true;
|
||||||
}
|
|
||||||
else if (version == _http10VersionLong)
|
|
||||||
{
|
|
||||||
length = sizeof(ulong);
|
|
||||||
knownVersion = HttpVersion.Http10;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
length = 0;
|
|
||||||
knownVersion = HttpVersion.Unknown;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
knownVersion = HttpVersion.Unknown;
|
knownScheme = HttpScheme.Unknown;
|
||||||
length = 0;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,5 +289,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
||||||
}
|
}
|
||||||
return null;
|
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.Numerics.Vectors" Version="$(CoreFxVersion)" />
|
||||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="$(CoreFxVersion)" />
|
<PackageReference Include="System.Threading.Tasks.Extensions" Version="$(CoreFxVersion)" />
|
||||||
<PackageReference Include="System.IO.Pipelines" Version="$(CoreFxLabsPipelinesVersion)" />
|
<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)" />
|
<PackageReference Include="System.Text.Encodings.Web.Utf8" Version="$(CoreFxLabsVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
|
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
|
@ -90,7 +91,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
$"Invalid content length: {contentLength}");
|
$"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;
|
BadHttpRequestException loggedException = null;
|
||||||
var mockKestrelTrace = new Mock<IKestrelTrace>();
|
var mockKestrelTrace = new Mock<IKestrelTrace>();
|
||||||
|
|
@ -106,7 +119,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
{
|
{
|
||||||
await connection.SendAll(request);
|
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);
|
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}",
|
$"HTTP/1.1 {expectedResponseStatusCode}",
|
||||||
"Connection: close",
|
"Connection: close",
|
||||||
$"Date: {expectedDateHeaderValue}",
|
$"Date: {expectedDateHeaderValue}",
|
||||||
"Content-Length: 0",
|
"Content-Length: 0",
|
||||||
|
expectedAllowHeader,
|
||||||
"",
|
"",
|
||||||
"");
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
await connection.ReceiveForcedEnd(lines.Where(f => f != null).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TheoryData<string, string> InvalidRequestLineData
|
public static TheoryData<string, string> InvalidRequestLineData
|
||||||
|
|
@ -131,9 +149,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
{
|
{
|
||||||
var data = new TheoryData<string, string>();
|
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)
|
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)
|
foreach (var requestLine in HttpParsingData.RequestLineWithEncodedNullCharInTargetData)
|
||||||
|
|
@ -143,7 +169,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
|
|
||||||
foreach (var requestLine in HttpParsingData.RequestLineWithNullCharInTargetData)
|
foreach (var requestLine in HttpParsingData.RequestLineWithNullCharInTargetData)
|
||||||
{
|
{
|
||||||
data.Add(requestLine, "Invalid request line.");
|
data.Add(requestLine, $"Invalid request line.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
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)
|
private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress)
|
||||||
{
|
{
|
||||||
var builder = new WebHostBuilder()
|
var builder = new WebHostBuilder()
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
||||||
|
|
||||||
private class NullParser : IHttpParser
|
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[] _target = Encoding.ASCII.GetBytes("/plaintext");
|
||||||
private readonly byte[] _hostHeaderName = Encoding.ASCII.GetBytes("Host");
|
private readonly byte[] _hostHeaderName = Encoding.ASCII.GetBytes("Host");
|
||||||
private readonly byte[] _hostHeaderValue = Encoding.ASCII.GetBytes("www.example.com");
|
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
|
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;
|
consumed = buffer.Start;
|
||||||
examined = buffer.End;
|
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)]
|
[Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount * RequestParsingData.Pipelining)]
|
||||||
public void PipelinedPlaintextTechEmpower()
|
public void PipelinedPlaintextTechEmpower()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
||||||
"Connection: keep-alive\r\n" +
|
"Connection: keep-alive\r\n" +
|
||||||
"\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 =
|
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" +
|
"Host: live.asp.net\r\n" +
|
||||||
"Connection: keep-alive\r\n" +
|
"Connection: keep-alive\r\n" +
|
||||||
"Upgrade-Insecure-Requests: 1\r\n" +
|
"Upgrade-Insecure-Requests: 1\r\n" +
|
||||||
|
|
@ -35,7 +43,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
||||||
"\r\n";
|
"\r\n";
|
||||||
|
|
||||||
private const string _unicodeRequest =
|
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: text/html, application/xhtml+xml, image/jxr, */*\r\n" +
|
||||||
"Accept-Language: en-US,en-GB;q=0.7,en;q=0.3\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" +
|
"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[] PlaintextTechEmpowerPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_plaintextTechEmpowerRequest, Pipelining)));
|
||||||
public static readonly byte[] PlaintextTechEmpowerRequest = Encoding.ASCII.GetBytes(_plaintextTechEmpowerRequest);
|
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[] LiveaspnetPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_liveaspnetRequest, Pipelining)));
|
||||||
public static readonly byte[] LiveaspnetRequest = Encoding.ASCII.GetBytes(_liveaspnetRequest);
|
public static readonly byte[] LiveaspnetRequest = Encoding.ASCII.GetBytes(_liveaspnetRequest);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
||||||
Output = new MockSocketOutput(),
|
Output = new MockSocketOutput(),
|
||||||
ConnectionControl = Mock.Of<IConnectionControl>()
|
ConnectionControl = Mock.Of<IConnectionControl>()
|
||||||
};
|
};
|
||||||
|
connectionContext.ListenerContext.ServiceContext.HttpParserFactory = f => new Internal.Http.KestrelHttpParser(log: null);
|
||||||
|
|
||||||
var frame = new TestFrame<object>(application: null, context: connectionContext);
|
var frame = new TestFrame<object>(application: null, context: connectionContext);
|
||||||
frame.Reset();
|
frame.Reset();
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel;
|
using Microsoft.AspNetCore.Server.Kestrel;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Internal;
|
using Microsoft.AspNetCore.Server.Kestrel.Internal;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.Internal;
|
using Microsoft.Extensions.Internal;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
@ -81,25 +82,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||||
_pipelineFactory.Dispose();
|
_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]
|
[Fact]
|
||||||
public async Task TakeMessageHeadersThrowsWhenHeadersExceedTotalSizeLimit()
|
public async Task TakeMessageHeadersThrowsWhenHeadersExceedTotalSizeLimit()
|
||||||
{
|
{
|
||||||
|
|
@ -347,6 +329,37 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||||
Assert.Equal(new InvalidOperationException().Message, exception.Message);
|
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]
|
[Fact]
|
||||||
public void RequestProcessingAsyncEnablesKeepAliveTimeout()
|
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
|
public static TheoryData<string> RequestLineWithNullCharInTargetData
|
||||||
{
|
{
|
||||||
get
|
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>>(),
|
||||||
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<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();
|
parsedMethod = method != HttpMethod.Custom ? HttpUtilities.MethodToString(method) : customMethod.GetAsciiStringNonNullCharacters();
|
||||||
parsedVersion = HttpUtilities.VersionToString(version);
|
parsedVersion = HttpUtilities.VersionToString(version);
|
||||||
parsedRawTarget = target.GetAsciiStringNonNullCharacters();
|
parsedRawTarget = target.GetAsciiStringNonNullCharacters();
|
||||||
parsedRawPath = path.GetAsciiStringNonNullCharacters();
|
parsedRawPath = path.GetAsciiStringNonNullCharacters();
|
||||||
parsedQuery = query.GetAsciiStringNonNullCharacters();
|
parsedQuery = query.GetAsciiStringNonNullCharacters();
|
||||||
|
pathEncoded = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.True(parser.ParseRequestLine(requestLineHandler.Object, buffer, out var consumed, out var examined));
|
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>(() =>
|
var exception = Assert.Throws<BadHttpRequestException>(() =>
|
||||||
parser.ParseRequestLine(Mock.Of<IHttpRequestLineHandler>(), buffer, out var consumed, out var examined));
|
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);
|
Assert.Equal(StatusCodes.Status400BadRequest, (exception as BadHttpRequestException).StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -306,7 +309,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||||
[MemberData(nameof(RequestHeaderInvalidData))]
|
[MemberData(nameof(RequestHeaderInvalidData))]
|
||||||
public void ParseHeadersThrowsOnInvalidRequestHeaders(string rawHeaders, string expectedExceptionMessage)
|
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 buffer = ReadableBuffer.Create(Encoding.ASCII.GetBytes(rawHeaders));
|
||||||
|
|
||||||
var exception = Assert.Throws<BadHttpRequestException>(() =>
|
var exception = Assert.Throws<BadHttpRequestException>(() =>
|
||||||
|
|
|
||||||
|
|
@ -84,11 +84,23 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||||
{
|
{
|
||||||
TestKnownStringsInterning(input, expected, span =>
|
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);
|
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]
|
[Theory]
|
||||||
[InlineData("CONNECT / HTTP/1.1", "CONNECT")]
|
[InlineData("CONNECT / HTTP/1.1", "CONNECT")]
|
||||||
[InlineData("DELETE / HTTP/1.1", "DELETE")]
|
[InlineData("DELETE / HTTP/1.1", "DELETE")]
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel;
|
using Microsoft.AspNetCore.Server.Kestrel;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -93,19 +95,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("*")]
|
[InlineData(HttpMethod.Options, "*")]
|
||||||
[InlineData("*/?arg=value")]
|
[InlineData(HttpMethod.Connect, "host")]
|
||||||
[InlineData("*?arg=value")]
|
public async Task NonPathRequestTargetSetInRawTarget(HttpMethod method, string requestTarget)
|
||||||
[InlineData("DoesNotStartWith/")]
|
|
||||||
[InlineData("DoesNotStartWith/?arg=value")]
|
|
||||||
[InlineData("DoesNotStartWithSlash?arg=value")]
|
|
||||||
[InlineData("./")]
|
|
||||||
[InlineData("../")]
|
|
||||||
[InlineData("../.")]
|
|
||||||
[InlineData(".././")]
|
|
||||||
[InlineData("../..")]
|
|
||||||
[InlineData("../../")]
|
|
||||||
public async Task NonPathRequestTargetSetInRawTarget(string requestTarget)
|
|
||||||
{
|
{
|
||||||
var testContext = new TestServiceContext();
|
var testContext = new TestServiceContext();
|
||||||
|
|
||||||
|
|
@ -123,7 +115,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
{
|
{
|
||||||
await connection.Send(
|
await connection.Send(
|
||||||
$"GET {requestTarget} HTTP/1.1",
|
$"{HttpUtilities.MethodToString(method)} {requestTarget} HTTP/1.1",
|
||||||
"",
|
"",
|
||||||
"");
|
"");
|
||||||
await connection.ReceiveEnd(
|
await connection.ReceiveEnd(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// 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.
|
// 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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -33,6 +34,26 @@ namespace Microsoft.AspNetCore.Testing
|
||||||
Tuple.Create("/%C3%A5/bc", "/\u00E5/bc"),
|
Tuple.Create("/%C3%A5/bc", "/\u00E5/bc"),
|
||||||
Tuple.Create("/%25", "/%"),
|
Tuple.Create("/%25", "/%"),
|
||||||
Tuple.Create("/%2F", "/%2F"),
|
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[]
|
var queryStrings = new[]
|
||||||
{
|
{
|
||||||
|
|
@ -173,9 +194,73 @@ namespace Microsoft.AspNetCore.Testing
|
||||||
"GET /%E8%01%00 HTTP/1.1\r\n",
|
"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[]
|
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 HTTP/1.1\r\n",
|
||||||
"GET /\0\0 HTTP/1.1\r\n",
|
"GET /\0\0 HTTP/1.1\r\n",
|
||||||
"GET /%C8\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>
|
public static TheoryData<string> UnrecognizedHttpVersionData => new TheoryData<string>
|
||||||
{
|
{
|
||||||
|
" ",
|
||||||
|
"/",
|
||||||
"H",
|
"H",
|
||||||
"HT",
|
"HT",
|
||||||
"HTT",
|
"HTT",
|
||||||
|
|
@ -203,106 +290,79 @@ namespace Microsoft.AspNetCore.Testing
|
||||||
"8charact",
|
"8charact",
|
||||||
};
|
};
|
||||||
|
|
||||||
public static IEnumerable<object[]> RequestHeaderInvalidData
|
public static IEnumerable<object[]> RequestHeaderInvalidData => new[]
|
||||||
{
|
{
|
||||||
get
|
// Missing CR
|
||||||
{
|
new[] { "Header: value\n\r\n", @"Invalid request header: 'Header: value\x0A'" },
|
||||||
// Line folding
|
new[] { "Header-1: value1\nHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header-1: value1\x0A'" },
|
||||||
var headersWithLineFolding = new[]
|
new[] { "Header-1: value1\r\nHeader-2: value2\n\r\n", @"Invalid request header: 'Header-2: value2\x0A'" },
|
||||||
{
|
|
||||||
"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",
|
|
||||||
};
|
|
||||||
|
|
||||||
// CR in value
|
// Line folding
|
||||||
var headersWithCRInValue = new[]
|
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'" },
|
||||||
"Header-1: value1\r\r\n",
|
new[] { "Header: line1\r\n line2\r\n\r\n", @"Invalid request header: ' line2\x0D\x0A'" },
|
||||||
"Header-1: val\rue1\r\n",
|
new[] { "Header: line1\r\n \tline2\r\n\r\n", @"Invalid request header: ' \x09line2\x0D\x0A'" },
|
||||||
"Header-1: value1\rHeader-2: value2\r\n\r\n",
|
new[] { "Header: line1\r\n\t line2\r\n\r\n", @"Invalid request header: '\x09 line2\x0D\x0A'" },
|
||||||
"Header-1: value1\r\nHeader-2: value2\r\r\n",
|
new[] { "Header: line1\r\n\t\tline2\r\n\r\n", @"Invalid request header: '\x09\x09line2\x0D\x0A'" },
|
||||||
"Header-1: value1\r\nHeader-2: v\ralue2\r\n",
|
new[] { "Header: line1\r\n \t\t line2\r\n\r\n", @"Invalid request header: ' \x09\x09 line2\x0D\x0A'" },
|
||||||
"Header-1: Value__\rVector16________Vector32\r\n",
|
new[] { "Header: line1\r\n \t \t line2\r\n\r\n", @"Invalid request header: ' \x09 \x09 line2\x0D\x0A'" },
|
||||||
"Header-1: Value___Vector16\r________Vector32\r\n",
|
new[] { "Header-1: multi\r\n line\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: ' line\x0D\x0A'" },
|
||||||
"Header-1: Value___Vector16_______\rVector32\r\n",
|
new[] { "Header-1: value1\r\nHeader-2: multi\r\n line\r\n\r\n", @"Invalid request header: ' line\x0D\x0A'" },
|
||||||
"Header-1: Value___Vector16________Vector32\r\r\n",
|
new[] { "Header-1: value1\r\n Header-2: value2\r\n\r\n", @"Invalid request header: ' Header-2: value2\x0D\x0A'" },
|
||||||
"Header-1: Value___Vector16________Vector32_\r\r\n",
|
new[] { "Header-1: value1\r\n\tHeader-2: value2\r\n\r\n", @"Invalid request header: '\x09Header-2: value2\x0D\x0A'" },
|
||||||
"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",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Missing colon
|
// CR in value
|
||||||
var headersWithMissingColon = new[]
|
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'" },
|
||||||
"Header-1 value1\r\n\r\n",
|
new[] { "Header-1: value1\rHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header-1: value1\x0DHeader-2: value2\x0D\x0A'" },
|
||||||
"Header-1 value1\r\nHeader-2: value2\r\n\r\n",
|
new[] { "Header-1: value1\r\nHeader-2: value2\r\r\n", @"Invalid request header: 'Header-2: value2\x0D\x0D\x0A'" },
|
||||||
"Header-1: value1\r\nHeader-2 value2\r\n\r\n",
|
new[] { "Header-1: value1\r\nHeader-2: v\ralue2\r\n", @"Invalid request header: 'Header-2: v\x0Dalue2\x0D\x0A'" },
|
||||||
"\n"
|
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
|
// Missing colon
|
||||||
var headersStartingWithWhitespace = new[]
|
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'" },
|
||||||
" Header: value\r\n\r\n",
|
new[] { "Header-1: value1\r\nHeader-2 value2\r\n\r\n", @"Invalid request header: 'Header-2 value2\x0D\x0A'" },
|
||||||
"\tHeader: value\r\n\r\n",
|
new[] { "\n", @"Invalid request header: '\x0A'" },
|
||||||
" Header-1: value1\r\nHeader-2: value2\r\n\r\n",
|
|
||||||
"\tHeader-1: value1\r\nHeader-2: value2\r\n\r\n",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Whitespace in header name
|
// Starting with whitespace
|
||||||
var headersWithWithspaceInName = new[]
|
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'" },
|
||||||
"Header : value\r\n\r\n",
|
new[] { " Header-1: value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: ' Header-1: value1\x0D\x0A'" },
|
||||||
"Header\t: value\r\n\r\n",
|
new[] { "\tHeader-1: value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: '\x09Header-1: value1\x0D\x0A'" },
|
||||||
"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",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Headers not ending in CRLF line
|
// Whitespace in header name
|
||||||
var headersNotEndingInCrLfLine = new[]
|
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'" },
|
||||||
"Header-1: value1\r\nHeader-2: value2\r\n\r\r",
|
new[] { "Header\r: value\r\n\r\n", @"Invalid request header: 'Header\x0D: value\x0D\x0A'" },
|
||||||
"Header-1: value1\r\nHeader-2: value2\r\n\r ",
|
new[] { "Header_\rVector16: value\r\n\r\n", @"Invalid request header: 'Header_\x0DVector16: value\x0D\x0A'" },
|
||||||
"Header-1: value1\r\nHeader-2: value2\r\n\r \n",
|
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[]
|
// 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." },
|
||||||
Tuple.Create(headersWithLineFolding, "Whitespace is not allowed in header name."),
|
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r ", @"Invalid request headers: missing final CRLF in header fields." },
|
||||||
Tuple.Create(headersWithCRInValue, "Header value must not contain CR characters."),
|
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r \n", @"Invalid request headers: missing final CRLF in header fields." },
|
||||||
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 }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
||||||
{0}
|
{0}
|
||||||
|
|
||||||
{1}
|
{1}
|
||||||
private readonly static Tuple<ulong, ulong, HttpMethod, int, bool>[] _knownMethods =
|
private readonly static Tuple<ulong, ulong, HttpMethod, int>[] _knownMethods =
|
||||||
new Tuple<ulong, ulong, HttpMethod, int, bool>[{2}];
|
new Tuple<ulong, ulong, HttpMethod, int>[{2}];
|
||||||
|
|
||||||
private readonly static string[] _methodNames = new string[{3}];
|
private readonly static string[] _methodNames = new string[{3}];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue