Merge branch 'dev' into knownmethods-optimizations

This commit is contained in:
arespr 2017-03-11 22:50:58 +01:00
commit d755f6183a
24 changed files with 966 additions and 496 deletions

View File

@ -16,8 +16,7 @@ env:
global:
- DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
- DOTNET_CLI_TELEMETRY_OPTOUT: 1
mono:
- 4.0.5
mono: none
os:
- linux
- osx

View File

@ -4,38 +4,39 @@
using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Server.Kestrel
{
public sealed class BadHttpRequestException : IOException
{
private BadHttpRequestException(string message, int statusCode)
: this(message, statusCode, null)
{ }
private BadHttpRequestException(string message, int statusCode, HttpMethod? requiredMethod)
: base(message)
{
StatusCode = statusCode;
if (requiredMethod.HasValue)
{
AllowedHeader = HttpUtilities.MethodToString(requiredMethod.Value);
}
}
internal int StatusCode { get; }
internal StringValues AllowedHeader { get; }
internal static BadHttpRequestException GetException(RequestRejectionReason reason)
{
BadHttpRequestException ex;
switch (reason)
{
case RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence:
ex = new BadHttpRequestException("Headers corrupted, invalid header sequence.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.NoColonCharacterFoundInHeaderLine:
ex = new BadHttpRequestException("No ':' character found in header line.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName:
ex = new BadHttpRequestException("Whitespace is not allowed in header name.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.HeaderValueMustNotContainCR:
ex = new BadHttpRequestException("Header value must not contain CR characters.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.HeaderValueLineFoldingNotSupported:
ex = new BadHttpRequestException("Header value line folding not supported.", StatusCodes.Status400BadRequest);
case RequestRejectionReason.InvalidRequestHeadersNoCRLF:
ex = new BadHttpRequestException("Invalid request headers: missing final CRLF in header fields.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.InvalidRequestLine:
ex = new BadHttpRequestException("Invalid request line.", StatusCodes.Status400BadRequest);
@ -58,30 +59,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel
case RequestRejectionReason.ChunkedRequestIncomplete:
ex = new BadHttpRequestException("Chunked request incomplete.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.PathContainsNullCharacters:
ex = new BadHttpRequestException("The path contains null characters.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.InvalidCharactersInHeaderName:
ex = new BadHttpRequestException("Invalid characters in header name.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.NonAsciiOrNullCharactersInInputString:
ex = new BadHttpRequestException("The input string contains non-ASCII or null characters.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.RequestLineTooLong:
ex = new BadHttpRequestException("Request line too long.", StatusCodes.Status414UriTooLong);
break;
case RequestRejectionReason.HeadersExceedMaxTotalSize:
ex = new BadHttpRequestException("Request headers too long.", StatusCodes.Status431RequestHeaderFieldsTooLarge);
break;
case RequestRejectionReason.MissingCRInHeaderLine:
ex = new BadHttpRequestException("No CR character found in header line.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.TooManyHeaders:
ex = new BadHttpRequestException("Request contains too many headers.", StatusCodes.Status431RequestHeaderFieldsTooLarge);
break;
case RequestRejectionReason.RequestTimeout:
ex = new BadHttpRequestException("Request timed out.", StatusCodes.Status408RequestTimeout);
break;
case RequestRejectionReason.OptionsMethodRequired:
ex = new BadHttpRequestException("Method not allowed.", StatusCodes.Status405MethodNotAllowed, HttpMethod.Options);
break;
case RequestRejectionReason.ConnectMethodRequired:
ex = new BadHttpRequestException("Method not allowed.", StatusCodes.Status405MethodNotAllowed, HttpMethod.Connect);
break;
default:
ex = new BadHttpRequestException("Bad request.", StatusCodes.Status400BadRequest);
break;
@ -95,7 +93,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel
switch (reason)
{
case RequestRejectionReason.InvalidRequestLine:
ex = new BadHttpRequestException($"Invalid request line: {value}", StatusCodes.Status400BadRequest);
ex = new BadHttpRequestException($"Invalid request line: '{value}'", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.InvalidRequestHeader:
ex = new BadHttpRequestException($"Invalid request header: '{value}'", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.InvalidContentLength:
ex = new BadHttpRequestException($"Invalid content length: {value}", StatusCodes.Status400BadRequest);

View File

@ -6,13 +6,11 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipelines;
using System.IO.Pipelines.Text.Primitives;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Encodings.Web.Utf8;
using System.Text.Utf8;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@ -28,6 +26,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public abstract partial class Frame : IFrameControl, IHttpRequestLineHandler, IHttpHeadersHandler
{
private const byte ByteAsterisk = (byte)'*';
private const byte ByteForwardSlash = (byte)'/';
private const byte BytePercentage = (byte)'%';
private static readonly ArraySegment<byte> _endChunkedResponseBytes = CreateAsciiByteArraySegment("0\r\n\r\n");
@ -40,6 +40,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n");
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: Kestrel");
private const string EmptyPath = "/";
private const string Asterisk = "*";
private readonly object _onStartingSync = new Object();
private readonly object _onCompletedSync = new Object();
@ -819,7 +822,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
// that should take precedence.
if (_requestRejectedException != null)
{
SetErrorResponseHeaders(statusCode: _requestRejectedException.StatusCode);
SetErrorResponseException(_requestRejectedException);
}
else
{
@ -1078,7 +1081,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
buffer = buffer.Slice(buffer.Start, _remainingRequestHeadersBytesAllowed);
// If we sliced it means the current buffer bigger than what we're
// If we sliced it means the current buffer bigger than what we're
// allowed to look at
overLength = true;
}
@ -1128,6 +1131,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
private void SetErrorResponseException(BadHttpRequestException ex)
{
SetErrorResponseHeaders(ex.StatusCode);
if (!StringValues.IsNullOrEmpty(ex.AllowedHeader))
{
FrameResponseHeaders.HeaderAllow = ex.AllowedHeader;
}
}
private void SetErrorResponseHeaders(int statusCode)
{
Debug.Assert(!HasResponseStarted, $"{nameof(SetErrorResponseHeaders)} called after response had already started.");
@ -1180,6 +1193,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
throw BadHttpRequestException.GetException(reason, value);
}
private void RejectRequestLine(Span<byte> requestLine)
{
Debug.Assert(Log.IsEnabled(LogLevel.Information) == true, "Use RejectRequest instead to improve inlining when log is disabled");
const int MaxRequestLineError = 32;
var line = requestLine.GetAsciiStringEscaped(MaxRequestLineError);
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine, line);
}
public void SetBadRequestState(RequestRejectionReason reason)
{
SetBadRequestState(BadHttpRequestException.GetException(reason));
@ -1191,7 +1213,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
if (!HasResponseStarted)
{
SetErrorResponseHeaders(ex.StatusCode);
SetErrorResponseException(ex);
}
_keepAlive = false;
@ -1217,22 +1239,64 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
Log.ApplicationError(ConnectionId, ex);
}
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod)
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, Span<byte> line, bool pathEncoded)
{
Debug.Assert(target.Length != 0, "Request target must be non-zero length");
var ch = target[0];
if (ch == ByteForwardSlash)
{
// origin-form.
// The most common form of request-target.
// https://tools.ietf.org/html/rfc7230#section-5.3.1
OnOriginFormTarget(method, version, target, path, query, customMethod, pathEncoded);
}
else if (ch == ByteAsterisk && target.Length == 1)
{
OnAsteriskFormTarget(method);
}
else if (target.GetKnownHttpScheme(out var scheme))
{
OnAbsoluteFormTarget(target, query, line);
}
else
{
// Assume anything else is considered authority form.
// FYI: this should be an edge case. This should only happen when
// a client mistakenly things this server is a proxy server.
OnAuthorityFormTarget(method, target, line);
}
Method = method != HttpMethod.Custom
? HttpUtilities.MethodToString(method) ?? string.Empty
: customMethod.GetAsciiStringNonNullCharacters();
HttpVersion = HttpUtilities.VersionToString(version);
Debug.Assert(RawTarget != null, "RawTarget was not set");
Debug.Assert(Method != null, "Method was not set");
Debug.Assert(Path != null, "Path was not set");
Debug.Assert(QueryString != "QueryString was not set");
Debug.Assert(HttpVersion != "HttpVersion was not set");
}
private void OnOriginFormTarget(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
{
Debug.Assert(target[0] == ByteForwardSlash, "Should only be called when path starts with /");
// URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11
// Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8;
// then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
string requestUrlPath;
string rawTarget;
var needDecode = path.IndexOf(BytePercentage) >= 0;
if (needDecode)
if (pathEncoded)
{
// Read raw target before mutating memory.
rawTarget = target.GetAsciiStringNonNullCharacters();
// URI was encoded, unescape and then parse as utf8
int pathLength = UrlEncoder.Decode(path, path);
requestUrlPath = new Utf8String(path.Slice(0, pathLength)).ToString();
requestUrlPath = GetUtf8String(path.Slice(0, pathLength));
}
else
{
@ -1251,35 +1315,126 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
var normalizedTarget = PathNormalizer.RemoveDotSegments(requestUrlPath);
if (method != HttpMethod.Custom)
{
Method = HttpUtilities.MethodToString(method) ?? string.Empty;
}
else
{
Method = customMethod.GetAsciiStringNonNullCharacters();
}
QueryString = query.GetAsciiStringNonNullCharacters();
RawTarget = rawTarget;
HttpVersion = HttpUtilities.VersionToString(version);
SetNormalizedPath(requestUrlPath);
}
private void OnAuthorityFormTarget(HttpMethod method, Span<byte> target, Span<byte> line)
{
// TODO Validate that target is a correct host[:port] string.
// Reject as 400 if not. This is just a quick scan for invalid characters
// but doesn't check that the target fully matches the URI spec.
for (var i = 0; i < target.Length; i++)
{
var ch = target[i];
if (!UriUtilities.IsValidAuthorityCharacter(ch))
{
if (Log.IsEnabled(LogLevel.Information))
{
RejectRequestLine(line);
}
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine);
}
}
// The authority-form of request-target is only used for CONNECT
// requests (https://tools.ietf.org/html/rfc7231#section-4.3.6).
if (method != HttpMethod.Connect)
{
RejectRequest(RequestRejectionReason.ConnectMethodRequired);
}
// When making a CONNECT request to establish a tunnel through one or
// more proxies, a client MUST send only the target URI's authority
// component(excluding any userinfo and its "@" delimiter) as the
// request - target.For example,
//
// CONNECT www.example.com:80 HTTP/1.1
//
// Allowed characters in the 'host + port' section of authority.
// See https://tools.ietf.org/html/rfc3986#section-3.2
RawTarget = target.GetAsciiStringNonNullCharacters();
Path = string.Empty;
PathBase = string.Empty;
QueryString = string.Empty;
}
private void OnAsteriskFormTarget(HttpMethod method)
{
// The asterisk-form of request-target is only used for a server-wide
// OPTIONS request (https://tools.ietf.org/html/rfc7231#section-4.3.7).
if (method != HttpMethod.Options)
{
RejectRequest(RequestRejectionReason.OptionsMethodRequired);
}
RawTarget = Asterisk;
Path = string.Empty;
PathBase = string.Empty;
QueryString = string.Empty;
}
private void OnAbsoluteFormTarget(Span<byte> target, Span<byte> query, Span<byte> line)
{
// absolute-form
// https://tools.ietf.org/html/rfc7230#section-5.3.2
// This code should be the edge-case.
// From the spec:
// a server MUST accept the absolute-form in requests, even though
// HTTP/1.1 clients will only send them in requests to proxies.
RawTarget = target.GetAsciiStringNonNullCharacters();
// Validation of absolute URIs is slow, but clients
// should not be sending this form anyways, so perf optimization
// not high priority
if (!Uri.TryCreate(RawTarget, UriKind.Absolute, out var uri))
{
if (Log.IsEnabled(LogLevel.Information))
{
RejectRequestLine(line);
}
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine);
}
SetNormalizedPath(uri.LocalPath);
// don't use uri.Query because we need the unescaped version
QueryString = query.GetAsciiStringNonNullCharacters();
}
private void SetNormalizedPath(string requestPath)
{
var normalizedTarget = PathNormalizer.RemoveDotSegments(requestPath);
if (RequestUrlStartsWithPathBase(normalizedTarget, out bool caseMatches))
{
PathBase = caseMatches ? _pathBase : normalizedTarget.Substring(0, _pathBase.Length);
Path = normalizedTarget.Substring(_pathBase.Length);
}
else if (rawTarget[0] == '/') // check rawTarget since normalizedTarget can be "" or "/" after dot segment removal
else
{
Path = normalizedTarget;
}
else
}
private unsafe static string GetUtf8String(Span<byte> path)
{
// .NET 451 doesn't have pointer overloads for Encoding.GetString so we
// copy to an array
#if NET451
return Encoding.UTF8.GetString(path.ToArray());
#else
fixed (byte* pointer = &path.DangerousGetPinnableReference())
{
Path = string.Empty;
PathBase = string.Empty;
QueryString = string.Empty;
return Encoding.UTF8.GetString(pointer, path.Length);
}
#endif
}
public void OnHeader(Span<byte> name, Span<byte> value)

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public enum HttpScheme
{
Unknown = -1,
Http = 0,
Https = 1
}
}

View File

@ -7,6 +7,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public interface IHttpRequestLineHandler
{
void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod);
void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, Span<byte> line, bool pathEncoded);
}
}

View File

@ -34,219 +34,132 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
consumed = buffer.Start;
examined = buffer.End;
ReadCursor end;
Span<byte> span;
// If the buffer is a single span then use it to find the LF
if (buffer.IsSingleSpan)
// Prepare the first span
var span = buffer.First.Span;
var lineIndex = span.IndexOfVectorized(ByteLF);
if (lineIndex >= 0)
{
var startLineSpan = buffer.First.Span;
var lineIndex = startLineSpan.IndexOfVectorized(ByteLF);
if (lineIndex == -1)
{
return false;
}
end = buffer.Move(consumed, lineIndex + 1);
span = startLineSpan.Slice(0, lineIndex + 1);
consumed = buffer.Move(consumed, lineIndex + 1);
span = span.Slice(0, lineIndex + 1);
}
else
else if (buffer.IsSingleSpan || !TryGetNewLineSpan(ref buffer, ref span, out consumed))
{
var start = buffer.Start;
if (ReadCursorOperations.Seek(start, buffer.End, out end, ByteLF) == -1)
{
return false;
}
// Move 1 byte past the \n
end = buffer.Move(end, 1);
var startLineBuffer = buffer.Slice(start, end);
span = startLineBuffer.ToSpan();
// No request line end
return false;
}
var pathStart = -1;
var queryStart = -1;
var queryEnd = -1;
var pathEnd = -1;
var versionStart = -1;
var httpVersion = HttpVersion.Unknown;
HttpMethod method;
Span<byte> customMethod;
var i = 0;
var length = span.Length;
var done = false;
// Fix and parse the span
fixed (byte* data = &span.DangerousGetPinnableReference())
{
switch (StartLineState.KnownMethod)
ParseRequestLine(handler, data, span.Length);
}
examined = consumed;
return true;
}
private unsafe void ParseRequestLine<T>(T handler, byte* data, int length) where T : IHttpRequestLineHandler
{
int offset;
Span<byte> customMethod;
// Get Method and set the offset
var method = HttpUtilities.GetKnownMethod(data, length, out offset);
if (method == HttpMethod.Custom)
{
customMethod = GetUnknownMethod(data, length, out offset);
}
// Skip space
offset++;
byte ch = 0;
// Target = Path and Query
var pathEncoded = false;
var pathStart = -1;
for (; offset < length; offset++)
{
ch = data[offset];
if (ch == ByteSpace)
{
case StartLineState.KnownMethod:
if (span.GetKnownMethod(out method, out var methodLength))
{
// Update the index, current char, state and jump directly
// to the next state
i += methodLength + 1;
if (pathStart == -1)
{
// Empty path is illegal
RejectRequestLine(data, length);
}
goto case StartLineState.Path;
}
goto case StartLineState.UnknownMethod;
break;
}
else if (ch == ByteQuestionMark)
{
if (pathStart == -1)
{
// Empty path is illegal
RejectRequestLine(data, length);
}
case StartLineState.UnknownMethod:
for (; i < length; i++)
{
var ch = data[i];
break;
}
else if (ch == BytePercentage)
{
if (pathStart == -1)
{
// Path starting with % is illegal
RejectRequestLine(data, length);
}
if (ch == ByteSpace)
{
customMethod = span.Slice(0, i);
if (customMethod.Length == 0)
{
RejectRequestLine(span);
}
// Consume space
i++;
goto case StartLineState.Path;
}
if (!IsValidTokenChar((char)ch))
{
RejectRequestLine(span);
}
}
break;
case StartLineState.Path:
for (; i < length; i++)
{
var ch = data[i];
if (ch == ByteSpace)
{
pathEnd = i;
if (pathStart == -1)
{
// Empty path is illegal
RejectRequestLine(span);
}
// No query string found
queryStart = queryEnd = i;
// Consume space
i++;
goto case StartLineState.KnownVersion;
}
else if (ch == ByteQuestionMark)
{
pathEnd = i;
if (pathStart == -1)
{
// Empty path is illegal
RejectRequestLine(span);
}
queryStart = i;
goto case StartLineState.QueryString;
}
else if (ch == BytePercentage)
{
if (pathStart == -1)
{
RejectRequestLine(span);
}
}
if (pathStart == -1)
{
pathStart = i;
}
}
break;
case StartLineState.QueryString:
for (; i < length; i++)
{
var ch = data[i];
if (ch == ByteSpace)
{
queryEnd = i;
// Consume space
i++;
goto case StartLineState.KnownVersion;
}
}
break;
case StartLineState.KnownVersion:
// REVIEW: We don't *need* to slice here but it makes the API
// nicer, slicing should be free :)
if (span.Slice(i).GetKnownVersion(out httpVersion, out var versionLenght))
{
// Update the index, current char, state and jump directly
// to the next state
i += versionLenght + 1;
goto case StartLineState.NewLine;
}
versionStart = i;
goto case StartLineState.UnknownVersion;
case StartLineState.UnknownVersion:
for (; i < length; i++)
{
var ch = data[i];
if (ch == ByteCR)
{
var versionSpan = span.Slice(versionStart, i - versionStart);
if (versionSpan.Length == 0)
{
RejectRequestLine(span);
}
else
{
RejectRequest(RequestRejectionReason.UnrecognizedHTTPVersion,
versionSpan.GetAsciiStringEscaped(32));
}
}
}
break;
case StartLineState.NewLine:
if (data[i] != ByteLF)
{
RejectRequestLine(span);
}
i++;
goto case StartLineState.Complete;
case StartLineState.Complete:
done = true;
break;
pathEncoded = true;
}
else if (pathStart == -1)
{
pathStart = offset;
}
}
if (!done)
if (pathStart == -1)
{
RejectRequestLine(span);
// End of path not found
RejectRequestLine(data, length);
}
var pathBuffer = span.Slice(pathStart, pathEnd - pathStart);
var targetBuffer = span.Slice(pathStart, queryEnd - pathStart);
var query = span.Slice(queryStart, queryEnd - queryStart);
var pathBuffer = new Span<byte>(data + pathStart, offset - pathStart);
handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod);
var queryStart = offset;
// Query string
if (ch == ByteQuestionMark)
{
// We have a query string
for (; offset < length; offset++)
{
ch = data[offset];
if (ch == ByteSpace)
{
break;
}
}
}
consumed = end;
examined = consumed;
return true;
var targetBuffer = new Span<byte>(data + pathStart, offset - pathStart);
var query = new Span<byte>(data + queryStart, offset - queryStart);
// Consume space
offset++;
// Version
var httpVersion = HttpUtilities.GetKnownVersion(data + offset, length - offset);
if (httpVersion == HttpVersion.Unknown)
{
RejectUnknownVersion(data, length, offset);
}
// After version 8 bytes and cr 1 byte, expect lf
if (data[offset + 8 + 1] != ByteLF)
{
RejectRequestLine(data, length);
}
var line = new Span<byte>(data, length);
handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod, line, pathEncoded);
}
public unsafe bool ParseHeaders<T>(T handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes) where T : IHttpHeadersHandler
@ -316,11 +229,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
// Headers don't end in CRLF line.
RejectRequest(RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence);
}
else if(ch1 == ByteSpace || ch1 == ByteTab)
{
RejectRequest(RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName);
RejectRequest(RequestRejectionReason.InvalidRequestHeadersNoCRLF);
}
// We moved the reader so look ahead 2 bytes so reset both the reader
@ -390,7 +299,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe int FindEndOfName(byte* headerLine, int length)
private unsafe int FindEndOfName(byte* headerLine, int length)
{
var index = 0;
var sawWhitespace = false;
@ -407,14 +316,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
if (index == length)
if (index == length || sawWhitespace)
{
RejectRequest(RequestRejectionReason.NoColonCharacterFoundInHeaderLine);
}
if (sawWhitespace)
{
RejectRequest(RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName);
RejectRequestHeader(headerLine, length);
}
return index;
}
@ -427,17 +333,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
if (headerLine[valueEnd + 2] != ByteLF)
{
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
RejectRequestHeader(headerLine, length);
}
if (headerLine[valueEnd + 1] != ByteCR)
{
RejectRequest(RequestRejectionReason.MissingCRInHeaderLine);
RejectRequestHeader(headerLine, length);
}
// Skip colon from value start
var valueStart = nameEnd + 1;
// Ignore start whitespace
for(; valueStart < valueEnd; valueStart++)
for (; valueStart < valueEnd; valueStart++)
{
var ch = headerLine[valueStart];
if (ch != ByteTab && ch != ByteSpace && ch != ByteCR)
@ -446,16 +352,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
else if (ch == ByteCR)
{
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
RejectRequestHeader(headerLine, length);
}
}
// Check for CR in value
var i = valueStart + 1;
if (Contains(headerLine + i, valueEnd - i, ByteCR))
{
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
RejectRequestHeader(headerLine, length);
}
// Ignore end whitespace
@ -506,10 +411,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
return false;
found:
found:
return true;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool TryGetNewLineSpan(ref ReadableBuffer buffer, ref Span<byte> span, out ReadCursor end)
{
var start = buffer.Start;
if (ReadCursorOperations.Seek(start, buffer.End, out end, ByteLF) != -1)
{
// Move 1 byte past the \n
end = buffer.Move(end, 1);
span = buffer.Slice(start, end).ToSpan();
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private unsafe Span<byte> GetUnknownMethod(byte* data, int length, out int methodLength)
{
methodLength = 0;
for (var i = 0; i < length; i++)
{
var ch = data[i];
if (ch == ByteSpace)
{
if (i == 0)
{
RejectRequestLine(data, length);
}
methodLength = i;
break;
}
else if (!IsValidTokenChar((char)ch))
{
RejectRequestLine(data, length);
}
}
return new Span<byte>(data, methodLength);
}
private static bool IsValidTokenChar(char c)
{
// Determines if a character is valid as a 'token' as defined in the
@ -540,9 +487,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
throw BadHttpRequestException.GetException(reason);
}
public static void RejectRequest(RequestRejectionReason reason, string value)
private unsafe void RejectUnknownVersion(byte* data, int length, int versionStart)
{
throw BadHttpRequestException.GetException(reason, value);
throw GetRejectUnknownVersion(data, length, versionStart);
}
private unsafe void RejectRequestLine(byte* data, int length)
{
throw GetRejectRequestLineException(new Span<byte>(data, length));
}
private void RejectRequestLine(Span<byte> span)
@ -557,9 +509,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
Log.IsEnabled(LogLevel.Information) ? span.GetAsciiStringEscaped(MaxRequestLineError) : string.Empty);
}
private unsafe BadHttpRequestException GetRejectUnknownVersion(byte* data, int length, int versionStart)
{
var span = new Span<byte>(data, length);
length -= versionStart;
for (var i = 0; i < length; i++)
{
var ch = span[i + versionStart];
if (ch == ByteCR)
{
if (i == 0)
{
return GetRejectRequestLineException(span);
}
else
{
return BadHttpRequestException.GetException(RequestRejectionReason.UnrecognizedHTTPVersion,
span.Slice(versionStart, i).GetAsciiStringEscaped(32));
}
}
}
return GetRejectRequestLineException(span);
}
private unsafe void RejectRequestHeader(byte* headerLine, int length)
{
RejectRequestHeader(new Span<byte>(headerLine, length));
}
private void RejectRequestHeader(Span<byte> span)
{
throw GetRejectRequestHeaderException(span);
}
private BadHttpRequestException GetRejectRequestHeaderException(Span<byte> span)
{
const int MaxRequestHeaderError = 128;
return BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestHeader,
Log.IsEnabled(LogLevel.Information) ? span.GetAsciiStringEscaped(MaxRequestHeaderError) : string.Empty);
}
public void Reset()
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -570,26 +562,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
// https://github.com/dotnet/coreclr/issues/7459#issuecomment-253965670
return Vector.AsVectorByte(new Vector<uint>(vectorByte * 0x01010101u));
}
private enum HeaderState
{
Name,
Whitespace,
ExpectValue,
ExpectNewLine,
Complete
}
private enum StartLineState
{
KnownMethod,
UnknownMethod,
Path,
QueryString,
KnownVersion,
UnknownVersion,
NewLine,
Complete
}
}
}

View File

@ -6,12 +6,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public enum RequestRejectionReason
{
UnrecognizedHTTPVersion,
HeadersCorruptedInvalidHeaderSequence,
NoColonCharacterFoundInHeaderLine,
WhitespaceIsNotAllowedInHeaderName,
HeaderValueMustNotContainCR,
HeaderValueLineFoldingNotSupported,
InvalidRequestLine,
InvalidRequestHeader,
InvalidRequestHeadersNoCRLF,
MalformedRequestInvalidHeaders,
InvalidContentLength,
MultipleContentLengths,
@ -19,16 +16,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
BadChunkSuffix,
BadChunkSizeData,
ChunkedRequestIncomplete,
PathContainsNullCharacters,
InvalidCharactersInHeaderName,
NonAsciiOrNullCharactersInInputString,
RequestLineTooLong,
HeadersExceedMaxTotalSize,
MissingCRInHeaderLine,
TooManyHeaders,
RequestTimeout,
FinalTransferCodingNotChunked,
LengthRequired,
LengthRequiredHttp10
LengthRequiredHttp10,
OptionsMethodRequired,
ConnectMethodRequired,
}
}

View File

@ -35,8 +35,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
private readonly static ulong _mask4Chars = GetMaskAsLong(new byte[]
{0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00});
private readonly static Tuple<ulong, ulong, HttpMethod, int, bool>[] _knownMethods =
new Tuple<ulong, ulong, HttpMethod, int, bool>[17];
private readonly static Tuple<ulong, ulong, HttpMethod, int>[] _knownMethods =
new Tuple<ulong, ulong, HttpMethod, int>[17];
private readonly static string[] _methodNames = new string[9];

View File

@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
@ -13,34 +14,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
{
public const string Http10Version = "HTTP/1.0";
public const string Http11Version = "HTTP/1.1";
private const uint _httpGetMethodInt = 542393671; // retun of GetAsciiStringAsInt("GET "); const results in better codegen
public const string HttpUriScheme = "http://";
public const string HttpsUriScheme = "https://";
// readonly primitive statics can be Jit'd to consts https://github.com/dotnet/coreclr/issues/1079
private readonly static ulong _httpSchemeLong = GetAsciiStringAsLong(HttpUriScheme + "\0");
private readonly static ulong _httpsSchemeLong = GetAsciiStringAsLong(HttpsUriScheme);
private const uint _httpGetMethodInt = 542393671; // retun of GetAsciiStringAsInt("GET "); const results in better codegen
private const ulong _http10VersionLong = 3471766442030158920; // GetAsciiStringAsLong("HTTP/1.0"); const results in better codegen
private const ulong _http11VersionLong = 3543824036068086856; // GetAsciiStringAsLong("HTTP/1.1"); const results in better codegen
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SetKnownMethod(ulong mask, ulong knownMethodUlong, HttpMethod knownMethod, int length)
{
_knownMethods[GetKnownMethodIndex(knownMethodUlong)] = new Tuple<ulong, ulong, HttpMethod, int, bool>(mask, knownMethodUlong, knownMethod, length, true);
}
private unsafe static ulong GetMaskAsLong(byte[] bytes)
{
Debug.Assert(bytes.Length == 8, "Mask must be exactly 8 bytes long.");
fixed (byte* ptr = bytes)
{
return *(ulong*)ptr;
}
_knownMethods[GetKnownMethodIndex(knownMethodUlong)] = new Tuple<ulong, ulong, HttpMethod, int>(mask, knownMethodUlong, knownMethod, length);
}
private static void FillKnownMethodsGaps()
{
var knownMethods = _knownMethods;
var length = knownMethods.Length;
var invalidHttpMethod = new Tuple<ulong, ulong, HttpMethod, int, bool>(_mask8Chars, 0ul, HttpMethod.Custom, 0, false);
var invalidHttpMethod = new Tuple<ulong, ulong, HttpMethod, int>(_mask8Chars, 0ul, HttpMethod.Custom, 0);
for (int i = 0; i < length; i++)
{
if (knownMethods[i] == null)
@ -74,7 +71,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
}
}
private unsafe static ulong GetMaskAsLong(byte[] bytes)
{
Debug.Assert(bytes.Length == 8, "Mask must be exactly 8 bytes long.");
fixed (byte* ptr = bytes)
{
return *(ulong*)ptr;
}
}
public unsafe static string GetAsciiStringNonNullCharacters(this Span<byte> span)
{
@ -106,7 +111,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
for (i = 0; i < Math.Min(span.Length, maxChars); ++i)
{
var ch = span[i];
sb.Append(ch < 0x20 || ch >= 0x7F ? $"<0x{ch:X2}>" : ((char)ch).ToString());
sb.Append(ch < 0x20 || ch >= 0x7F ? $"\\x{ch:X2}" : ((char)ch).ToString());
}
if (span.Length > maxChars)
@ -129,35 +134,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
/// </remarks>
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool GetKnownMethod(this Span<byte> span, out HttpMethod method, out int length)
public static unsafe bool GetKnownMethod(this Span<byte> span, out HttpMethod method, out int length)
{
if (span.TryRead<uint>(out var possiblyGet))
fixed (byte* data = &span.DangerousGetPinnableReference())
{
if (possiblyGet == _httpGetMethodInt)
{
length = 3;
method = HttpMethod.Get;
return true;
}
method = GetKnownMethod(data, span.Length, out length);
return method != HttpMethod.Custom;
}
}
if (span.TryRead<ulong>(out var value))
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe static HttpMethod GetKnownMethod(byte* data, int length, out int methodLength)
{
methodLength = 0;
if (length < sizeof(uint))
{
return HttpMethod.Custom;
}
else if (*(uint*)data == _httpGetMethodInt)
{
methodLength = 3;
return HttpMethod.Get;
}
else if (length < sizeof(ulong))
{
return HttpMethod.Custom;
}
else
{
var value = *(ulong*)data;
var key = GetKnownMethodIndex(value);
var x = _knownMethods[key];
if (x != null && (value & x.Item1) == x.Item2)
{
method = x.Item3;
length = x.Item4;
return x.Item5;
methodLength = x.Item4;
return x.Item3;
}
}
method = HttpMethod.Custom;
length = 0;
return false;
return HttpMethod.Custom;
}
/// <summary>
@ -172,35 +188,83 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
/// </remarks>
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool GetKnownVersion(this Span<byte> span, out HttpVersion knownVersion, out byte length)
public static unsafe bool GetKnownVersion(this Span<byte> span, out HttpVersion knownVersion, out byte length)
{
if (span.TryRead<ulong>(out var version))
fixed (byte* data = &span.DangerousGetPinnableReference())
{
if (version == _http11VersionLong)
knownVersion = GetKnownVersion(data, span.Length);
if (knownVersion != HttpVersion.Unknown)
{
length = sizeof(ulong);
knownVersion = HttpVersion.Http11;
}
else if (version == _http10VersionLong)
{
length = sizeof(ulong);
knownVersion = HttpVersion.Http10;
}
else
{
length = 0;
knownVersion = HttpVersion.Unknown;
return false;
return true;
}
if (span[sizeof(ulong)] == (byte)'\r')
length = 0;
return false;
}
}
/// <summary>
/// Checks 9 bytes from <paramref name="location"/> correspond to a known HTTP version.
/// </summary>
/// <remarks>
/// A "known HTTP version" Is is either HTTP/1.0 or HTTP/1.1.
/// Since those fit in 8 bytes, they can be optimally looked up by reading those bytes as a long. Once
/// in that format, it can be checked against the known versions.
/// The Known versions will be checked with the required '\r'.
/// To optimize performance the HTTP/1.1 will be checked first.
/// </remarks>
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe static HttpVersion GetKnownVersion(byte* location, int length)
{
HttpVersion knownVersion;
var version = *(ulong*)location;
if (length < sizeof(ulong) + 1 || location[sizeof(ulong)] != (byte)'\r')
{
knownVersion = HttpVersion.Unknown;
}
else if (version == _http11VersionLong)
{
knownVersion = HttpVersion.Http11;
}
else if (version == _http10VersionLong)
{
knownVersion = HttpVersion.Http10;
}
else
{
knownVersion = HttpVersion.Unknown;
}
return knownVersion;
}
/// <summary>
/// Checks 8 bytes from <paramref name="span"/> that correspond to 'http://' or 'https://'
/// </summary>
/// <param name="span">The span</param>
/// <param name="knownScheme">A reference to the known scheme, if the input matches any</param>
/// <returns>True when memory starts with known http or https schema</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool GetKnownHttpScheme(this Span<byte> span, out HttpScheme knownScheme)
{
if (span.TryRead<ulong>(out var value))
{
if ((value & _mask7Chars) == _httpSchemeLong)
{
knownScheme = HttpScheme.Http;
return true;
}
if (value == _httpsSchemeLong)
{
knownScheme = HttpScheme.Https;
return true;
}
}
knownVersion = HttpVersion.Unknown;
length = 0;
knownScheme = HttpScheme.Unknown;
return false;
}
@ -225,5 +289,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
}
return null;
}
public static string SchemeToString(HttpScheme scheme)
{
switch (scheme)
{
case HttpScheme.Http:
return HttpUriScheme;
case HttpScheme.Https:
return HttpsUriScheme;
default:
return null;
}
}
}
}

View File

@ -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 == '@';
}
}
}

View File

@ -23,7 +23,6 @@
<PackageReference Include="System.Numerics.Vectors" Version="$(CoreFxVersion)" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="$(CoreFxVersion)" />
<PackageReference Include="System.IO.Pipelines" Version="$(CoreFxLabsPipelinesVersion)" />
<PackageReference Include="System.IO.Pipelines.Text.Primitives" Version="$(CoreFxLabsVersion)" />
<PackageReference Include="System.Text.Encodings.Web.Utf8" Version="$(CoreFxLabsVersion)" />
</ItemGroup>

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
@ -90,7 +91,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
$"Invalid content length: {contentLength}");
}
private async Task TestBadRequest(string request, string expectedResponseStatusCode, string expectedExceptionMessage)
[Theory]
[InlineData("GET *", "OPTIONS")]
[InlineData("GET www.host.com", "CONNECT")]
public Task RejectsIncorrectMethods(string request, string allowedMethod)
{
return TestBadRequest(
$"{request} HTTP/1.1\r\n",
"405 Method Not Allowed",
"Method not allowed.",
$"Allow: {allowedMethod}");
}
private async Task TestBadRequest(string request, string expectedResponseStatusCode, string expectedExceptionMessage, string expectedAllowHeader = null)
{
BadHttpRequestException loggedException = null;
var mockKestrelTrace = new Mock<IKestrelTrace>();
@ -106,7 +119,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
using (var connection = server.CreateConnection())
{
await connection.SendAll(request);
await ReceiveBadRequestResponse(connection, expectedResponseStatusCode, server.Context.DateHeaderValue);
await ReceiveBadRequestResponse(connection, expectedResponseStatusCode, server.Context.DateHeaderValue, expectedAllowHeader);
}
}
@ -114,15 +127,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
Assert.Equal(expectedExceptionMessage, loggedException.Message);
}
private async Task ReceiveBadRequestResponse(TestConnection connection, string expectedResponseStatusCode, string expectedDateHeaderValue)
private async Task ReceiveBadRequestResponse(TestConnection connection, string expectedResponseStatusCode, string expectedDateHeaderValue, string expectedAllowHeader = null)
{
await connection.ReceiveForcedEnd(
var lines = new[]
{
$"HTTP/1.1 {expectedResponseStatusCode}",
"Connection: close",
$"Date: {expectedDateHeaderValue}",
"Content-Length: 0",
expectedAllowHeader,
"",
"");
""
};
await connection.ReceiveForcedEnd(lines.Where(f => f != null).ToArray());
}
public static TheoryData<string, string> InvalidRequestLineData
@ -131,9 +149,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
var data = new TheoryData<string, string>();
string Escape(string line)
{
return line
.Replace("\r", @"\x0D")
.Replace("\n", @"\x0A")
.Replace("\0", @"\x00");
}
foreach (var requestLine in HttpParsingData.RequestLineInvalidData)
{
data.Add(requestLine, $"Invalid request line: {requestLine.Replace("\r", "<0x0D>").Replace("\n", "<0x0A>")}");
data.Add(requestLine, $"Invalid request line: '{Escape(requestLine)}'");
}
foreach (var requestLine in HttpParsingData.RequestLineWithEncodedNullCharInTargetData)
@ -143,7 +169,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
foreach (var requestLine in HttpParsingData.RequestLineWithNullCharInTargetData)
{
data.Add(requestLine, "Invalid request line.");
data.Add(requestLine, $"Invalid request line.");
}
return data;

View File

@ -454,6 +454,72 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
}
}
[Theory]
[InlineData("http://localhost/abs/path", "/abs/path", null)]
[InlineData("https://localhost/abs/path", "/abs/path", null)] // handles mismatch scheme
[InlineData("https://localhost:22/abs/path", "/abs/path", null)] // handles mismatched ports
[InlineData("https://differenthost/abs/path", "/abs/path", null)] // handles mismatched hostname
[InlineData("http://localhost/", "/", null)]
[InlineData("http://root@contoso.com/path", "/path", null)]
[InlineData("http://root:password@contoso.com/path", "/path", null)]
[InlineData("https://localhost/", "/", null)]
[InlineData("http://localhost", "/", null)]
[InlineData("http://127.0.0.1/", "/", null)]
[InlineData("http://[::1]/", "/", null)]
[InlineData("http://[::1]:8080/", "/", null)]
[InlineData("http://localhost?q=123&w=xyz", "/", "123")]
[InlineData("http://localhost/?q=123&w=xyz", "/", "123")]
[InlineData("http://localhost/path?q=123&w=xyz", "/path", "123")]
[InlineData("http://localhost/path%20with%20space?q=abc%20123", "/path with space", "abc 123")]
public async Task CanHandleRequestsWithUrlInAbsoluteForm(string requestUrl, string expectedPath, string queryValue)
{
var pathTcs = new TaskCompletionSource<PathString>();
var rawTargetTcs = new TaskCompletionSource<string>();
var hostTcs = new TaskCompletionSource<HostString>();
var queryTcs = new TaskCompletionSource<IQueryCollection>();
using (var server = new TestServer(async context =>
{
pathTcs.TrySetResult(context.Request.Path);
hostTcs.TrySetResult(context.Request.Host);
queryTcs.TrySetResult(context.Request.Query);
rawTargetTcs.TrySetResult(context.Features.Get<IHttpRequestFeature>().RawTarget);
await context.Response.WriteAsync("Done");
}))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
$"GET {requestUrl} HTTP/1.1",
"Content-Length: 0",
"Host: localhost",
"",
"");
await connection.Receive($"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"4",
"Done")
.TimeoutAfter(TimeSpan.FromSeconds(10));
await Task.WhenAll(pathTcs.Task, rawTargetTcs.Task, hostTcs.Task, queryTcs.Task).TimeoutAfter(TimeSpan.FromSeconds(30));
Assert.Equal(new PathString(expectedPath), pathTcs.Task.Result);
Assert.Equal(requestUrl, rawTargetTcs.Task.Result);
Assert.Equal("localhost", hostTcs.Task.Result.ToString());
if (queryValue == null)
{
Assert.False(queryTcs.Task.Result.ContainsKey("q"));
}
else
{
Assert.Equal(queryValue, queryTcs.Task.Result["q"]);
}
}
}
}
private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress)
{
var builder = new WebHostBuilder()

View File

@ -94,6 +94,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
private class NullParser : IHttpParser
{
private readonly byte[] _startLine = Encoding.ASCII.GetBytes("GET /plaintext HTTP/1.1\r\n");
private readonly byte[] _target = Encoding.ASCII.GetBytes("/plaintext");
private readonly byte[] _hostHeaderName = Encoding.ASCII.GetBytes("Host");
private readonly byte[] _hostHeaderValue = Encoding.ASCII.GetBytes("www.example.com");
@ -119,7 +120,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
public bool ParseRequestLine<T>(T handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined) where T : IHttpRequestLineHandler
{
handler.OnStartLine(HttpMethod.Get, HttpVersion.Http11, new Span<byte>(_target), new Span<byte>(_target), Span<byte>.Empty, Span<byte>.Empty);
handler.OnStartLine(HttpMethod.Get,
HttpVersion.Http11,
new Span<byte>(_target),
new Span<byte>(_target),
Span<byte>.Empty,
Span<byte>.Empty,
new Span<byte>(_startLine),
false);
consumed = buffer.Start;
examined = buffer.End;

View File

@ -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)
{
}

View File

@ -42,6 +42,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
}
}
[Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
public void PlaintextAbsoluteUri()
{
for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
{
InsertData(RequestParsingData.PlaintextAbsoluteUriRequest);
ParseData();
}
}
[Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount * RequestParsingData.Pipelining)]
public void PipelinedPlaintextTechEmpower()
{

View File

@ -21,8 +21,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
"Connection: keep-alive\r\n" +
"\r\n";
// edge-casey - client's don't normally send this
private const string _plaintextAbsoluteUriRequest =
"GET http://localhost/plaintext HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7\r\n" +
"Connection: keep-alive\r\n" +
"\r\n";
private const string _liveaspnetRequest =
"GET https://live.asp.net/ HTTP/1.1\r\n" +
"GET / HTTP/1.1\r\n" +
"Host: live.asp.net\r\n" +
"Connection: keep-alive\r\n" +
"Upgrade-Insecure-Requests: 1\r\n" +
@ -35,7 +43,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
"\r\n";
private const string _unicodeRequest =
"GET http://stackoverflow.com/questions/40148683/why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric HTTP/1.1\r\n" +
"GET /questions/40148683/why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric HTTP/1.1\r\n" +
"Accept: text/html, application/xhtml+xml, image/jxr, */*\r\n" +
"Accept-Language: en-US,en-GB;q=0.7,en;q=0.3\r\n" +
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.14965\r\n" +
@ -53,6 +61,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
public static readonly byte[] PlaintextTechEmpowerPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_plaintextTechEmpowerRequest, Pipelining)));
public static readonly byte[] PlaintextTechEmpowerRequest = Encoding.ASCII.GetBytes(_plaintextTechEmpowerRequest);
public static readonly byte[] PlaintextAbsoluteUriRequest = Encoding.ASCII.GetBytes(_plaintextAbsoluteUriRequest);
public static readonly byte[] LiveaspnetPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_liveaspnetRequest, Pipelining)));
public static readonly byte[] LiveaspnetRequest = Encoding.ASCII.GetBytes(_liveaspnetRequest);

View File

@ -107,6 +107,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
Output = new MockSocketOutput(),
ConnectionControl = Mock.Of<IConnectionControl>()
};
connectionContext.ListenerContext.ServiceContext.HttpParserFactory = f => new Internal.Http.KestrelHttpParser(log: null);
var frame = new TestFrame<object>(application: null, context: connectionContext);
frame.Reset();

View File

@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
using Moq;
@ -81,25 +82,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
_pipelineFactory.Dispose();
}
[Fact]
public async Task TakeMessageHeadersThrowsOnHeaderValueWithLineFolding_CharacterNotAvailableOnFirstAttempt()
{
await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes("Header-1: value1\r\n"));
var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
Assert.False(_frame.TakeMessageHeaders(readableBuffer, out _consumed, out _examined));
_input.Reader.Advance(_consumed, _examined);
await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes(" "));
readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
var exception = Assert.Throws<BadHttpRequestException>(() => _frame.TakeMessageHeaders(readableBuffer, out _consumed, out _examined));
_input.Reader.Advance(_consumed, _examined);
Assert.Equal("Whitespace is not allowed in header name.", exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
}
[Fact]
public async Task TakeMessageHeadersThrowsWhenHeadersExceedTotalSizeLimit()
{
@ -347,6 +329,37 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Equal(new InvalidOperationException().Message, exception.Message);
}
[Theory]
[MemberData(nameof(RequestLineWithInvalidRequestTargetData))]
public async Task TakeStartLineThrowsWhenRequestTargetIsInvalid(string requestLine)
{
await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes(requestLine));
var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
var exception = Assert.Throws<BadHttpRequestException>(() =>
_frame.TakeStartLine(readableBuffer, out _consumed, out _examined));
_input.Reader.Advance(_consumed, _examined);
Assert.Equal($"Invalid request line: '{Escape(requestLine)}'", exception.Message);
}
[Theory]
[MemberData(nameof(MethodNotAllowedTargetData))]
public async Task TakeStartLineThrowsWhenMethodNotAllowed(string requestLine, HttpMethod allowedMethod)
{
await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes(requestLine));
var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
var exception = Assert.Throws<BadHttpRequestException>(() =>
_frame.TakeStartLine(readableBuffer, out _consumed, out _examined));
_input.Reader.Advance(_consumed, _examined);
Assert.Equal(405, exception.StatusCode);
Assert.Equal("Method not allowed.", exception.Message);
Assert.Equal(HttpUtilities.MethodToString(allowedMethod), exception.AllowedHeader);
}
[Fact]
public void RequestProcessingAsyncEnablesKeepAliveTimeout()
{
@ -535,6 +548,25 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
}
private string Escape(string requestLine)
{
var ellipsis = requestLine.Length > 32
? "..."
: string.Empty;
return requestLine
.Substring(0, Math.Min(32, requestLine.Length))
.Replace("\r", @"\x0D")
.Replace("\n", @"\x0A")
.Replace("\0", @"\x00")
+ ellipsis;
}
public static TheoryData<string> RequestLineWithInvalidRequestTargetData
=> HttpParsingData.RequestLineWithInvalidRequestTarget;
public static TheoryData<string, HttpMethod> MethodNotAllowedTargetData
=> HttpParsingData.MethodNotAllowedRequestLine;
public static TheoryData<string> RequestLineWithNullCharInTargetData
{
get

View File

@ -49,14 +49,17 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
It.IsAny<Span<byte>>(),
It.IsAny<Span<byte>>(),
It.IsAny<Span<byte>>(),
It.IsAny<Span<byte>>()))
.Callback<HttpMethod, HttpVersion, Span<byte>, Span<byte>, Span<byte>, Span<byte>>((method, version, target, path, query, customMethod) =>
It.IsAny<Span<byte>>(),
It.IsAny<Span<byte>>(),
It.IsAny<bool>()))
.Callback<HttpMethod, HttpVersion, Span<byte>, Span<byte>, Span<byte>, Span<byte>, Span<byte>, bool>((method, version, target, path, query, customMethod, line, pathEncoded) =>
{
parsedMethod = method != HttpMethod.Custom ? HttpUtilities.MethodToString(method) : customMethod.GetAsciiStringNonNullCharacters();
parsedVersion = HttpUtilities.VersionToString(version);
parsedRawTarget = target.GetAsciiStringNonNullCharacters();
parsedRawPath = path.GetAsciiStringNonNullCharacters();
parsedQuery = query.GetAsciiStringNonNullCharacters();
pathEncoded = false;
});
Assert.True(parser.ParseRequestLine(requestLineHandler.Object, buffer, out var consumed, out var examined));
@ -108,7 +111,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
var exception = Assert.Throws<BadHttpRequestException>(() =>
parser.ParseRequestLine(Mock.Of<IHttpRequestLineHandler>(), buffer, out var consumed, out var examined));
Assert.Equal($"Invalid request line: {requestLine.Replace("\r", "<0x0D>").Replace("\n", "<0x0A>")}", exception.Message);
Assert.Equal($"Invalid request line: '{requestLine.Replace("\r", "\\x0D").Replace("\n", "\\x0A")}'", exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, (exception as BadHttpRequestException).StatusCode);
}
@ -306,7 +309,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[MemberData(nameof(RequestHeaderInvalidData))]
public void ParseHeadersThrowsOnInvalidRequestHeaders(string rawHeaders, string expectedExceptionMessage)
{
var parser = CreateParser(Mock.Of<IKestrelTrace>());
var mockTrace = new Mock<IKestrelTrace>();
mockTrace
.Setup(trace => trace.IsEnabled(LogLevel.Information))
.Returns(true);
var parser = CreateParser(mockTrace.Object);
var buffer = ReadableBuffer.Create(Encoding.ASCII.GetBytes(rawHeaders));
var exception = Assert.Throws<BadHttpRequestException>(() =>

View File

@ -84,11 +84,23 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{
TestKnownStringsInterning(input, expected, span =>
{
HttpUtilities.GetKnownVersion(span, out var version, out var lenght);
HttpUtilities.GetKnownVersion(span, out var version, out var _);
return HttpUtilities.VersionToString(version);
});
}
[Theory]
[InlineData("https://host/", "https://")]
[InlineData("http://host/", "http://")]
public void KnownSchemesAreInterned(string input, string expected)
{
TestKnownStringsInterning(input, expected, span =>
{
HttpUtilities.GetKnownHttpScheme(span, out var scheme);
return HttpUtilities.SchemeToString(scheme);
});
}
[Theory]
[InlineData("CONNECT / HTTP/1.1", "CONNECT")]
[InlineData("DELETE / HTTP/1.1", "DELETE")]

View File

@ -6,6 +6,8 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Xunit;
@ -93,19 +95,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Theory]
[InlineData("*")]
[InlineData("*/?arg=value")]
[InlineData("*?arg=value")]
[InlineData("DoesNotStartWith/")]
[InlineData("DoesNotStartWith/?arg=value")]
[InlineData("DoesNotStartWithSlash?arg=value")]
[InlineData("./")]
[InlineData("../")]
[InlineData("../.")]
[InlineData(".././")]
[InlineData("../..")]
[InlineData("../../")]
public async Task NonPathRequestTargetSetInRawTarget(string requestTarget)
[InlineData(HttpMethod.Options, "*")]
[InlineData(HttpMethod.Connect, "host")]
public async Task NonPathRequestTargetSetInRawTarget(HttpMethod method, string requestTarget)
{
var testContext = new TestServiceContext();
@ -123,7 +115,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
using (var connection = server.CreateConnection())
{
await connection.Send(
$"GET {requestTarget} HTTP/1.1",
$"{HttpUtilities.MethodToString(method)} {requestTarget} HTTP/1.1",
"",
"");
await connection.ReceiveEnd(

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using System;
using System.Collections.Generic;
using System.Linq;
@ -33,6 +34,26 @@ namespace Microsoft.AspNetCore.Testing
Tuple.Create("/%C3%A5/bc", "/\u00E5/bc"),
Tuple.Create("/%25", "/%"),
Tuple.Create("/%2F", "/%2F"),
Tuple.Create("http://host/abs/path", "/abs/path"),
Tuple.Create("http://host/abs/path/", "/abs/path/"),
Tuple.Create("http://host/a%20b%20c/", "/a b c/"),
Tuple.Create("https://host/abs/path", "/abs/path"),
Tuple.Create("https://host/abs/path/", "/abs/path/"),
Tuple.Create("https://host:22/abs/path", "/abs/path"),
Tuple.Create("https://user@host:9080/abs/path", "/abs/path"),
Tuple.Create("http://host/", "/"),
Tuple.Create("http://host", "/"),
Tuple.Create("https://host/", "/"),
Tuple.Create("https://host", "/"),
Tuple.Create("http://user@host/", "/"),
Tuple.Create("http://127.0.0.1/", "/"),
Tuple.Create("http://user@127.0.0.1/", "/"),
Tuple.Create("http://user@127.0.0.1:8080/", "/"),
Tuple.Create("http://127.0.0.1:8080/", "/"),
Tuple.Create("http://[::1]", "/"),
Tuple.Create("http://[::1]/path", "/path"),
Tuple.Create("http://[::1]:8080/", "/"),
Tuple.Create("http://user@[::1]:8080/", "/"),
};
var queryStrings = new[]
{
@ -173,9 +194,73 @@ namespace Microsoft.AspNetCore.Testing
"GET /%E8%01%00 HTTP/1.1\r\n",
};
public static TheoryData<string> RequestLineWithInvalidRequestTarget => new TheoryData<string>
{
// Invalid absolute-form requests
"GET http:// HTTP/1.1\r\n",
"GET http:/ HTTP/1.1\r\n",
"GET https:/ HTTP/1.1\r\n",
"GET http:/// HTTP/1.1\r\n",
"GET https:// HTTP/1.1\r\n",
"GET http://// HTTP/1.1\r\n",
"GET http://:80 HTTP/1.1\r\n",
"GET http://:80/abc HTTP/1.1\r\n",
"GET http://user@ HTTP/1.1\r\n",
"GET http://user@/abc HTTP/1.1\r\n",
"GET http://abc%20xyz/abc HTTP/1.1\r\n",
"GET http://%20/abc?query=%0A HTTP/1.1\r\n",
// Valid absolute-form but with unsupported schemes
"GET otherscheme://host/ HTTP/1.1\r\n",
"GET ws://host/ HTTP/1.1\r\n",
"GET wss://host/ HTTP/1.1\r\n",
// Must only have one asterisk
"OPTIONS ** HTTP/1.1\r\n",
// Relative form
"GET ../../ HTTP/1.1\r\n",
"GET ..\\. HTTP/1.1\r\n",
};
public static TheoryData<string, HttpMethod> MethodNotAllowedRequestLine
{
get
{
var methods = new[]
{
"GET",
"PUT",
"DELETE",
"POST",
"HEAD",
"TRACE",
"PATCH",
"CONNECT",
//"OPTIONS",
"CUSTOM",
};
var theoryData = new TheoryData<string, HttpMethod>();
foreach (var line in methods
.Select(m => Tuple.Create($"{m} * HTTP/1.1\r\n", HttpMethod.Options))
.Concat(new[]
{
// CONNECT required for authority-form targets
Tuple.Create("GET http:80 HTTP/1.1\r\n", HttpMethod.Connect),
Tuple.Create("GET http: HTTP/1.1\r\n", HttpMethod.Connect),
Tuple.Create("GET https: HTTP/1.1\r\n", HttpMethod.Connect),
Tuple.Create("GET . HTTP/1.1\r\n", HttpMethod.Connect),
}))
{
theoryData.Add(line.Item1, line.Item2);
}
return theoryData;
}
}
public static IEnumerable<string> RequestLineWithNullCharInTargetData => new[]
{
"GET \0 HTTP/1.1\r\n",
// TODO re-enable after we get both #1469 and #1470 merged
// "GET \0 HTTP/1.1\r\n",
"GET /\0 HTTP/1.1\r\n",
"GET /\0\0 HTTP/1.1\r\n",
"GET /%C8\0 HTTP/1.1\r\n",
@ -183,6 +268,8 @@ namespace Microsoft.AspNetCore.Testing
public static TheoryData<string> UnrecognizedHttpVersionData => new TheoryData<string>
{
" ",
"/",
"H",
"HT",
"HTT",
@ -203,106 +290,79 @@ namespace Microsoft.AspNetCore.Testing
"8charact",
};
public static IEnumerable<object[]> RequestHeaderInvalidData
public static IEnumerable<object[]> RequestHeaderInvalidData => new[]
{
get
{
// Line folding
var headersWithLineFolding = new[]
{
"Header: line1\r\n line2\r\n\r\n",
"Header: line1\r\n\tline2\r\n\r\n",
"Header: line1\r\n line2\r\n\r\n",
"Header: line1\r\n \tline2\r\n\r\n",
"Header: line1\r\n\t line2\r\n\r\n",
"Header: line1\r\n\t\tline2\r\n\r\n",
"Header: line1\r\n \t\t line2\r\n\r\n",
"Header: line1\r\n \t \t line2\r\n\r\n",
"Header-1: multi\r\n line\r\nHeader-2: value2\r\n\r\n",
"Header-1: value1\r\nHeader-2: multi\r\n line\r\n\r\n",
"Header-1: value1\r\n Header-2: value2\r\n\r\n",
"Header-1: value1\r\n\tHeader-2: value2\r\n\r\n",
};
// Missing CR
new[] { "Header: value\n\r\n", @"Invalid request header: 'Header: value\x0A'" },
new[] { "Header-1: value1\nHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header-1: value1\x0A'" },
new[] { "Header-1: value1\r\nHeader-2: value2\n\r\n", @"Invalid request header: 'Header-2: value2\x0A'" },
// CR in value
var headersWithCRInValue = new[]
{
"Header-1: value1\r\r\n",
"Header-1: val\rue1\r\n",
"Header-1: value1\rHeader-2: value2\r\n\r\n",
"Header-1: value1\r\nHeader-2: value2\r\r\n",
"Header-1: value1\r\nHeader-2: v\ralue2\r\n",
"Header-1: Value__\rVector16________Vector32\r\n",
"Header-1: Value___Vector16\r________Vector32\r\n",
"Header-1: Value___Vector16_______\rVector32\r\n",
"Header-1: Value___Vector16________Vector32\r\r\n",
"Header-1: Value___Vector16________Vector32_\r\r\n",
"Header-1: Value___Vector16________Vector32Value___Vector16_______\rVector32\r\n",
"Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\r\r\n",
"Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\r\r\n",
};
// Line folding
new[] { "Header: line1\r\n line2\r\n\r\n", @"Invalid request header: ' line2\x0D\x0A'" },
new[] { "Header: line1\r\n\tline2\r\n\r\n", @"Invalid request header: '\x09line2\x0D\x0A'" },
new[] { "Header: line1\r\n line2\r\n\r\n", @"Invalid request header: ' line2\x0D\x0A'" },
new[] { "Header: line1\r\n \tline2\r\n\r\n", @"Invalid request header: ' \x09line2\x0D\x0A'" },
new[] { "Header: line1\r\n\t line2\r\n\r\n", @"Invalid request header: '\x09 line2\x0D\x0A'" },
new[] { "Header: line1\r\n\t\tline2\r\n\r\n", @"Invalid request header: '\x09\x09line2\x0D\x0A'" },
new[] { "Header: line1\r\n \t\t line2\r\n\r\n", @"Invalid request header: ' \x09\x09 line2\x0D\x0A'" },
new[] { "Header: line1\r\n \t \t line2\r\n\r\n", @"Invalid request header: ' \x09 \x09 line2\x0D\x0A'" },
new[] { "Header-1: multi\r\n line\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: ' line\x0D\x0A'" },
new[] { "Header-1: value1\r\nHeader-2: multi\r\n line\r\n\r\n", @"Invalid request header: ' line\x0D\x0A'" },
new[] { "Header-1: value1\r\n Header-2: value2\r\n\r\n", @"Invalid request header: ' Header-2: value2\x0D\x0A'" },
new[] { "Header-1: value1\r\n\tHeader-2: value2\r\n\r\n", @"Invalid request header: '\x09Header-2: value2\x0D\x0A'" },
// Missing colon
var headersWithMissingColon = new[]
{
"Header-1 value1\r\n\r\n",
"Header-1 value1\r\nHeader-2: value2\r\n\r\n",
"Header-1: value1\r\nHeader-2 value2\r\n\r\n",
"\n"
};
// CR in value
new[] { "Header-1: value1\r\r\n", @"Invalid request header: 'Header-1: value1\x0D\x0D\x0A'" },
new[] { "Header-1: val\rue1\r\n", @"Invalid request header: 'Header-1: val\x0Due1\x0D\x0A'" },
new[] { "Header-1: value1\rHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header-1: value1\x0DHeader-2: value2\x0D\x0A'" },
new[] { "Header-1: value1\r\nHeader-2: value2\r\r\n", @"Invalid request header: 'Header-2: value2\x0D\x0D\x0A'" },
new[] { "Header-1: value1\r\nHeader-2: v\ralue2\r\n", @"Invalid request header: 'Header-2: v\x0Dalue2\x0D\x0A'" },
new[] { "Header-1: Value__\rVector16________Vector32\r\n", @"Invalid request header: 'Header-1: Value__\x0DVector16________Vector32\x0D\x0A'" },
new[] { "Header-1: Value___Vector16\r________Vector32\r\n", @"Invalid request header: 'Header-1: Value___Vector16\x0D________Vector32\x0D\x0A'" },
new[] { "Header-1: Value___Vector16_______\rVector32\r\n", @"Invalid request header: 'Header-1: Value___Vector16_______\x0DVector32\x0D\x0A'" },
new[] { "Header-1: Value___Vector16________Vector32\r\r\n", @"Invalid request header: 'Header-1: Value___Vector16________Vector32\x0D\x0D\x0A'" },
new[] { "Header-1: Value___Vector16________Vector32_\r\r\n", @"Invalid request header: 'Header-1: Value___Vector16________Vector32_\x0D\x0D\x0A'" },
new[] { "Header-1: Value___Vector16________Vector32Value___Vector16_______\rVector32\r\n", @"Invalid request header: 'Header-1: Value___Vector16________Vector32Value___Vector16_______\x0DVector32\x0D\x0A'" },
new[] { "Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\r\r\n", @"Invalid request header: 'Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\x0D\x0D\x0A'" },
new[] { "Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\r\r\n", @"Invalid request header: 'Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\x0D\x0D\x0A'" },
// Starting with whitespace
var headersStartingWithWhitespace = new[]
{
" Header: value\r\n\r\n",
"\tHeader: value\r\n\r\n",
" Header-1: value1\r\nHeader-2: value2\r\n\r\n",
"\tHeader-1: value1\r\nHeader-2: value2\r\n\r\n",
};
// Missing colon
new[] { "Header-1 value1\r\n\r\n", @"Invalid request header: 'Header-1 value1\x0D\x0A'" },
new[] { "Header-1 value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header-1 value1\x0D\x0A'" },
new[] { "Header-1: value1\r\nHeader-2 value2\r\n\r\n", @"Invalid request header: 'Header-2 value2\x0D\x0A'" },
new[] { "\n", @"Invalid request header: '\x0A'" },
// Whitespace in header name
var headersWithWithspaceInName = new[]
{
"Header : value\r\n\r\n",
"Header\t: value\r\n\r\n",
"Header\r: value\r\n\r\n",
"Header_\rVector16: value\r\n\r\n",
"Header__Vector16\r: value\r\n\r\n",
"Header__Vector16_\r: value\r\n\r\n",
"Header_\rVector16________Vector32: value\r\n\r\n",
"Header__Vector16________Vector32\r: value\r\n\r\n",
"Header__Vector16________Vector32_\r: value\r\n\r\n",
"Header__Vector16________Vector32Header_\rVector16________Vector32: value\r\n\r\n",
"Header__Vector16________Vector32Header__Vector16________Vector32\r: value\r\n\r\n",
"Header__Vector16________Vector32Header__Vector16________Vector32_\r: value\r\n\r\n",
"Header 1: value1\r\nHeader-2: value2\r\n\r\n",
"Header 1 : value1\r\nHeader-2: value2\r\n\r\n",
"Header 1\t: value1\r\nHeader-2: value2\r\n\r\n",
"Header 1\r: value1\r\nHeader-2: value2\r\n\r\n",
"Header-1: value1\r\nHeader 2: value2\r\n\r\n",
"Header-1: value1\r\nHeader-2 : value2\r\n\r\n",
"Header-1: value1\r\nHeader-2\t: value2\r\n\r\n",
};
// Starting with whitespace
new[] { " Header: value\r\n\r\n", @"Invalid request header: ' Header: value\x0D\x0A'" },
new[] { "\tHeader: value\r\n\r\n", @"Invalid request header: '\x09Header: value\x0D\x0A'" },
new[] { " Header-1: value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: ' Header-1: value1\x0D\x0A'" },
new[] { "\tHeader-1: value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: '\x09Header-1: value1\x0D\x0A'" },
// Headers not ending in CRLF line
var headersNotEndingInCrLfLine = new[]
{
"Header-1: value1\r\nHeader-2: value2\r\n\r\r",
"Header-1: value1\r\nHeader-2: value2\r\n\r ",
"Header-1: value1\r\nHeader-2: value2\r\n\r \n",
};
// Whitespace in header name
new[] { "Header : value\r\n\r\n", @"Invalid request header: 'Header : value\x0D\x0A'" },
new[] { "Header\t: value\r\n\r\n", @"Invalid request header: 'Header\x09: value\x0D\x0A'" },
new[] { "Header\r: value\r\n\r\n", @"Invalid request header: 'Header\x0D: value\x0D\x0A'" },
new[] { "Header_\rVector16: value\r\n\r\n", @"Invalid request header: 'Header_\x0DVector16: value\x0D\x0A'" },
new[] { "Header__Vector16\r: value\r\n\r\n", @"Invalid request header: 'Header__Vector16\x0D: value\x0D\x0A'" },
new[] { "Header__Vector16_\r: value\r\n\r\n", @"Invalid request header: 'Header__Vector16_\x0D: value\x0D\x0A'" },
new[] { "Header_\rVector16________Vector32: value\r\n\r\n", @"Invalid request header: 'Header_\x0DVector16________Vector32: value\x0D\x0A'" },
new[] { "Header__Vector16________Vector32\r: value\r\n\r\n", @"Invalid request header: 'Header__Vector16________Vector32\x0D: value\x0D\x0A'" },
new[] { "Header__Vector16________Vector32_\r: value\r\n\r\n", @"Invalid request header: 'Header__Vector16________Vector32_\x0D: value\x0D\x0A'" },
new[] { "Header__Vector16________Vector32Header_\rVector16________Vector32: value\r\n\r\n", @"Invalid request header: 'Header__Vector16________Vector32Header_\x0DVector16________Vector32: value\x0D\x0A'" },
new[] { "Header__Vector16________Vector32Header__Vector16________Vector32\r: value\r\n\r\n", @"Invalid request header: 'Header__Vector16________Vector32Header__Vector16________Vector32\x0D: value\x0D\x0A'" },
new[] { "Header__Vector16________Vector32Header__Vector16________Vector32_\r: value\r\n\r\n", @"Invalid request header: 'Header__Vector16________Vector32Header__Vector16________Vector32_\x0D: value\x0D\x0A'" },
new[] { "Header 1: value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header 1: value1\x0D\x0A'" },
new[] { "Header 1 : value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header 1 : value1\x0D\x0A'" },
new[] { "Header 1\t: value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header 1\x09: value1\x0D\x0A'" },
new[] { "Header 1\r: value1\r\nHeader-2: value2\r\n\r\n", @"Invalid request header: 'Header 1\x0D: value1\x0D\x0A'" },
new[] { "Header-1: value1\r\nHeader 2: value2\r\n\r\n", @"Invalid request header: 'Header 2: value2\x0D\x0A'" },
new[] { "Header-1: value1\r\nHeader-2 : value2\r\n\r\n", @"Invalid request header: 'Header-2 : value2\x0D\x0A'" },
new[] { "Header-1: value1\r\nHeader-2\t: value2\r\n\r\n", @"Invalid request header: 'Header-2\x09: value2\x0D\x0A'" },
return new[]
{
Tuple.Create(headersWithLineFolding, "Whitespace is not allowed in header name."),
Tuple.Create(headersWithCRInValue, "Header value must not contain CR characters."),
Tuple.Create(headersWithMissingColon, "No ':' character found in header line."),
Tuple.Create(headersStartingWithWhitespace, "Whitespace is not allowed in header name."),
Tuple.Create(headersWithWithspaceInName, "Whitespace is not allowed in header name."),
Tuple.Create(headersNotEndingInCrLfLine, "Headers corrupted, invalid header sequence.")
}
.SelectMany(t => t.Item1.Select(headers => new[] { headers, t.Item2 }));
}
}
// Headers not ending in CRLF line
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r\r", @"Invalid request headers: missing final CRLF in header fields." },
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r ", @"Invalid request headers: missing final CRLF in header fields." },
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r \n", @"Invalid request headers: missing final CRLF in header fields." },
};
}
}

View File

@ -76,8 +76,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
{0}
{1}
private readonly static Tuple<ulong, ulong, HttpMethod, int, bool>[] _knownMethods =
new Tuple<ulong, ulong, HttpMethod, int, bool>[{2}];
private readonly static Tuple<ulong, ulong, HttpMethod, int>[] _knownMethods =
new Tuple<ulong, ulong, HttpMethod, int>[{2}];
private readonly static string[] _methodNames = new string[{3}];