Merge branch 'dev' into knownmethods-optimizations

This commit is contained in:
arespr 2017-03-08 19:09:00 +01:00
commit 0a45cbbb95
41 changed files with 1683 additions and 3657 deletions

View File

@ -32,5 +32,5 @@ branches:
before_install: before_install:
- if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi
script: script:
- ./build.sh --quiet verify - ./build.sh
- if test "$TRAVIS_OS_NAME" != "osx"; then bash test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker.sh; fi - if test "$TRAVIS_OS_NAME" != "osx"; then bash test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker.sh; fi

View File

@ -3,7 +3,6 @@
<packageSources> <packageSources>
<add key="AspNetCore" value="https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json" /> <add key="AspNetCore" value="https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json" />
<add key="AspNetCoreTools" value="https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json" /> <add key="AspNetCoreTools" value="https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json" />
<add key="appveyor-bdn" value="https://ci.appveyor.com/nuget/benchmarkdotnet" />
<add key="NuGet" value="https://api.nuget.org/v3/index.json" /> <add key="NuGet" value="https://api.nuget.org/v3/index.json" />
</packageSources> </packageSources>
</configuration> </configuration>

View File

@ -8,7 +8,7 @@ branches:
- feature/dev-si - feature/dev-si
- /^(.*\/)?ci-.*$/ - /^(.*\/)?ci-.*$/
build_script: build_script:
- build.cmd --quiet verify - ps: .\build.ps1
clone_depth: 1 clone_depth: 1
test: off test: off
deploy: off deploy: off

View File

@ -33,7 +33,7 @@ cd $PSScriptRoot
$repoFolder = $PSScriptRoot $repoFolder = $PSScriptRoot
$env:REPO_FOLDER = $repoFolder $env:REPO_FOLDER = $repoFolder
$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/feature/msbuild.zip" $koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip"
if ($env:KOREBUILD_ZIP) if ($env:KOREBUILD_ZIP)
{ {
$koreBuildZip=$env:KOREBUILD_ZIP $koreBuildZip=$env:KOREBUILD_ZIP
@ -64,4 +64,4 @@ if (!(Test-Path $buildFolder)) {
} }
} }
&"$buildFile" $args &"$buildFile" @args

View File

@ -2,7 +2,7 @@
repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd $repoFolder cd $repoFolder
koreBuildZip="https://github.com/aspnet/KoreBuild/archive/feature/msbuild.zip" koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip"
if [ ! -z $KOREBUILD_ZIP ]; then if [ ! -z $KOREBUILD_ZIP ]; then
koreBuildZip=$KOREBUILD_ZIP koreBuildZip=$KOREBUILD_ZIP
fi fi
@ -43,4 +43,4 @@ if test ! -d $buildFolder; then
fi fi
fi fi
$buildFile -r $repoFolder "$@" $buildFile -r $repoFolder "$@"

View File

@ -3,5 +3,7 @@
<NetStandardImplicitPackageVersion>1.6.1</NetStandardImplicitPackageVersion> <NetStandardImplicitPackageVersion>1.6.1</NetStandardImplicitPackageVersion>
<CoreFxVersion>4.3.0</CoreFxVersion> <CoreFxVersion>4.3.0</CoreFxVersion>
<LibUvVersion>1.9.1</LibUvVersion> <LibUvVersion>1.9.1</LibUvVersion>
<CoreFxLabsVersion>0.1.0-*</CoreFxLabsVersion>
<CoreFxLabsPipelinesVersion>0.1.0-*</CoreFxLabsPipelinesVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -25,9 +25,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel
case RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence: case RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence:
ex = new BadHttpRequestException("Headers corrupted, invalid header sequence.", StatusCodes.Status400BadRequest); ex = new BadHttpRequestException("Headers corrupted, invalid header sequence.", StatusCodes.Status400BadRequest);
break; break;
case RequestRejectionReason.HeaderLineMustNotStartWithWhitespace:
ex = new BadHttpRequestException("Header line must not start with whitespace.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.NoColonCharacterFoundInHeaderLine: case RequestRejectionReason.NoColonCharacterFoundInHeaderLine:
ex = new BadHttpRequestException("No ':' character found in header line.", StatusCodes.Status400BadRequest); ex = new BadHttpRequestException("No ':' character found in header line.", StatusCodes.Status400BadRequest);
break; break;
@ -40,6 +37,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel
case RequestRejectionReason.HeaderValueLineFoldingNotSupported: case RequestRejectionReason.HeaderValueLineFoldingNotSupported:
ex = new BadHttpRequestException("Header value line folding not supported.", StatusCodes.Status400BadRequest); ex = new BadHttpRequestException("Header value line folding not supported.", StatusCodes.Status400BadRequest);
break; break;
case RequestRejectionReason.InvalidRequestLine:
ex = new BadHttpRequestException("Invalid request line.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.MalformedRequestInvalidHeaders: case RequestRejectionReason.MalformedRequestInvalidHeaders:
ex = new BadHttpRequestException("Malformed request: invalid headers.", StatusCodes.Status400BadRequest); ex = new BadHttpRequestException("Malformed request: invalid headers.", StatusCodes.Status400BadRequest);
break; break;

View File

@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private CancellationTokenSource _abortedCts; private CancellationTokenSource _abortedCts;
private CancellationToken? _manuallySetRequestAbortToken; private CancellationToken? _manuallySetRequestAbortToken;
private RequestProcessingStatus _requestProcessingStatus; protected RequestProcessingStatus _requestProcessingStatus;
protected bool _keepAlive; protected bool _keepAlive;
protected bool _upgrade; protected bool _upgrade;
private bool _canHaveBody; private bool _canHaveBody;
@ -982,15 +982,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
Output.ProducingComplete(end); Output.ProducingComplete(end);
} }
public void ParseRequest(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined)
{
consumed = buffer.Start;
examined = buffer.End;
switch (_requestProcessingStatus)
{
case RequestProcessingStatus.RequestPending:
if (buffer.IsEmpty)
{
break;
}
ConnectionControl.ResetTimeout(_requestHeadersTimeoutMilliseconds, TimeoutAction.SendTimeoutResponse);
_requestProcessingStatus = RequestProcessingStatus.ParsingRequestLine;
goto case RequestProcessingStatus.ParsingRequestLine;
case RequestProcessingStatus.ParsingRequestLine:
if (TakeStartLine(buffer, out consumed, out examined))
{
buffer = buffer.Slice(consumed, buffer.End);
_requestProcessingStatus = RequestProcessingStatus.ParsingHeaders;
goto case RequestProcessingStatus.ParsingHeaders;
}
else
{
break;
}
case RequestProcessingStatus.ParsingHeaders:
if (TakeMessageHeaders(buffer, out consumed, out examined))
{
_requestProcessingStatus = RequestProcessingStatus.AppStarted;
}
break;
}
}
public bool TakeStartLine(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined) public bool TakeStartLine(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined)
{ {
if (_requestProcessingStatus == RequestProcessingStatus.RequestPending)
{
ConnectionControl.ResetTimeout(_requestHeadersTimeoutMilliseconds, TimeoutAction.SendTimeoutResponse);
}
_requestProcessingStatus = RequestProcessingStatus.RequestStarted;
var overLength = false; var overLength = false;
if (buffer.Length >= ServerOptions.Limits.MaxRequestLineSize) if (buffer.Length >= ServerOptions.Limits.MaxRequestLineSize)
{ {
@ -1039,7 +1070,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return true; return true;
} }
public bool TakeMessageHeaders(ReadableBuffer buffer, FrameRequestHeaders requestHeaders, out ReadCursor consumed, out ReadCursor examined) public bool TakeMessageHeaders(ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined)
{ {
// Make sure the buffer is limited // Make sure the buffer is limited
bool overLength = false; bool overLength = false;
@ -1085,7 +1116,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
if (!appCompleted) if (!appCompleted)
{ {
// Back out of header creation surface exeception in user code // Back out of header creation surface exeception in user code
_requestProcessingStatus = RequestProcessingStatus.RequestStarted; _requestProcessingStatus = RequestProcessingStatus.AppStarted;
throw ex; throw ex;
} }
else else
@ -1141,18 +1172,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public void RejectRequest(RequestRejectionReason reason) public void RejectRequest(RequestRejectionReason reason)
{ {
RejectRequest(BadHttpRequestException.GetException(reason)); throw BadHttpRequestException.GetException(reason);
} }
public void RejectRequest(RequestRejectionReason reason, string value) public void RejectRequest(RequestRejectionReason reason, string value)
{ {
RejectRequest(BadHttpRequestException.GetException(reason, value)); throw BadHttpRequestException.GetException(reason, value);
}
private void RejectRequest(BadHttpRequestException ex)
{
Log.ConnectionBadRequest(ConnectionId, ex);
throw ex;
} }
public void SetBadRequestState(RequestRejectionReason reason) public void SetBadRequestState(RequestRejectionReason reason)
@ -1162,6 +1187,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public void SetBadRequestState(BadHttpRequestException ex) public void SetBadRequestState(BadHttpRequestException ex)
{ {
Log.ConnectionBadRequest(ConnectionId, ex);
if (!HasResponseStarted) if (!HasResponseStarted)
{ {
SetErrorResponseHeaders(ex.StatusCode); SetErrorResponseHeaders(ex.StatusCode);
@ -1190,20 +1217,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
Log.ApplicationError(ConnectionId, ex); Log.ApplicationError(ConnectionId, ex);
} }
public enum RequestLineStatus
{
Empty,
Incomplete,
Done
}
private enum RequestProcessingStatus
{
RequestPending,
RequestStarted,
ResponseStarted
}
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)
{ {
// 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
@ -1215,7 +1228,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
if (needDecode) if (needDecode)
{ {
// Read raw target before mutating memory. // Read raw target before mutating memory.
rawTarget = target.GetAsciiString() ?? string.Empty; 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);
@ -1224,7 +1237,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
else else
{ {
// URI wasn't encoded, parse as ASCII // URI wasn't encoded, parse as ASCII
requestUrlPath = path.GetAsciiString() ?? string.Empty; requestUrlPath = path.GetAsciiStringNonNullCharacters();
if (query.Length == 0) if (query.Length == 0)
{ {
@ -1234,21 +1247,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
else else
{ {
rawTarget = target.GetAsciiString() ?? string.Empty; rawTarget = target.GetAsciiStringNonNullCharacters();
} }
} }
var normalizedTarget = PathNormalizer.RemoveDotSegments(requestUrlPath); var normalizedTarget = PathNormalizer.RemoveDotSegments(requestUrlPath);
if (method != HttpMethod.Custom) if (method != HttpMethod.Custom)
{ {
Method = HttpUtilities.MethodToString(method) ?? String.Empty; Method = HttpUtilities.MethodToString(method) ?? string.Empty;
} }
else else
{ {
Method = customMethod.GetAsciiString() ?? string.Empty; Method = customMethod.GetAsciiStringNonNullCharacters();
} }
QueryString = query.GetAsciiString() ?? string.Empty; QueryString = query.GetAsciiStringNonNullCharacters();
RawTarget = rawTarget; RawTarget = rawTarget;
HttpVersion = HttpUtilities.VersionToString(version); HttpVersion = HttpUtilities.VersionToString(version);
@ -1276,7 +1289,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
RejectRequest(RequestRejectionReason.TooManyHeaders); RejectRequest(RequestRejectionReason.TooManyHeaders);
} }
var valueString = value.GetAsciiString() ?? string.Empty; var valueString = value.GetAsciiStringNonNullCharacters();
FrameRequestHeaders.Append(name, valueString); FrameRequestHeaders.Append(name, valueString);
} }

View File

@ -3,7 +3,6 @@
using System; using System;
using System.IO; using System.IO;
using System.IO.Pipelines;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
@ -31,16 +30,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
/// </summary> /// </summary>
public override async Task RequestProcessingAsync() public override async Task RequestProcessingAsync()
{ {
var requestLineStatus = default(RequestLineStatus);
try try
{ {
while (!_requestProcessingStopping) while (!_requestProcessingStopping)
{ {
// If writer completes with an error Input.ReadAsyncDispatched would throw and
// this would not be reset to empty. But it's required by ECONNRESET check lower in the method.
requestLineStatus = RequestLineStatus.Empty;
ConnectionControl.SetTimeout(_keepAliveMilliseconds, TimeoutAction.CloseConnection); ConnectionControl.SetTimeout(_keepAliveMilliseconds, TimeoutAction.CloseConnection);
while (!_requestProcessingStopping) while (!_requestProcessingStopping)
@ -49,76 +42,44 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
var examined = result.Buffer.End; var examined = result.Buffer.End;
var consumed = result.Buffer.End; var consumed = result.Buffer.End;
InitializeHeaders();
try try
{ {
if (!result.Buffer.IsEmpty) ParseRequest(result.Buffer, out consumed, out examined);
{
requestLineStatus = TakeStartLine(result.Buffer, out consumed, out examined)
? RequestLineStatus.Done : RequestLineStatus.Incomplete;
}
else
{
requestLineStatus = RequestLineStatus.Empty;
}
} }
catch (InvalidOperationException) catch (InvalidOperationException)
{ {
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine); switch (_requestProcessingStatus)
{
case RequestProcessingStatus.ParsingRequestLine:
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine);
case RequestProcessingStatus.ParsingHeaders:
throw BadHttpRequestException.GetException(RequestRejectionReason.MalformedRequestInvalidHeaders);
}
throw;
} }
finally finally
{ {
Input.Reader.Advance(consumed, examined); Input.Reader.Advance(consumed, examined);
} }
if (requestLineStatus == RequestLineStatus.Done) if (_requestProcessingStatus == RequestProcessingStatus.AppStarted)
{ {
break; break;
} }
if (result.IsCompleted) if (result.IsCompleted)
{ {
if (requestLineStatus == RequestLineStatus.Empty) switch (_requestProcessingStatus)
{ {
return; case RequestProcessingStatus.RequestPending:
return;
case RequestProcessingStatus.ParsingRequestLine:
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine);
case RequestProcessingStatus.ParsingHeaders:
throw BadHttpRequestException.GetException(RequestRejectionReason.MalformedRequestInvalidHeaders);
} }
RejectRequest(RequestRejectionReason.InvalidRequestLine, requestLineStatus.ToString());
}
}
InitializeHeaders();
while (!_requestProcessingStopping)
{
var result = await Input.Reader.ReadAsync();
var examined = result.Buffer.End;
var consumed = result.Buffer.End;
bool headersDone;
try
{
headersDone = TakeMessageHeaders(result.Buffer, FrameRequestHeaders, out consumed,
out examined);
}
catch (InvalidOperationException)
{
throw BadHttpRequestException.GetException(RequestRejectionReason.MalformedRequestInvalidHeaders);
}
finally
{
Input.Reader.Advance(consumed, examined);
}
if (headersDone)
{
break;
}
if (result.IsCompleted)
{
RejectRequest(RequestRejectionReason.MalformedRequestInvalidHeaders);
} }
} }
@ -231,7 +192,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
catch (IOException ex) when (ex.InnerException is UvException) catch (IOException ex) when (ex.InnerException is UvException)
{ {
// Don't log ECONNRESET errors made between requests. Browsers like IE will reset connections regularly. // Don't log ECONNRESET errors made between requests. Browsers like IE will reset connections regularly.
if (requestLineStatus != RequestLineStatus.Empty || if (_requestProcessingStatus != RequestProcessingStatus.RequestPending ||
((UvException)ex.InnerException).StatusCode != Constants.ECONNRESET) ((UvException)ex.InnerException).StatusCode != Constants.ECONNRESET)
{ {
Log.RequestProcessingError(ConnectionId, ex); Log.RequestProcessingError(ConnectionId, ex);

View File

@ -2,7 +2,10 @@
// 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; using System;
using System.Buffers;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Numerics;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -31,38 +34,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
consumed = buffer.Start; consumed = buffer.Start;
examined = buffer.End; examined = buffer.End;
var start = buffer.Start;
ReadCursor end; ReadCursor end;
if (ReadCursorOperations.Seek(start, buffer.End, out end, ByteLF) == -1)
{
return false;
}
const int stackAllocLimit = 512;
// Move 1 byte past the \n
end = buffer.Move(end, 1);
var startLineBuffer = buffer.Slice(start, end);
Span<byte> span; Span<byte> span;
if (startLineBuffer.IsSingleSpan) // If the buffer is a single span then use it to find the LF
if (buffer.IsSingleSpan)
{ {
// No copies, directly use the one and only span var startLineSpan = buffer.First.Span;
span = startLineBuffer.First.Span; var lineIndex = startLineSpan.IndexOfVectorized(ByteLF);
}
else if (startLineBuffer.Length < stackAllocLimit) if (lineIndex == -1)
{ {
// Multiple buffers and < stackAllocLimit, copy into a stack buffer return false;
byte* stackBuffer = stackalloc byte[startLineBuffer.Length]; }
span = new Span<byte>(stackBuffer, startLineBuffer.Length);
startLineBuffer.CopyTo(span); end = buffer.Move(consumed, lineIndex + 1);
span = startLineSpan.Slice(0, lineIndex + 1);
} }
else else
{ {
// We're not a single span here but we can use pooled arrays to avoid allocations in the rare case var start = buffer.Start;
span = new Span<byte>(new byte[startLineBuffer.Length]); if (ReadCursorOperations.Seek(start, buffer.End, out end, ByteLF) == -1)
startLineBuffer.CopyTo(span); {
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; var pathStart = -1;
@ -71,37 +72,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
var pathEnd = -1; var pathEnd = -1;
var versionStart = -1; var versionStart = -1;
HttpVersion httpVersion = HttpVersion.Unknown; var httpVersion = HttpVersion.Unknown;
HttpMethod method = HttpMethod.Custom; HttpMethod method;
Span<byte> customMethod; Span<byte> customMethod;
var state = StartLineState.KnownMethod; var i = 0;
var length = span.Length;
var done = false;
int i;
fixed (byte* data = &span.DangerousGetPinnableReference()) fixed (byte* data = &span.DangerousGetPinnableReference())
{ {
var length = span.Length; switch (StartLineState.KnownMethod)
for (i = 0; i < length; i++)
{ {
var ch = data[i]; 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;
switch (state) goto case StartLineState.Path;
{ }
case StartLineState.KnownMethod: goto case StartLineState.UnknownMethod;
if (span.GetKnownMethod(out method, out var methodLength))
{
// Update the index, current char, state and jump directly
// to the next state
i += methodLength + 1;
ch = data[i];
state = StartLineState.Path;
goto case StartLineState.Path; case StartLineState.UnknownMethod:
} for (; i < length; i++)
{
var ch = data[i];
state = StartLineState.UnknownMethod;
goto case StartLineState.UnknownMethod;
case StartLineState.UnknownMethod:
if (ch == ByteSpace) if (ch == ByteSpace)
{ {
customMethod = span.Slice(0, i); customMethod = span.Slice(0, i);
@ -110,16 +107,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
RejectRequestLine(span); RejectRequestLine(span);
} }
// Consume space
i++;
state = StartLineState.Path; goto case StartLineState.Path;
} }
else if (!IsValidTokenChar((char)ch))
if (!IsValidTokenChar((char)ch))
{ {
RejectRequestLine(span); RejectRequestLine(span);
} }
}
break; break;
case StartLineState.Path: case StartLineState.Path:
for (; i < length; i++)
{
var ch = data[i];
if (ch == ByteSpace) if (ch == ByteSpace)
{ {
pathEnd = i; pathEnd = i;
@ -133,7 +137,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
// No query string found // No query string found
queryStart = queryEnd = i; queryStart = queryEnd = i;
state = StartLineState.KnownVersion; // Consume space
i++;
goto case StartLineState.KnownVersion;
} }
else if (ch == ByteQuestionMark) else if (ch == ByteQuestionMark)
{ {
@ -146,7 +153,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
queryStart = i; queryStart = i;
state = StartLineState.QueryString; goto case StartLineState.QueryString;
} }
else if (ch == BytePercentage) else if (ch == BytePercentage)
{ {
@ -160,35 +167,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
pathStart = i; pathStart = i;
} }
break; }
break;
case StartLineState.QueryString: case StartLineState.QueryString:
for (; i < length; i++)
{
var ch = data[i];
if (ch == ByteSpace) if (ch == ByteSpace)
{ {
queryEnd = i; queryEnd = i;
state = StartLineState.KnownVersion;
// Consume space
i++;
goto case StartLineState.KnownVersion;
} }
break; }
case StartLineState.KnownVersion: break;
// REVIEW: We don't *need* to slice here but it makes the API case StartLineState.KnownVersion:
// nicer, slicing should be free :) // REVIEW: We don't *need* to slice here but it makes the API
if (span.Slice(i).GetKnownVersion(out httpVersion, out var versionLenght)) // 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 // Update the index, current char, state and jump directly
i += versionLenght + 1; // to the next state
ch = data[i]; i += versionLenght + 1;
state = StartLineState.NewLine; goto case StartLineState.NewLine;
}
goto case StartLineState.NewLine; versionStart = i;
}
versionStart = i; goto case StartLineState.UnknownVersion;
state = StartLineState.UnknownVersion;
goto case StartLineState.UnknownVersion;
case StartLineState.UnknownVersion: case StartLineState.UnknownVersion:
for (; i < length; i++)
{
var ch = data[i];
if (ch == ByteCR) if (ch == ByteCR)
{ {
var versionSpan = span.Slice(versionStart, i - versionStart); var versionSpan = span.Slice(versionStart, i - versionStart);
@ -199,25 +213,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
else else
{ {
RejectRequest(RequestRejectionReason.UnrecognizedHTTPVersion, versionSpan.GetAsciiStringEscaped(32)); RejectRequest(RequestRejectionReason.UnrecognizedHTTPVersion,
versionSpan.GetAsciiStringEscaped(32));
} }
} }
break; }
case StartLineState.NewLine: break;
if (ch != ByteLF) case StartLineState.NewLine:
{ if (data[i] != ByteLF)
RejectRequestLine(span); {
} RejectRequestLine(span);
}
i++;
state = StartLineState.Complete; goto case StartLineState.Complete;
break; case StartLineState.Complete:
case StartLineState.Complete: done = true;
break; break;
}
} }
} }
if (state != StartLineState.Complete) if (!done)
{ {
RejectRequestLine(span); RejectRequestLine(span);
} }
@ -227,7 +243,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
var query = span.Slice(queryStart, queryEnd - queryStart); var query = span.Slice(queryStart, queryEnd - queryStart);
handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod); handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod);
consumed = buffer.Move(start, i);
consumed = end;
examined = consumed; examined = consumed;
return true; return true;
} }
@ -241,241 +258,256 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
var bufferEnd = buffer.End; var bufferEnd = buffer.End;
var reader = new ReadableBufferReader(buffer); var reader = new ReadableBufferReader(buffer);
while (true) var start = default(ReadableBufferReader);
var done = false;
try
{ {
var start = reader; while (!reader.End)
int ch1 = reader.Take();
var ch2 = reader.Take();
if (ch1 == -1)
{ {
return false; var span = reader.Span;
} var remaining = span.Length;
if (ch1 == ByteCR) fixed (byte* pBuffer = &span.DangerousGetPinnableReference())
{
// Check for final CRLF.
if (ch2 == -1)
{ {
return false; while (remaining > 0)
}
else if (ch2 == ByteLF)
{
consumed = reader.Cursor;
examined = consumed;
return true;
}
// Headers don't end in CRLF line.
RejectRequest(RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence);
}
else if (ch1 == ByteSpace || ch1 == ByteTab)
{
RejectRequest(RequestRejectionReason.HeaderLineMustNotStartWithWhitespace);
}
// Reset the reader since we're not at the end of headers
reader = start;
if (ReadCursorOperations.Seek(consumed, bufferEnd, out var lineEnd, ByteLF) == -1)
{
return false;
}
const int stackAllocLimit = 512;
if (lineEnd != bufferEnd)
{
lineEnd = buffer.Move(lineEnd, 1);
}
var headerBuffer = buffer.Slice(consumed, lineEnd);
Span<byte> span;
if (headerBuffer.IsSingleSpan)
{
// No copies, directly use the one and only span
span = headerBuffer.First.Span;
}
else if (headerBuffer.Length < stackAllocLimit)
{
// Multiple buffers and < stackAllocLimit, copy into a stack buffer
byte* stackBuffer = stackalloc byte[headerBuffer.Length];
span = new Span<byte>(stackBuffer, headerBuffer.Length);
headerBuffer.CopyTo(span);
}
else
{
// We're not a single span here but we can use pooled arrays to avoid allocations in the rare case
span = new Span<byte>(new byte[headerBuffer.Length]);
headerBuffer.CopyTo(span);
}
var state = HeaderState.Name;
var nameStart = 0;
var nameEnd = -1;
var valueStart = -1;
var valueEnd = -1;
var nameHasWhitespace = false;
var previouslyWhitespace = false;
var headerLineLength = span.Length;
fixed (byte* data = &span.DangerousGetPinnableReference())
{
for (var i = 0; i < headerLineLength; i++)
{
var ch = data[i];
switch (state)
{ {
case HeaderState.Name: var index = reader.Index;
if (ch == ByteColon) int ch1;
{ int ch2;
if (nameHasWhitespace)
{
RejectRequest(RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName);
}
state = HeaderState.Whitespace; // Fast path, we're still looking at the same span
nameEnd = i; if (remaining >= 2)
}
if (ch == ByteSpace || ch == ByteTab)
{
nameHasWhitespace = true;
}
break;
case HeaderState.Whitespace:
{ {
var whitespace = ch == ByteTab || ch == ByteSpace || ch == ByteCR; ch1 = pBuffer[index];
ch2 = pBuffer[index + 1];
if (!whitespace)
{
// Mark the first non whitespace char as the start of the
// header value and change the state to expect to the header value
valueStart = i;
state = HeaderState.ExpectValue;
}
// If we see a CR then jump to the next state directly
else if (ch == ByteCR)
{
state = HeaderState.ExpectValue;
goto case HeaderState.ExpectValue;
}
} }
break; else
case HeaderState.ExpectValue:
{ {
var whitespace = ch == ByteTab || ch == ByteSpace; // Store the reader before we look ahead 2 bytes (probably straddling
// spans)
start = reader;
if (whitespace) // Possibly split across spans
{ ch1 = reader.Take();
if (!previouslyWhitespace) ch2 = reader.Take();
{
// If we see a whitespace char then maybe it's end of the
// header value
valueEnd = i;
}
}
else if (ch == ByteCR)
{
// If we see a CR and we haven't ever seen whitespace then
// this is the end of the header value
if (valueEnd == -1)
{
valueEnd = i;
}
// We never saw a non whitespace character before the CR
if (valueStart == -1)
{
valueStart = valueEnd;
}
state = HeaderState.ExpectNewLine;
}
else
{
// If we find a non whitespace char that isn't CR then reset the end index
valueEnd = -1;
}
previouslyWhitespace = whitespace;
} }
break;
case HeaderState.ExpectNewLine: if (ch1 == ByteCR)
if (ch != ByteLF) {
// Check for final CRLF.
if (ch2 == -1)
{ {
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR); // Reset the reader so we don't consume anything
reader = start;
return false;
}
else if (ch2 == ByteLF)
{
// If we got 2 bytes from the span directly so skip ahead 2 so that
// the reader's state matches what we expect
if (index == reader.Index)
{
reader.Skip(2);
}
done = true;
return true;
} }
state = HeaderState.Complete; // Headers don't end in CRLF line.
break; RejectRequest(RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence);
default: }
break; else if(ch1 == ByteSpace || ch1 == ByteTab)
{
RejectRequest(RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName);
}
// We moved the reader so look ahead 2 bytes so reset both the reader
// and the index
if (index != reader.Index)
{
reader = start;
index = reader.Index;
}
var endIndex = new Span<byte>(pBuffer + index, remaining).IndexOfVectorized(ByteLF);
var length = 0;
if (endIndex != -1)
{
length = endIndex + 1;
var pHeader = pBuffer + index;
TakeSingleHeader(pHeader, length, handler);
}
else
{
var current = reader.Cursor;
// Split buffers
if (ReadCursorOperations.Seek(current, bufferEnd, out var lineEnd, ByteLF) == -1)
{
// Not there
return false;
}
// Make sure LF is included in lineEnd
lineEnd = buffer.Move(lineEnd, 1);
var headerSpan = buffer.Slice(current, lineEnd).ToSpan();
length = headerSpan.Length;
fixed (byte* pHeader = &headerSpan.DangerousGetPinnableReference())
{
TakeSingleHeader(pHeader, length, handler);
}
// We're going to the next span after this since we know we crossed spans here
// so mark the remaining as equal to the headerSpan so that we end up at 0
// on the next iteration
remaining = length;
}
// Skip the reader forward past the header line
reader.Skip(length);
remaining -= length;
} }
} }
} }
if (state == HeaderState.Name) return false;
{
RejectRequest(RequestRejectionReason.NoColonCharacterFoundInHeaderLine);
}
if (state == HeaderState.ExpectValue || state == HeaderState.Whitespace)
{
RejectRequest(RequestRejectionReason.MissingCRInHeaderLine);
}
if (state != HeaderState.Complete)
{
return false;
}
// Skip the reader forward past the header line
reader.Skip(headerLineLength);
// Before accepting the header line, we need to see at least one character
// > so we can make sure there's no space or tab
var next = reader.Peek();
// TODO: We don't need to reject the line here, we can use the state machine
// to store the fact that we're reading a header value
if (next == -1)
{
// If we can't see the next char then reject the entire line
return false;
}
if (next == ByteSpace || next == ByteTab)
{
// From https://tools.ietf.org/html/rfc7230#section-3.2.4:
//
// Historically, HTTP header field values could be extended over
// multiple lines by preceding each extra line with at least one space
// or horizontal tab (obs-fold). This specification deprecates such
// line folding except within the message/http media type
// (Section 8.3.1). A sender MUST NOT generate a message that includes
// line folding (i.e., that has any field-value that contains a match to
// the obs-fold rule) unless the message is intended for packaging
// within the message/http media type.
//
// A server that receives an obs-fold in a request message that is not
// within a message/http container MUST either reject the message by
// sending a 400 (Bad Request), preferably with a representation
// explaining that obsolete line folding is unacceptable, or replace
// each received obs-fold with one or more SP octets prior to
// interpreting the field value or forwarding the message downstream.
RejectRequest(RequestRejectionReason.HeaderValueLineFoldingNotSupported);
}
var nameBuffer = span.Slice(nameStart, nameEnd - nameStart);
var valueBuffer = span.Slice(valueStart, valueEnd - valueStart);
consumedBytes += headerLineLength;
handler.OnHeader(nameBuffer, valueBuffer);
consumed = reader.Cursor;
} }
finally
{
consumed = reader.Cursor;
consumedBytes = reader.ConsumedBytes;
if (done)
{
examined = consumed;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe int FindEndOfName(byte* headerLine, int length)
{
var index = 0;
var sawWhitespace = false;
for (; index < length; index++)
{
var ch = headerLine[index];
if (ch == ByteColon)
{
break;
}
if (ch == ByteTab || ch == ByteSpace || ch == ByteCR)
{
sawWhitespace = true;
}
}
if (index == length)
{
RejectRequest(RequestRejectionReason.NoColonCharacterFoundInHeaderLine);
}
if (sawWhitespace)
{
RejectRequest(RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName);
}
return index;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe void TakeSingleHeader<T>(byte* headerLine, int length, T handler) where T : IHttpHeadersHandler
{
// Skip CR, LF from end position
var valueEnd = length - 3;
var nameEnd = FindEndOfName(headerLine, length);
if (headerLine[valueEnd + 2] != ByteLF)
{
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
}
if (headerLine[valueEnd + 1] != ByteCR)
{
RejectRequest(RequestRejectionReason.MissingCRInHeaderLine);
}
// Skip colon from value start
var valueStart = nameEnd + 1;
// Ignore start whitespace
for(; valueStart < valueEnd; valueStart++)
{
var ch = headerLine[valueStart];
if (ch != ByteTab && ch != ByteSpace && ch != ByteCR)
{
break;
}
else if (ch == ByteCR)
{
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
}
}
// Check for CR in value
var i = valueStart + 1;
if (Contains(headerLine + i, valueEnd - i, ByteCR))
{
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
}
// Ignore end whitespace
for (; valueEnd >= valueStart; valueEnd--)
{
var ch = headerLine[valueEnd];
if (ch != ByteTab && ch != ByteSpace)
{
break;
}
}
var nameBuffer = new Span<byte>(headerLine, nameEnd);
var valueBuffer = new Span<byte>(headerLine + valueStart, valueEnd - valueStart + 1);
handler.OnHeader(nameBuffer, valueBuffer);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe bool Contains(byte* searchSpace, int length, byte value)
{
var i = 0;
if (Vector.IsHardwareAccelerated)
{
// Check Vector lengths
if (length - Vector<byte>.Count >= i)
{
var vValue = GetVector(value);
do
{
if (!Vector<byte>.Zero.Equals(Vector.Equals(vValue, Unsafe.Read<Vector<byte>>(searchSpace + i))))
{
goto found;
}
i += Vector<byte>.Count;
} while (length - Vector<byte>.Count >= i);
}
}
// Check remaining for CR
for (; i <= length; i++)
{
var ch = searchSpace[i];
if (ch == value)
{
goto found;
}
}
return false;
found:
return true;
} }
private static bool IsValidTokenChar(char c) private static bool IsValidTokenChar(char c)
@ -503,31 +535,40 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
c == '~'; c == '~';
} }
public void RejectRequest(RequestRejectionReason reason) public static void RejectRequest(RequestRejectionReason reason)
{ {
RejectRequest(BadHttpRequestException.GetException(reason)); throw BadHttpRequestException.GetException(reason);
} }
public void RejectRequest(RequestRejectionReason reason, string value) public static void RejectRequest(RequestRejectionReason reason, string value)
{ {
RejectRequest(BadHttpRequestException.GetException(reason, value)); throw BadHttpRequestException.GetException(reason, value);
}
private void RejectRequest(BadHttpRequestException ex)
{
throw ex;
} }
private void RejectRequestLine(Span<byte> span) private void RejectRequestLine(Span<byte> span)
{
throw GetRejectRequestLineException(span);
}
private BadHttpRequestException GetRejectRequestLineException(Span<byte> span)
{ {
const int MaxRequestLineError = 32; const int MaxRequestLineError = 32;
RejectRequest(RequestRejectionReason.InvalidRequestLine, return BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine,
Log.IsEnabled(LogLevel.Information) ? span.GetAsciiStringEscaped(MaxRequestLineError) : string.Empty); Log.IsEnabled(LogLevel.Information) ? span.GetAsciiStringEscaped(MaxRequestLineError) : string.Empty);
} }
public void Reset() public void Reset()
{ {
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector<byte> GetVector(byte vectorByte)
{
// Vector<byte> .ctor doesn't become an intrinsic due to detection issue
// However this does cause it to become an intrinsic (with additional multiply and reg->reg copy)
// https://github.com/dotnet/coreclr/issues/7459#issuecomment-253965670
return Vector.AsVectorByte(new Vector<uint>(vectorByte * 0x01010101u));
} }
private enum HeaderState private enum HeaderState
@ -551,4 +592,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
Complete Complete
} }
} }
} }

View File

@ -595,7 +595,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
try try
{ {
if (_context.TakeMessageHeaders(buffer, _requestHeaders, out consumed, out examined)) if (_context.TakeMessageHeaders(buffer, out consumed, out examined))
{ {
break; break;
} }

View File

@ -3,9 +3,8 @@
using System; using System;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Threading; using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
@ -47,8 +46,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
var result = await readingTask; var result = await readingTask;
await AwaitableThreadPool.Yield();
try try
{ {
if (!result.Buffer.IsEmpty) if (!result.Buffer.IsEmpty)
@ -70,13 +67,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
} }
private static async Task<ReadResult> ReadAsyncDispatchedAwaited(ReadableBufferAwaitable awaitable) [MethodImpl(MethodImplOptions.AggressiveInlining)]
{
var result = await awaitable;
await AwaitableThreadPool.Yield();
return result;
}
public static Span<byte> ToSpan(this ReadableBuffer buffer) public static Span<byte> ToSpan(this ReadableBuffer buffer)
{ {
if (buffer.IsSingleSpan) if (buffer.IsSingleSpan)
@ -86,15 +77,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return buffer.ToArray(); return buffer.ToArray();
} }
public static ArraySegment<byte> ToArraySegment(this ReadableBuffer buffer)
{
if (buffer.IsSingleSpan)
{
return buffer.First.GetArray();
}
return new ArraySegment<byte>(buffer.ToArray());
}
public static ArraySegment<byte> GetArray(this Memory<byte> memory) public static ArraySegment<byte> GetArray(this Memory<byte> memory)
{ {
ArraySegment<byte> result; ArraySegment<byte> result;

View File

@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public enum RequestProcessingStatus
{
RequestPending,
ParsingRequestLine,
ParsingHeaders,
AppStarted,
ResponseStarted
}
}

View File

@ -7,7 +7,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
UnrecognizedHTTPVersion, UnrecognizedHTTPVersion,
HeadersCorruptedInvalidHeaderSequence, HeadersCorruptedInvalidHeaderSequence,
HeaderLineMustNotStartWithWhitespace,
NoColonCharacterFoundInHeaderLine, NoColonCharacterFoundInHeaderLine,
WhitespaceIsNotAllowedInHeaderName, WhitespaceIsNotAllowedInHeaderName,
HeaderValueMustNotContainCR, HeaderValueMustNotContainCR,

View File

@ -1,312 +0,0 @@
// 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.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public class UrlPathDecoder
{
/// <summary>
/// Unescapes the string between given memory iterators in place.
/// </summary>
/// <param name="start">The iterator points to the beginning of the sequence.</param>
/// <param name="end">The iterator points to the byte behind the end of the sequence.</param>
/// <returns>The iterator points to the byte behind the end of the processed sequence.</returns>
public static MemoryPoolIterator Unescape(MemoryPoolIterator start, MemoryPoolIterator end)
{
// the slot to read the input
var reader = start;
// the slot to write the unescaped byte
var writer = reader;
while (true)
{
if (CompareIterators(ref reader, ref end))
{
return writer;
}
if (reader.Peek() == '%')
{
var decodeReader = reader;
// If decoding process succeeds, the writer iterator will be moved
// to the next write-ready location. On the other hand if the scanned
// percent-encodings cannot be interpreted as sequence of UTF-8 octets,
// these bytes should be copied to output as is.
// The decodeReader iterator is always moved to the first byte not yet
// be scanned after the process. A failed decoding means the chars
// between the reader and decodeReader can be copied to output untouched.
if (!DecodeCore(ref decodeReader, ref writer, end))
{
Copy(reader, decodeReader, ref writer);
}
reader = decodeReader;
}
else
{
writer.Put((byte)reader.Take());
}
}
}
/// <summary>
/// Unescape the percent-encodings
/// </summary>
/// <param name="reader">The iterator point to the first % char</param>
/// <param name="writer">The place to write to</param>
/// <param name="end">The end of the sequence</param>
private static bool DecodeCore(ref MemoryPoolIterator reader, ref MemoryPoolIterator writer, MemoryPoolIterator end)
{
// preserves the original head. if the percent-encodings cannot be interpreted as sequence of UTF-8 octets,
// bytes from this till the last scanned one will be copied to the memory pointed by writer.
var byte1 = UnescapePercentEncoding(ref reader, end);
if (byte1 == 0)
{
throw BadHttpRequestException.GetException(RequestRejectionReason.PathContainsNullCharacters);
}
if (byte1 == -1)
{
return false;
}
if (byte1 <= 0x7F)
{
// first byte < U+007f, it is a single byte ASCII
writer.Put((byte)byte1);
return true;
}
int byte2 = 0, byte3 = 0, byte4 = 0;
// anticipate more bytes
var currentDecodeBits = 0;
var byteCount = 1;
var expectValueMin = 0;
if ((byte1 & 0xE0) == 0xC0)
{
// 110x xxxx, expect one more byte
currentDecodeBits = byte1 & 0x1F;
byteCount = 2;
expectValueMin = 0x80;
}
else if ((byte1 & 0xF0) == 0xE0)
{
// 1110 xxxx, expect two more bytes
currentDecodeBits = byte1 & 0x0F;
byteCount = 3;
expectValueMin = 0x800;
}
else if ((byte1 & 0xF8) == 0xF0)
{
// 1111 0xxx, expect three more bytes
currentDecodeBits = byte1 & 0x07;
byteCount = 4;
expectValueMin = 0x10000;
}
else
{
// invalid first byte
return false;
}
var remainingBytes = byteCount - 1;
while (remainingBytes > 0)
{
// read following three chars
if (CompareIterators(ref reader, ref end))
{
return false;
}
var nextItr = reader;
var nextByte = UnescapePercentEncoding(ref nextItr, end);
if (nextByte == -1)
{
return false;
}
if ((nextByte & 0xC0) != 0x80)
{
// the follow up byte is not in form of 10xx xxxx
return false;
}
currentDecodeBits = (currentDecodeBits << 6) | (nextByte & 0x3F);
remainingBytes--;
if (remainingBytes == 1 && currentDecodeBits >= 0x360 && currentDecodeBits <= 0x37F)
{
// this is going to end up in the range of 0xD800-0xDFFF UTF-16 surrogates that
// are not allowed in UTF-8;
return false;
}
if (remainingBytes == 2 && currentDecodeBits >= 0x110)
{
// this is going to be out of the upper Unicode bound 0x10FFFF.
return false;
}
reader = nextItr;
if (byteCount - remainingBytes == 2)
{
byte2 = nextByte;
}
else if (byteCount - remainingBytes == 3)
{
byte3 = nextByte;
}
else if (byteCount - remainingBytes == 4)
{
byte4 = nextByte;
}
}
if (currentDecodeBits < expectValueMin)
{
// overlong encoding (e.g. using 2 bytes to encode something that only needed 1).
return false;
}
// all bytes are verified, write to the output
if (byteCount > 0)
{
writer.Put((byte)byte1);
}
if (byteCount > 1)
{
writer.Put((byte)byte2);
}
if (byteCount > 2)
{
writer.Put((byte)byte3);
}
if (byteCount > 3)
{
writer.Put((byte)byte4);
}
return true;
}
private static void Copy(MemoryPoolIterator head, MemoryPoolIterator tail, ref MemoryPoolIterator writer)
{
while (!CompareIterators(ref head, ref tail))
{
writer.Put((byte)head.Take());
}
}
/// <summary>
/// Read the percent-encoding and try unescape it.
///
/// The operation first peek at the character the <paramref name="scan"/>
/// iterator points at. If it is % the <paramref name="scan"/> is then
/// moved on to scan the following to characters. If the two following
/// characters are hexadecimal literals they will be unescaped and the
/// value will be returned.
///
/// If the first character is not % the <paramref name="scan"/> iterator
/// will be removed beyond the location of % and -1 will be returned.
///
/// If the following two characters can't be successfully unescaped the
/// <paramref name="scan"/> iterator will be move behind the % and -1
/// will be returned.
/// </summary>
/// <param name="scan">The value to read</param>
/// <param name="end">The end of the sequence</param>
/// <returns>The unescaped byte if success. Otherwise return -1.</returns>
private static int UnescapePercentEncoding(ref MemoryPoolIterator scan, MemoryPoolIterator end)
{
if (scan.Take() != '%')
{
return -1;
}
var probe = scan;
int value1 = ReadHex(ref probe, end);
if (value1 == -1)
{
return -1;
}
int value2 = ReadHex(ref probe, end);
if (value2 == -1)
{
return -1;
}
if (SkipUnescape(value1, value2))
{
return -1;
}
scan = probe;
return (value1 << 4) + value2;
}
/// <summary>
/// Read the next char and convert it into hexadecimal value.
///
/// The <paramref name="scan"/> iterator will be moved to the next
/// byte no matter no matter whether the operation successes.
/// </summary>
/// <param name="scan">The value to read</param>
/// <param name="end">The end of the sequence</param>
/// <returns>The hexadecimal value if successes, otherwise -1.</returns>
private static int ReadHex(ref MemoryPoolIterator scan, MemoryPoolIterator end)
{
if (CompareIterators(ref scan, ref end))
{
return -1;
}
var value = scan.Take();
var isHead = (((value >= '0') && (value <= '9')) ||
((value >= 'A') && (value <= 'F')) ||
((value >= 'a') && (value <= 'f')));
if (!isHead)
{
return -1;
}
if (value <= '9')
{
return value - '0';
}
else if (value <= 'F')
{
return (value - 'A') + 10;
}
else // a - f
{
return (value - 'a') + 10;
}
}
private static bool SkipUnescape(int value1, int value2)
{
// skip %2F
if (value1 == 2 && value2 == 15)
{
return true;
}
return false;
}
private static bool CompareIterators(ref MemoryPoolIterator lhs, ref MemoryPoolIterator rhs)
{
// uses ref parameter to save cost of copying
return (lhs.Block == rhs.Block) && (lhs.Index == rhs.Index);
}
}
}

View File

@ -1,39 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
{
internal static class AwaitableThreadPool
{
internal static Awaitable Yield()
{
return new Awaitable();
}
internal struct Awaitable : ICriticalNotifyCompletion
{
public void GetResult()
{
}
public Awaitable GetAwaiter() => this;
public bool IsCompleted => false;
public void OnCompleted(Action continuation)
{
Task.Run(continuation);
}
public void UnsafeOnCompleted(Action continuation)
{
OnCompleted(continuation);
}
}
}
}

View File

@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
/// <summary> /// <summary>
/// The IPEndPoint Kestrel will bind to if nothing else is specified. /// The IPEndPoint Kestrel will bind to if nothing else is specified.
/// </summary> /// </summary>
public static readonly IPEndPoint DefaultIPEndPoint = new IPEndPoint(IPAddress.Loopback, 5000); public static readonly string DefaultServerAddress = "http://localhost:5000";
/// <summary> /// <summary>
/// Prefix of host name used to specify Unix sockets in the configuration. /// Prefix of host name used to specify Unix sockets in the configuration.

View File

@ -124,6 +124,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
} }
} }
public unsafe static string GetAsciiStringNonNullCharacters(this Span<byte> span)
{
if (span.IsEmpty)
{
return string.Empty;
}
var asciiString = new string('\0', span.Length);
fixed (char* output = asciiString)
fixed (byte* buffer = &span.DangerousGetPinnableReference())
{
// This version if AsciiUtilities returns null if there are any null (0 byte) characters
// in the string
if (!AsciiUtilities.TryGetAsciiString(buffer, output, span.Length))
{
throw new InvalidOperationException();
}
}
return asciiString;
}
public static string GetAsciiStringEscaped(this Span<byte> span, int maxChars) public static string GetAsciiStringEscaped(this Span<byte> span, int maxChars)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();

View File

@ -14,15 +14,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
public struct MemoryPoolIterator public struct MemoryPoolIterator
{ {
private const int _maxULongByteLength = 20; private const int _maxULongByteLength = 20;
private const ulong _xorPowerOfTwoToHighByte = (0x07ul |
0x06ul << 8 |
0x05ul << 16 |
0x04ul << 24 |
0x03ul << 32 |
0x02ul << 40 |
0x01ul << 48 ) + 1;
private static readonly int _vectorSpan = Vector<byte>.Count;
[ThreadStatic] [ThreadStatic]
private static byte[] _numericBytesScratch; private static byte[] _numericBytesScratch;
@ -135,668 +126,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
} }
} while (true); } while (true);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Skip(int bytesToSkip)
{
var block = _block;
if (block == null && bytesToSkip > 0)
{
ThrowInvalidOperationException_SkipMoreThanAvailable();
}
// Always set wasLastBlock before checking .End to avoid race which may cause data loss
var wasLastBlock = block.Next == null;
var following = block.End - _index;
if (following >= bytesToSkip)
{
_index += bytesToSkip;
return;
}
if (wasLastBlock)
{
ThrowInvalidOperationException_SkipMoreThanAvailable();
}
SkipMultiBlock(bytesToSkip, following);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void SkipMultiBlock(int bytesToSkip, int following)
{
var block = _block;
do
{
bytesToSkip -= following;
block = block.Next;
var index = block.Start;
// Always set wasLastBlock before checking .End to avoid race which may cause data loss
var wasLastBlock = block.Next == null;
following = block.End - index;
if (following >= bytesToSkip)
{
_block = block;
_index = index + bytesToSkip;
return;
}
if (wasLastBlock)
{
ThrowInvalidOperationException_SkipMoreThanAvailable();
}
} while (true);
}
private static void ThrowInvalidOperationException_SkipMoreThanAvailable()
{
throw new InvalidOperationException("Attempted to skip more bytes than available.");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Peek()
{
var block = _block;
if (block == null)
{
return -1;
}
var index = _index;
// Always set wasLastBlock before checking .End to avoid race which may cause data loss
var wasLastBlock = block.Next == null;
if (index < block.End)
{
return block.Array[index];
}
return wasLastBlock ? -1 : PeekMultiBlock();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private int PeekMultiBlock()
{
var block = _block;
do
{
block = block.Next;
var index = block.Start;
// Always set wasLastBlock before checking .End to avoid race which may cause data loss
var wasLastBlock = block.Next == null;
if (index < block.End)
{
return block.Array[index];
}
if (wasLastBlock)
{
return -1;
}
} while (true);
}
// NOTE: Little-endian only!
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe bool TryPeekLong(out ulong longValue)
{
longValue = 0;
var block = _block;
if (block == null)
{
return false;
}
// Always set wasLastBlock before checking .End to avoid race which may cause data loss
var wasLastBlock = block.Next == null;
var blockBytes = block.End - _index;
if (blockBytes >= sizeof(ulong))
{
longValue = *(ulong*)(block.DataFixedPtr + _index);
return true;
}
return wasLastBlock ? false : TryPeekLongMultiBlock(ref longValue, blockBytes);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private unsafe bool TryPeekLongMultiBlock(ref ulong longValue, int blockBytes)
{
// Each block will be filled with at least 2048 bytes before the Next pointer is set, so a long
// will cross at most one block boundary assuming there are at least 8 bytes following the iterator.
var nextBytes = sizeof(ulong) - blockBytes;
var block = _block;
if (block.Next.End - block.Next.Start < nextBytes)
{
return false;
}
var nextLong = *(ulong*)(block.Next.DataFixedPtr + block.Next.Start);
if (blockBytes == 0)
{
// This case can not fall through to the else block since that would cause a 64-bit right shift
// on blockLong which is equivalent to no shift at all instead of shifting in all zeros.
// https://msdn.microsoft.com/en-us/library/xt18et0d.aspx
longValue = nextLong;
}
else
{
var blockLong = *(ulong*)(block.DataFixedPtr + block.End - sizeof(ulong));
// Ensure that the right shift has a ulong operand so a logical shift is performed.
longValue = (blockLong >> nextBytes * 8) | (nextLong << blockBytes * 8);
}
return true;
}
public int Seek(byte byte0)
{
int bytesScanned;
return Seek(byte0, out bytesScanned);
}
public unsafe int Seek(
byte byte0,
out int bytesScanned,
int limit = int.MaxValue)
{
bytesScanned = 0;
var block = _block;
if (block == null || limit <= 0)
{
return -1;
}
var index = _index;
var wasLastBlock = block.Next == null;
var following = block.End - index;
byte[] array;
var byte0Vector = GetVector(byte0);
while (true)
{
while (following == 0)
{
if (bytesScanned >= limit || wasLastBlock)
{
_block = block;
_index = index;
return -1;
}
block = block.Next;
index = block.Start;
wasLastBlock = block.Next == null;
following = block.End - index;
}
array = block.Array;
while (following > 0)
{
// Need unit tests to test Vector path
#if !DEBUG
// Check will be Jitted away https://github.com/dotnet/coreclr/issues/1079
if (Vector.IsHardwareAccelerated)
{
#endif
if (following >= _vectorSpan)
{
var byte0Equals = Vector.Equals(new Vector<byte>(array, index), byte0Vector);
if (byte0Equals.Equals(Vector<byte>.Zero))
{
if (bytesScanned + _vectorSpan >= limit)
{
_block = block;
// Ensure iterator is left at limit position
_index = index + (limit - bytesScanned);
bytesScanned = limit;
return -1;
}
bytesScanned += _vectorSpan;
following -= _vectorSpan;
index += _vectorSpan;
continue;
}
_block = block;
var firstEqualByteIndex = LocateFirstFoundByte(byte0Equals);
var vectorBytesScanned = firstEqualByteIndex + 1;
if (bytesScanned + vectorBytesScanned > limit)
{
// Ensure iterator is left at limit position
_index = index + (limit - bytesScanned);
bytesScanned = limit;
return -1;
}
_index = index + firstEqualByteIndex;
bytesScanned += vectorBytesScanned;
return byte0;
}
// Need unit tests to test Vector path
#if !DEBUG
}
#endif
var pCurrent = (block.DataFixedPtr + index);
var pEnd = pCurrent + Math.Min(following, limit - bytesScanned);
do
{
bytesScanned++;
if (*pCurrent == byte0)
{
_block = block;
_index = index;
return byte0;
}
pCurrent++;
index++;
} while (pCurrent < pEnd);
following = 0;
break;
}
}
}
public unsafe int Seek(
byte byte0,
ref MemoryPoolIterator limit)
{
var block = _block;
if (block == null)
{
return -1;
}
var index = _index;
var wasLastBlock = block.Next == null;
var following = block.End - index;
while (true)
{
while (following == 0)
{
if ((block == limit.Block && index > limit.Index) ||
wasLastBlock)
{
_block = block;
// Ensure iterator is left at limit position
_index = limit.Index;
return -1;
}
block = block.Next;
index = block.Start;
wasLastBlock = block.Next == null;
following = block.End - index;
}
var array = block.Array;
while (following > 0)
{
// Need unit tests to test Vector path
#if !DEBUG
// Check will be Jitted away https://github.com/dotnet/coreclr/issues/1079
if (Vector.IsHardwareAccelerated)
{
#endif
if (following >= _vectorSpan)
{
var byte0Equals = Vector.Equals(new Vector<byte>(array, index), GetVector(byte0));
if (byte0Equals.Equals(Vector<byte>.Zero))
{
if (block == limit.Block && index + _vectorSpan > limit.Index)
{
_block = block;
// Ensure iterator is left at limit position
_index = limit.Index;
return -1;
}
following -= _vectorSpan;
index += _vectorSpan;
continue;
}
_block = block;
var firstEqualByteIndex = LocateFirstFoundByte(byte0Equals);
if (_block == limit.Block && index + firstEqualByteIndex > limit.Index)
{
// Ensure iterator is left at limit position
_index = limit.Index;
return -1;
}
_index = index + firstEqualByteIndex;
return byte0;
}
// Need unit tests to test Vector path
#if !DEBUG
}
#endif
var pCurrent = (block.DataFixedPtr + index);
var pEnd = block == limit.Block ? block.DataFixedPtr + limit.Index + 1 : pCurrent + following;
do
{
if (*pCurrent == byte0)
{
_block = block;
_index = index;
return byte0;
}
pCurrent++;
index++;
} while (pCurrent < pEnd);
following = 0;
break;
}
}
}
public int Seek(byte byte0, byte byte1)
{
var limit = new MemoryPoolIterator();
return Seek(byte0, byte1, ref limit);
}
public unsafe int Seek(
byte byte0,
byte byte1,
ref MemoryPoolIterator limit)
{
var block = _block;
if (block == null)
{
return -1;
}
var index = _index;
var wasLastBlock = block.Next == null;
var following = block.End - index;
int byteIndex = int.MaxValue;
while (true)
{
while (following == 0)
{
if ((block == limit.Block && index > limit.Index) ||
wasLastBlock)
{
_block = block;
// Ensure iterator is left at limit position
_index = limit.Index;
return -1;
}
block = block.Next;
index = block.Start;
wasLastBlock = block.Next == null;
following = block.End - index;
}
var array = block.Array;
while (following > 0)
{
// Need unit tests to test Vector path
#if !DEBUG
// Check will be Jitted away https://github.com/dotnet/coreclr/issues/1079
if (Vector.IsHardwareAccelerated)
{
#endif
if (following >= _vectorSpan)
{
var data = new Vector<byte>(array, index);
var byteEquals = Vector.Equals(data, GetVector(byte0));
byteEquals = Vector.ConditionalSelect(byteEquals, byteEquals, Vector.Equals(data, GetVector(byte1)));
if (!byteEquals.Equals(Vector<byte>.Zero))
{
byteIndex = LocateFirstFoundByte(byteEquals);
}
if (byteIndex == int.MaxValue)
{
following -= _vectorSpan;
index += _vectorSpan;
if (block == limit.Block && index > limit.Index)
{
_block = block;
// Ensure iterator is left at limit position
_index = limit.Index;
return -1;
}
continue;
}
_block = block;
_index = index + byteIndex;
if (block == limit.Block && _index > limit.Index)
{
// Ensure iterator is left at limit position
_index = limit.Index;
return -1;
}
_index = index + byteIndex;
if (block == limit.Block && _index > limit.Index)
{
// Ensure iterator is left at limit position
_index = limit.Index;
return -1;
}
return block.Array[index + byteIndex];
}
// Need unit tests to test Vector path
#if !DEBUG
}
#endif
var pCurrent = (block.DataFixedPtr + index);
var pEnd = block == limit.Block ? block.DataFixedPtr + limit.Index + 1 : pCurrent + following;
do
{
if (*pCurrent == byte0)
{
_block = block;
_index = index;
return byte0;
}
if (*pCurrent == byte1)
{
_block = block;
_index = index;
return byte1;
}
pCurrent++;
index++;
} while (pCurrent != pEnd);
following = 0;
break;
}
}
}
public int Seek(byte byte0, byte byte1, byte byte2)
{
var limit = new MemoryPoolIterator();
return Seek(byte0, byte1, byte2, ref limit);
}
public unsafe int Seek(
byte byte0,
byte byte1,
byte byte2,
ref MemoryPoolIterator limit)
{
var block = _block;
if (block == null)
{
return -1;
}
var index = _index;
var wasLastBlock = block.Next == null;
var following = block.End - index;
int byteIndex = int.MaxValue;
while (true)
{
while (following == 0)
{
if ((block == limit.Block && index > limit.Index) ||
wasLastBlock)
{
_block = block;
// Ensure iterator is left at limit position
_index = limit.Index;
return -1;
}
block = block.Next;
index = block.Start;
wasLastBlock = block.Next == null;
following = block.End - index;
}
var array = block.Array;
while (following > 0)
{
// Need unit tests to test Vector path
#if !DEBUG
// Check will be Jitted away https://github.com/dotnet/coreclr/issues/1079
if (Vector.IsHardwareAccelerated)
{
#endif
if (following >= _vectorSpan)
{
var data = new Vector<byte>(array, index);
var byteEquals = Vector.Equals(data, GetVector(byte0));
byteEquals = Vector.ConditionalSelect(byteEquals, byteEquals, Vector.Equals(data, GetVector(byte1)));
byteEquals = Vector.ConditionalSelect(byteEquals, byteEquals, Vector.Equals(data, GetVector(byte2)));
if (!byteEquals.Equals(Vector<byte>.Zero))
{
byteIndex = LocateFirstFoundByte(byteEquals);
}
if (byteIndex == int.MaxValue)
{
following -= _vectorSpan;
index += _vectorSpan;
if (block == limit.Block && index > limit.Index)
{
_block = block;
// Ensure iterator is left at limit position
_index = limit.Index;
return -1;
}
continue;
}
_block = block;
_index = index + byteIndex;
if (block == limit.Block && _index > limit.Index)
{
// Ensure iterator is left at limit position
_index = limit.Index;
return -1;
}
return block.Array[index + byteIndex];
}
// Need unit tests to test Vector path
#if !DEBUG
}
#endif
var pCurrent = (block.DataFixedPtr + index);
var pEnd = block == limit.Block ? block.DataFixedPtr + limit.Index + 1 : pCurrent + following;
do
{
if (*pCurrent == byte0)
{
_block = block;
_index = index;
return byte0;
}
if (*pCurrent == byte1)
{
_block = block;
_index = index;
return byte1;
}
if (*pCurrent == byte2)
{
_block = block;
_index = index;
return byte2;
}
pCurrent++;
index++;
} while (pCurrent != pEnd);
following = 0;
break;
}
}
}
/// <summary>
/// Locate the first of the found bytes
/// </summary>
/// <param name="byteEquals"></param >
/// <returns>The first index of the result vector</returns>
// Force inlining (64 IL bytes, 91 bytes asm) Issue: https://github.com/dotnet/coreclr/issues/7386
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int LocateFirstFoundByte(Vector<byte> byteEquals)
{
var vector64 = Vector.AsVectorUInt64(byteEquals);
ulong longValue = 0;
var i = 0;
// Pattern unrolled by jit https://github.com/dotnet/coreclr/pull/8001
for (; i < Vector<ulong>.Count; i++)
{
longValue = vector64[i];
if (longValue == 0) continue;
break;
}
// Flag least significant power of two bit
var powerOfTwoFlag = (longValue ^ (longValue - 1));
// Shift all powers of two into the high byte and extract
var foundByteIndex = (int)((powerOfTwoFlag * _xorPowerOfTwoToHighByte) >> 57);
// Single LEA instruction with jitted const (using function result)
return i * 8 + foundByteIndex;
}
/// <summary> /// <summary>
/// Save the data at the current location then move to the next available space. /// Save the data at the current location then move to the next available space.
/// </summary> /// </summary>
@ -914,57 +244,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
} }
} }
public MemoryPoolIterator CopyTo(byte[] array, int offset, int count, out int actual)
{
var block = _block;
if (block == null)
{
actual = 0;
return this;
}
var index = _index;
var remaining = count;
while (true)
{
// Determine if we might attempt to copy data from block.Next before
// calculating "following" so we don't risk skipping data that could
// be added after block.End when we decide to copy from block.Next.
// block.End will always be advanced before block.Next is set.
var wasLastBlock = block.Next == null;
var following = block.End - index;
if (remaining <= following)
{
actual = count;
if (array != null)
{
Buffer.BlockCopy(block.Array, index, array, offset, remaining);
}
return new MemoryPoolIterator(block, index + remaining);
}
else if (wasLastBlock)
{
actual = count - remaining + following;
if (array != null)
{
Buffer.BlockCopy(block.Array, index, array, offset, following);
}
return new MemoryPoolIterator(block, index + following);
}
else
{
if (array != null)
{
Buffer.BlockCopy(block.Array, index, array, offset, following);
}
offset += following;
remaining -= following;
block = block.Next;
index = block.Start;
}
}
}
public void CopyFrom(byte[] data) public void CopyFrom(byte[] data)
{ {
CopyFrom(data, 0, data.Length); CopyFrom(data, 0, data.Length);
@ -1180,177 +459,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
CopyFrom(byteBuffer, position, _maxULongByteLength - position); CopyFrom(byteBuffer, position, _maxULongByteLength - position);
} }
public unsafe string GetAsciiString(ref MemoryPoolIterator end)
{
var block = _block;
if (block == null || end.IsDefault)
{
return null;
}
var length = GetLength(end);
if (length == 0)
{
return null;
}
var inputOffset = _index;
var asciiString = new string('\0', length);
fixed (char* outputStart = asciiString)
{
var output = outputStart;
var remaining = length;
var endBlock = end.Block;
var endIndex = end.Index;
var outputOffset = 0;
while (true)
{
int following = (block != endBlock ? block.End : endIndex) - inputOffset;
if (following > 0)
{
if (!AsciiUtilities.TryGetAsciiString(block.DataFixedPtr + inputOffset, output + outputOffset, following))
{
throw BadHttpRequestException.GetException(RequestRejectionReason.NonAsciiOrNullCharactersInInputString);
}
outputOffset += following;
remaining -= following;
}
if (remaining == 0)
{
break;
}
block = block.Next;
inputOffset = block.Start;
}
}
return asciiString;
}
public string GetUtf8String(ref MemoryPoolIterator end)
{
var block = _block;
if (block == null || end.IsDefault)
{
return default(string);
}
var index = _index;
if (end.Block == block)
{
return Encoding.UTF8.GetString(block.Array, index, end.Index - index);
}
var decoder = Encoding.UTF8.GetDecoder();
var length = GetLength(end);
var charLength = length;
// Worse case is 1 byte = 1 char
var chars = new char[charLength];
var charIndex = 0;
var remaining = length;
while (true)
{
int bytesUsed;
int charsUsed;
bool completed;
var following = block.End - index;
if (remaining <= following)
{
decoder.Convert(
block.Array,
index,
remaining,
chars,
charIndex,
charLength - charIndex,
true,
out bytesUsed,
out charsUsed,
out completed);
return new string(chars, 0, charIndex + charsUsed);
}
else if (block.Next == null)
{
decoder.Convert(
block.Array,
index,
following,
chars,
charIndex,
charLength - charIndex,
true,
out bytesUsed,
out charsUsed,
out completed);
return new string(chars, 0, charIndex + charsUsed);
}
else
{
decoder.Convert(
block.Array,
index,
following,
chars,
charIndex,
charLength - charIndex,
false,
out bytesUsed,
out charsUsed,
out completed);
charIndex += charsUsed;
remaining -= following;
block = block.Next;
index = block.Start;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ArraySegment<byte> GetArraySegment(MemoryPoolIterator end)
{
var block = _block;
if (block == null || end.IsDefault)
{
return default(ArraySegment<byte>);
}
var index = _index;
if (end.Block == block)
{
return new ArraySegment<byte>(block.Array, index, end.Index - index);
}
return GetArraySegmentMultiBlock(ref end);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private ArraySegment<byte> GetArraySegmentMultiBlock(ref MemoryPoolIterator end)
{
var length = GetLength(end);
var array = new byte[length];
CopyTo(array, 0, length, out length);
return new ArraySegment<byte>(array, 0, length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector<byte> GetVector(byte vectorByte)
{
// Vector<byte> .ctor doesn't become an intrinsic due to detection issue
// However this does cause it to become an intrinsic (with additional multiply and reg->reg copy)
// https://github.com/dotnet/coreclr/issues/7459#issuecomment-253965670
return Vector.AsVectorByte(new Vector<uint>(vectorByte * 0x01010101u));
}
} }
} }

View File

@ -142,8 +142,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel
} }
else if (!hasListenOptions && !hasServerAddresses) else if (!hasListenOptions && !hasServerAddresses)
{ {
_logger.LogDebug($"No listening endpoints were configured. Binding to {Constants.DefaultIPEndPoint} by default."); _logger.LogDebug($"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default.");
listenOptions.Add(new ListenOptions(Constants.DefaultIPEndPoint));
// "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint.
StartLocalhost(engine, ServerAddress.FromUrl(Constants.DefaultServerAddress));
// If StartLocalhost doesn't throw, there is at least one listener.
// The port cannot change for "localhost".
_serverAddresses.Addresses.Add(Constants.DefaultServerAddress);
return;
} }
else if (!hasListenOptions) else if (!hasListenOptions)
{ {

View File

@ -18,10 +18,9 @@
<PackageReference Include="Microsoft.Extensions.TaskCache.Sources" Version="1.2.0-*" PrivateAssets="All" /> <PackageReference Include="Microsoft.Extensions.TaskCache.Sources" Version="1.2.0-*" PrivateAssets="All" />
<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="0.1.0-*" /> <PackageReference Include="System.IO.Pipelines.Text.Primitives" Version="$(CoreFxLabsVersion)" />
<PackageReference Include="System.IO.Pipelines.Text.Primitives" Version="0.1.0-*" /> <PackageReference Include="System.Text.Encodings.Web.Utf8" Version="$(CoreFxLabsVersion)" />
<PackageReference Include="System.Text.Encodings.Web.Utf8" Version="0.1.0-*" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' "> <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">

View File

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Testing.xunit; using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -129,31 +130,45 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
} }
} }
[ConditionalFact(Skip = "Waiting on https://github.com/aspnet/Hosting/issues/917")] [ConditionalFact]
[PortSupportedCondition(5000)] [PortSupportedCondition(5000)]
public async Task DefaultsToPort5000() public Task DefaultsServerAddress_BindsToIPv4()
{
return RegisterDefaultServerAddresses_Success(new[] { "http://127.0.0.1:5000" });
}
[ConditionalFact]
[IPv6SupportedCondition]
[PortSupportedCondition(5000)]
public Task DefaultsServerAddress_BindsToIPv6()
{
return RegisterDefaultServerAddresses_Success(new[] { "http://127.0.0.1:5000", "http://[::1]:5000" });
}
private async Task RegisterDefaultServerAddresses_Success(IEnumerable<string> addresses)
{ {
var testLogger = new TestApplicationErrorLogger(); var testLogger = new TestApplicationErrorLogger();
var hostBuilder = new WebHostBuilder() var hostBuilder = new WebHostBuilder()
.UseKestrel() .UseKestrel()
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddSingleton<ILoggerFactory>(new KestrelTestLoggerFactory(testLogger)); services.AddSingleton<ILoggerFactory>(new KestrelTestLoggerFactory(testLogger));
}) })
.Configure(ConfigureEchoAddress); .Configure(ConfigureEchoAddress);
using (var host = hostBuilder.Build()) using (var host = hostBuilder.Build())
{ {
host.Start(); host.Start();
var debugLog = testLogger.Messages.Single(log => log.LogLevel == LogLevel.Debug); Assert.Equal(5000, host.GetPort());
Assert.True(debugLog.Message.Contains("default")); Assert.Single(testLogger.Messages, log => log.LogLevel == LogLevel.Debug &&
string.Equals($"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default.",
log.Message, StringComparison.Ordinal));
foreach (var testUrl in new[] { "http://127.0.0.1:5000", "http://localhost:5000" }) foreach (var address in addresses)
{ {
var response = await HttpClientSlim.GetStringAsync(testUrl); Assert.Equal(new Uri(address).ToString(), await HttpClientSlim.GetStringAsync(address));
Assert.Equal(new Uri(testUrl).ToString(), response);
} }
} }
} }

View File

@ -2,10 +2,12 @@
// 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.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
@ -14,106 +16,102 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{ {
[Theory] [Theory]
[MemberData(nameof(InvalidRequestLineData))] [MemberData(nameof(InvalidRequestLineData))]
public async Task TestInvalidRequestLines(string request) public Task TestInvalidRequestLines(string request, string expectedExceptionMessage)
{ {
using (var server = new TestServer(context => TaskCache.CompletedTask)) return TestBadRequest(
{ request,
using (var connection = server.CreateConnection()) "400 Bad Request",
{ expectedExceptionMessage);
await connection.SendAll(request);
await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue);
}
}
} }
[Theory] [Theory]
[MemberData(nameof(UnrecognizedHttpVersionData))] [MemberData(nameof(UnrecognizedHttpVersionData))]
public async Task TestInvalidRequestLinesWithUnrecognizedVersion(string httpVersion) public Task TestInvalidRequestLinesWithUnrecognizedVersion(string httpVersion)
{ {
using (var server = new TestServer(context => TaskCache.CompletedTask)) return TestBadRequest(
{ $"GET / {httpVersion}\r\n",
using (var connection = server.CreateConnection()) "505 HTTP Version Not Supported",
{ $"Unrecognized HTTP version: {httpVersion}");
await connection.SendAll($"GET / {httpVersion}\r\n");
await ReceiveBadRequestResponse(connection, "505 HTTP Version Not Supported", server.Context.DateHeaderValue);
}
}
} }
[Theory] [Theory]
[MemberData(nameof(InvalidRequestHeaderData))] [MemberData(nameof(InvalidRequestHeaderData))]
public async Task TestInvalidHeaders(string rawHeaders) public Task TestInvalidHeaders(string rawHeaders, string expectedExceptionMessage)
{ {
using (var server = new TestServer(context => TaskCache.CompletedTask)) return TestBadRequest(
{ $"GET / HTTP/1.1\r\n{rawHeaders}",
using (var connection = server.CreateConnection()) "400 Bad Request",
{ expectedExceptionMessage);
await connection.SendAll($"GET / HTTP/1.1\r\n{rawHeaders}");
await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue);
}
}
} }
[Fact] [Theory]
public async Task BadRequestWhenHeaderNameContainsNonASCIICharacters() [InlineData("Hea\0der: value", "Invalid characters in header name.")]
[InlineData("Header: va\0lue", "Malformed request: invalid headers.")]
[InlineData("Head\x80r: value", "Invalid characters in header name.")]
[InlineData("Header: valu\x80", "Malformed request: invalid headers.")]
public Task BadRequestWhenHeaderNameContainsNonASCIIOrNullCharacters(string header, string expectedExceptionMessage)
{ {
using (var server = new TestServer(context => { return Task.FromResult(0); })) return TestBadRequest(
{ $"GET / HTTP/1.1\r\n{header}\r\n\r\n",
using (var connection = server.CreateConnection()) "400 Bad Request",
{ expectedExceptionMessage);
await connection.SendAll(
"GET / HTTP/1.1",
"H\u00eb\u00e4d\u00ebr: value",
"",
"");
await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue);
}
}
} }
[Theory] [Theory]
[InlineData("POST")] [InlineData("POST")]
[InlineData("PUT")] [InlineData("PUT")]
public async Task BadRequestIfMethodRequiresLengthButNoContentLengthOrTransferEncodingInRequest(string method) public Task BadRequestIfMethodRequiresLengthButNoContentLengthOrTransferEncodingInRequest(string method)
{ {
using (var server = new TestServer(context => { return Task.FromResult(0); })) return TestBadRequest(
{ $"{method} / HTTP/1.1\r\n\r\n",
using (var connection = server.CreateConnection()) "411 Length Required",
{ $"{method} request contains no Content-Length or Transfer-Encoding header");
await connection.Send($"{method} / HTTP/1.1\r\n\r\n");
await ReceiveBadRequestResponse(connection, "411 Length Required", server.Context.DateHeaderValue);
}
}
} }
[Theory] [Theory]
[InlineData("POST")] [InlineData("POST")]
[InlineData("PUT")] [InlineData("PUT")]
public async Task BadRequestIfMethodRequiresLengthButNoContentLengthInHttp10Request(string method) public Task BadRequestIfMethodRequiresLengthButNoContentLengthInHttp10Request(string method)
{ {
using (var server = new TestServer(context => { return Task.FromResult(0); })) return TestBadRequest(
{ $"{method} / HTTP/1.0\r\n\r\n",
using (var connection = server.CreateConnection()) "400 Bad Request",
{ $"{method} request contains no Content-Length header");
await connection.Send($"{method} / HTTP/1.0\r\n\r\n");
await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue);
}
}
} }
[Theory] [Theory]
[InlineData("NaN")] [InlineData("NaN")]
[InlineData("-1")] [InlineData("-1")]
public async Task BadRequestIfContentLengthInvalid(string contentLength) public Task BadRequestIfContentLengthInvalid(string contentLength)
{ {
using (var server = new TestServer(context => { return Task.FromResult(0); })) return TestBadRequest(
$"POST / HTTP/1.1\r\nContent-Length: {contentLength}\r\n\r\n",
"400 Bad Request",
$"Invalid content length: {contentLength}");
}
private async Task TestBadRequest(string request, string expectedResponseStatusCode, string expectedExceptionMessage)
{
BadHttpRequestException loggedException = null;
var mockKestrelTrace = new Mock<IKestrelTrace>();
mockKestrelTrace
.Setup(trace => trace.IsEnabled(LogLevel.Information))
.Returns(true);
mockKestrelTrace
.Setup(trace => trace.ConnectionBadRequest(It.IsAny<string>(), It.IsAny<BadHttpRequestException>()))
.Callback<string, BadHttpRequestException>((connectionId, exception) => loggedException = exception);
using (var server = new TestServer(context => TaskCache.CompletedTask, new TestServiceContext { Log = mockKestrelTrace.Object }))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
await connection.SendAll($"GET / HTTP/1.1\r\nContent-Length: {contentLength}\r\n\r\n"); await connection.SendAll(request);
await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); await ReceiveBadRequestResponse(connection, expectedResponseStatusCode, server.Context.DateHeaderValue);
} }
} }
mockKestrelTrace.Verify(trace => trace.ConnectionBadRequest(It.IsAny<string>(), It.IsAny<BadHttpRequestException>()));
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)
@ -127,10 +125,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
""); "");
} }
public static IEnumerable<object> InvalidRequestLineData => HttpParsingData.InvalidRequestLineData.Select(data => new[] { data[0] }); public static TheoryData<string, string> InvalidRequestLineData
{
get
{
var data = new TheoryData<string, string>();
foreach (var requestLine in HttpParsingData.RequestLineInvalidData)
{
data.Add(requestLine, $"Invalid request line: {requestLine.Replace("\r", "<0x0D>").Replace("\n", "<0x0A>")}");
}
foreach (var requestLine in HttpParsingData.RequestLineWithEncodedNullCharInTargetData)
{
data.Add(requestLine, "Invalid request line.");
}
foreach (var requestLine in HttpParsingData.RequestLineWithNullCharInTargetData)
{
data.Add(requestLine, "Invalid request line.");
}
return data;
}
}
public static TheoryData<string> UnrecognizedHttpVersionData => HttpParsingData.UnrecognizedHttpVersionData; public static TheoryData<string> UnrecognizedHttpVersionData => HttpParsingData.UnrecognizedHttpVersionData;
public static IEnumerable<object[]> InvalidRequestHeaderData => HttpParsingData.InvalidRequestHeaderData.Select(data => new[] { data[0] }); public static IEnumerable<object[]> InvalidRequestHeaderData => HttpParsingData.RequestHeaderInvalidData;
} }
} }

View File

@ -2,13 +2,8 @@
set -e set -e
# Ensure that dotnet is added to the PATH.
# build.sh should always be run before this script to create the .build/ directory and restore packages.
scriptDir=$(dirname "${BASH_SOURCE[0]}") scriptDir=$(dirname "${BASH_SOURCE[0]}")
repoDir=$(cd $scriptDir/../../.. && pwd) ~/.dotnet/dotnet publish -f netcoreapp1.1 ./samples/SampleApp/
source ./.build/KoreBuild.sh -r $repoDir --quiet
dotnet publish -f netcoreapp1.1 ./samples/SampleApp/
cp -R ./samples/SampleApp/bin/Debug/netcoreapp1.1/publish/ $scriptDir cp -R ./samples/SampleApp/bin/Debug/netcoreapp1.1/publish/ $scriptDir
cp -R ~/.dotnet/ $scriptDir cp -R ~/.dotnet/ $scriptDir

View File

@ -0,0 +1,135 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO.Pipelines;
using System.Text;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Testing;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
[Config(typeof(CoreConfig))]
public class FrameParsingOverhead
{
private const int InnerLoopCount = 512;
public ReadableBuffer _buffer;
public Frame<object> _frame;
[Setup]
public void Setup()
{
var connectionContext = new MockConnection(new KestrelServerOptions());
connectionContext.ListenerContext.ServiceContext.HttpParserFactory = frame => NullParser.Instance;
_frame = new Frame<object>(application: null, context: connectionContext);
}
[Benchmark(Baseline = true, OperationsPerInvoke = InnerLoopCount)]
public void FrameOverheadTotal()
{
for (var i = 0; i < InnerLoopCount; i++)
{
ParseRequest();
}
}
[Benchmark(OperationsPerInvoke = InnerLoopCount)]
public void FrameOverheadRequestLine()
{
for (var i = 0; i < InnerLoopCount; i++)
{
ParseRequestLine();
}
}
[Benchmark(OperationsPerInvoke = InnerLoopCount)]
public void FrameOverheadRequestHeaders()
{
for (var i = 0; i < InnerLoopCount; i++)
{
ParseRequestHeaders();
}
}
private void ParseRequest()
{
_frame.Reset();
if (!_frame.TakeStartLine(_buffer, out var consumed, out var examined))
{
RequestParsing.ThrowInvalidRequestLine();
}
_frame.InitializeHeaders();
if (!_frame.TakeMessageHeaders(_buffer, out consumed, out examined))
{
RequestParsing.ThrowInvalidRequestHeaders();
}
}
private void ParseRequestLine()
{
_frame.Reset();
if (!_frame.TakeStartLine(_buffer, out var consumed, out var examined))
{
RequestParsing.ThrowInvalidRequestLine();
}
}
private void ParseRequestHeaders()
{
_frame.Reset();
_frame.InitializeHeaders();
if (!_frame.TakeMessageHeaders(_buffer, out var consumed, out var examined))
{
RequestParsing.ThrowInvalidRequestHeaders();
}
}
private class NullParser : IHttpParser
{
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");
private readonly byte[] _acceptHeaderName = Encoding.ASCII.GetBytes("Accept");
private readonly byte[] _acceptHeaderValue = Encoding.ASCII.GetBytes("text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7\r\n\r\n");
private readonly byte[] _connectionHeaderName = Encoding.ASCII.GetBytes("Connection");
private readonly byte[] _connectionHeaderValue = Encoding.ASCII.GetBytes("keep-alive");
public static readonly NullParser Instance = new NullParser();
public bool ParseHeaders<T>(T handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes) where T : IHttpHeadersHandler
{
handler.OnHeader(new Span<byte>(_hostHeaderName), new Span<byte>(_hostHeaderValue));
handler.OnHeader(new Span<byte>(_acceptHeaderName), new Span<byte>(_acceptHeaderValue));
handler.OnHeader(new Span<byte>(_connectionHeaderName), new Span<byte>(_connectionHeaderValue));
consumedBytes = 0;
consumed = buffer.Start;
examined = buffer.End;
return true;
}
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);
consumed = buffer.Start;
examined = buffer.End;
return true;
}
public void Reset()
{
}
}
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO.Pipelines;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
[Config(typeof(CoreConfig))]
public class KestrelHttpParser : IHttpRequestLineHandler, IHttpHeadersHandler
{
private readonly Internal.Http.KestrelHttpParser _parser = new Internal.Http.KestrelHttpParser(log: null);
private ReadableBuffer _buffer;
[Benchmark(Baseline = true, OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
public void PlaintextTechEmpower()
{
for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
{
InsertData(RequestParsingData.PlaintextTechEmpowerRequest);
ParseData();
}
}
[Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
public void LiveAspNet()
{
for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
{
InsertData(RequestParsingData.LiveaspnetRequest);
ParseData();
}
}
[Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
public void Unicode()
{
for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
{
InsertData(RequestParsingData.UnicodeRequest);
ParseData();
}
}
private void InsertData(byte[] data)
{
_buffer = ReadableBuffer.Create(data);
}
private void ParseData()
{
if (!_parser.ParseRequestLine(this, _buffer, out var consumed, out var examined))
{
RequestParsing.ThrowInvalidRequestHeaders();
}
_buffer = _buffer.Slice(consumed, _buffer.End);
if (!_parser.ParseHeaders(this, _buffer, out consumed, out examined, out var consumedBytes))
{
RequestParsing.ThrowInvalidRequestHeaders();
}
}
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod)
{
}
public void OnHeader(Span<byte> name, Span<byte> value)
{
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" /> <Import Project="..\..\build\common.props" />
@ -6,6 +6,7 @@
<TargetFramework>netcoreapp1.1</TargetFramework> <TargetFramework>netcoreapp1.1</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<ServerGarbageCollection>true</ServerGarbageCollection> <ServerGarbageCollection>true</ServerGarbageCollection>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
@ -22,7 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.10.2.*" /> <PackageReference Include="BenchmarkDotNet" Version="0.10.3" />
<PackageReference Include="Moq" Version="4.6.36-*" /> <PackageReference Include="Moq" Version="4.6.36-*" />
</ItemGroup> </ItemGroup>

View File

@ -40,10 +40,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{ {
BenchmarkRunner.Run<PipeThroughput>(); BenchmarkRunner.Run<PipeThroughput>();
} }
if (type.HasFlag(BenchmarkType.KnownStrings)) if (type.HasFlag(BenchmarkType.KnownStrings))
{ {
BenchmarkRunner.Run<KnownStrings>(); BenchmarkRunner.Run<KnownStrings>();
} }
if (type.HasFlag(BenchmarkType.KestrelHttpParser))
{
BenchmarkRunner.Run<KestrelHttpParser>();
}
if (type.HasFlag(BenchmarkType.FrameParsingOverhead))
{
BenchmarkRunner.Run<FrameParsingOverhead>();
}
} }
} }
@ -54,6 +62,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
Writing = 2, Writing = 2,
Throughput = 4, Throughput = 4,
KnownStrings = 8, KnownStrings = 8,
KestrelHttpParser = 16,
FrameParsingOverhead = 32,
// add new ones in powers of two - e.g. 2,4,8,16... // add new ones in powers of two - e.g. 2,4,8,16...
All = uint.MaxValue All = uint.MaxValue

View File

@ -3,128 +3,101 @@
using System; using System;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Linq;
using System.Text;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
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 MemoryPool = Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure.MemoryPool;
using RequestLineStatus = Microsoft.AspNetCore.Server.Kestrel.Internal.Http.Frame.RequestLineStatus;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{ {
[Config(typeof(CoreConfig))] [Config(typeof(CoreConfig))]
public class RequestParsing public class RequestParsing
{ {
private const int InnerLoopCount = 512; [Params(typeof(Internal.Http.KestrelHttpParser))]
private const int Pipelining = 16;
private const string plaintextRequest = "GET /plaintext HTTP/1.1\r\nHost: www.example.com\r\n\r\n";
private const string liveaspnetRequest = "GET https://live.asp.net/ HTTP/1.1\r\n" +
"Host: live.asp.net\r\n" +
"Connection: keep-alive\r\n" +
"Upgrade-Insecure-Requests: 1\r\n" +
"User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36\r\n" +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n" +
"DNT: 1\r\n" +
"Accept-Encoding: gzip, deflate, sdch, br\r\n" +
"Accept-Language: en-US,en;q=0.8\r\n" +
"Cookie: __unam=7a67379-1s65dc575c4-6d778abe-1; omniID=9519gfde_3347_4762_8762_df51458c8ec2\r\n\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" +
"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" +
"Accept-Encoding: gzip, deflate\r\n" +
"Host: stackoverflow.com\r\n" +
"Connection: Keep-Alive\r\n" +
"Cache-Control: max-age=0\r\n" +
"Upgrade-Insecure-Requests: 1\r\n" +
"DNT: 1\r\n" +
"Referer: http://stackoverflow.com/?tab=month\r\n" +
"Pragma: no-cache\r\n" +
"Cookie: prov=20629ccd-8b0f-e8ef-2935-cd26609fc0bc; __qca=P0-1591065732-1479167353442; _ga=GA1.2.1298898376.1479167354; _gat=1; sgt=id=9519gfde_3347_4762_8762_df51458c8ec2; acct=t=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric&s=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric\r\n\r\n";
private static readonly byte[] _plaintextPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(plaintextRequest, Pipelining)));
private static readonly byte[] _plaintextRequest = Encoding.ASCII.GetBytes(plaintextRequest);
private static readonly byte[] _liveaspnentPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(liveaspnetRequest, Pipelining)));
private static readonly byte[] _liveaspnentRequest = Encoding.ASCII.GetBytes(liveaspnetRequest);
private static readonly byte[] _unicodePipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(unicodeRequest, Pipelining)));
private static readonly byte[] _unicodeRequest = Encoding.ASCII.GetBytes(unicodeRequest);
[Params(typeof(KestrelHttpParser))]
public Type ParserType { get; set; } public Type ParserType { get; set; }
[Benchmark(Baseline = true, OperationsPerInvoke = InnerLoopCount)] public IPipe Pipe { get; set; }
public void ParsePlaintext()
public Frame<object> Frame { get; set; }
public PipeFactory PipelineFactory { get; set; }
[Setup]
public void Setup()
{ {
for (var i = 0; i < InnerLoopCount; i++) var connectionContext = new MockConnection(new KestrelServerOptions());
connectionContext.ListenerContext.ServiceContext.HttpParserFactory = frame => (IHttpParser)Activator.CreateInstance(ParserType, frame.ConnectionContext.ListenerContext.ServiceContext.Log);
Frame = new Frame<object>(application: null, context: connectionContext);
PipelineFactory = new PipeFactory();
Pipe = PipelineFactory.Create();
}
[Benchmark(Baseline = true, OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
public void PlaintextTechEmpower()
{
for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
{ {
InsertData(_plaintextRequest); InsertData(RequestParsingData.PlaintextTechEmpowerRequest);
ParseData(); ParseData();
} }
} }
[Benchmark(OperationsPerInvoke = InnerLoopCount * Pipelining)] [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount * RequestParsingData.Pipelining)]
public void ParsePipelinedPlaintext() public void PipelinedPlaintextTechEmpower()
{ {
for (var i = 0; i < InnerLoopCount; i++) for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
{ {
InsertData(_plaintextPipelinedRequests); InsertData(RequestParsingData.PlaintextTechEmpowerPipelinedRequests);
ParseData(); ParseData();
} }
} }
[Benchmark(OperationsPerInvoke = InnerLoopCount)] [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
public void ParseLiveAspNet() public void LiveAspNet()
{ {
for (var i = 0; i < InnerLoopCount; i++) for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
{ {
InsertData(_liveaspnentRequest); InsertData(RequestParsingData.LiveaspnetRequest);
ParseData(); ParseData();
} }
} }
[Benchmark(OperationsPerInvoke = InnerLoopCount * Pipelining)] [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount * RequestParsingData.Pipelining)]
public void ParsePipelinedLiveAspNet() public void PipelinedLiveAspNet()
{ {
for (var i = 0; i < InnerLoopCount; i++) for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
{ {
InsertData(_liveaspnentPipelinedRequests); InsertData(RequestParsingData.LiveaspnetPipelinedRequests);
ParseData(); ParseData();
} }
} }
[Benchmark(OperationsPerInvoke = InnerLoopCount)] [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
public void ParseUnicode() public void Unicode()
{ {
for (var i = 0; i < InnerLoopCount; i++) for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
{ {
InsertData(_unicodeRequest); InsertData(RequestParsingData.UnicodeRequest);
ParseData(); ParseData();
} }
} }
[Benchmark(OperationsPerInvoke = InnerLoopCount * Pipelining)] [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount * RequestParsingData.Pipelining)]
public void ParseUnicodePipelined() public void UnicodePipelined()
{ {
for (var i = 0; i < InnerLoopCount; i++) for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
{ {
InsertData(_unicodePipelinedRequests); InsertData(RequestParsingData.UnicodePipelinedRequests);
ParseData(); ParseData();
} }
} }
private void InsertData(byte[] bytes) private void InsertData(byte[] bytes)
{ {
var buffer = Pipe.Writer.Alloc(2048);
buffer.WriteFast(bytes);
// There should not be any backpressure and task completes immediately // There should not be any backpressure and task completes immediately
Pipe.Writer.WriteAsync(bytes).GetAwaiter().GetResult(); buffer.FlushAsync().GetAwaiter().GetResult();
} }
private void ParseData() private void ParseData()
@ -143,11 +116,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
Frame.Reset(); Frame.Reset();
ReadCursor consumed; if (!Frame.TakeStartLine(readableBuffer, out var consumed, out var examined))
ReadCursor examined;
if (!Frame.TakeStartLine(readableBuffer, out consumed, out examined))
{ {
ThrowInvalidStartLine(); ThrowInvalidRequestLine();
} }
Pipe.Reader.Advance(consumed, examined); Pipe.Reader.Advance(consumed, examined);
@ -156,40 +127,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
Frame.InitializeHeaders(); Frame.InitializeHeaders();
if (!Frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)Frame.RequestHeaders, out consumed, out examined)) if (!Frame.TakeMessageHeaders(readableBuffer, out consumed, out examined))
{ {
ThrowInvalidMessageHeaders(); ThrowInvalidRequestHeaders();
} }
Pipe.Reader.Advance(consumed, examined); Pipe.Reader.Advance(consumed, examined);
} }
while (true); while (true);
} }
private void ThrowInvalidStartLine() public static void ThrowInvalidRequestLine()
{ {
throw new InvalidOperationException("Invalid StartLine"); throw new InvalidOperationException("Invalid request line");
} }
private void ThrowInvalidMessageHeaders() public static void ThrowInvalidRequestHeaders()
{ {
throw new InvalidOperationException("Invalid MessageHeaders"); throw new InvalidOperationException("Invalid request headers");
} }
[Setup]
public void Setup()
{
var connectionContext = new MockConnection(new KestrelServerOptions());
connectionContext.ListenerContext.ServiceContext.HttpParserFactory = frame => (IHttpParser)Activator.CreateInstance(ParserType, frame.ConnectionContext.ListenerContext.ServiceContext.Log);
Frame = new Frame<object>(application: null, context: connectionContext);
PipelineFactory = new PipeFactory();
Pipe = PipelineFactory.Create();
}
public IPipe Pipe { get; set; }
public Frame<object> Frame { get; set; }
public PipeFactory PipelineFactory { get; set; }
} }
} }

View File

@ -0,0 +1,62 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
using System.Text;
using BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
[Config(typeof(CoreConfig))]
public class RequestParsingData
{
public const int InnerLoopCount = 512;
public const int Pipelining = 16;
private const string _plaintextTechEmpowerRequest =
"GET /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" +
"Host: live.asp.net\r\n" +
"Connection: keep-alive\r\n" +
"Upgrade-Insecure-Requests: 1\r\n" +
"User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36\r\n" +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n" +
"DNT: 1\r\n" +
"Accept-Encoding: gzip, deflate, sdch, br\r\n" +
"Accept-Language: en-US,en;q=0.8\r\n" +
"Cookie: __unam=7a67379-1s65dc575c4-6d778abe-1; omniID=9519gfde_3347_4762_8762_df51458c8ec2\r\n" +
"\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" +
"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" +
"Accept-Encoding: gzip, deflate\r\n" +
"Host: stackoverflow.com\r\n" +
"Connection: Keep-Alive\r\n" +
"Cache-Control: max-age=0\r\n" +
"Upgrade-Insecure-Requests: 1\r\n" +
"DNT: 1\r\n" +
"Referer: http://stackoverflow.com/?tab=month\r\n" +
"Pragma: no-cache\r\n" +
"Cookie: prov=20629ccd-8b0f-e8ef-2935-cd26609fc0bc; __qca=P0-1591065732-1479167353442; _ga=GA1.2.1298898376.1479167354; _gat=1; sgt=id=9519gfde_3347_4762_8762_df51458c8ec2; acct=t=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric&s=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric\r\n" +
"\r\n";
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[] LiveaspnetPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_liveaspnetRequest, Pipelining)));
public static readonly byte[] LiveaspnetRequest = Encoding.ASCII.GetBytes(_liveaspnetRequest);
public static readonly byte[] UnicodePipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_unicodeRequest, Pipelining)));
public static readonly byte[] UnicodeRequest = Encoding.ASCII.GetBytes(_unicodeRequest);
}
}

View File

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Text;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
internal static class WritableBufferExtensions
{
public static void WriteFast(this WritableBuffer buffer, ReadOnlySpan<byte> source)
{
if (buffer.Memory.IsEmpty)
{
buffer.Ensure();
}
// Fast path, try copying to the available memory directly
if (source.Length <= buffer.Memory.Length)
{
source.CopyToFast(buffer.Memory.Span);
buffer.Advance(source.Length);
return;
}
var remaining = source.Length;
var offset = 0;
while (remaining > 0)
{
var writable = Math.Min(remaining, buffer.Memory.Length);
buffer.Ensure(writable);
if (writable == 0)
{
continue;
}
source.Slice(offset, writable).CopyToFast(buffer.Memory.Span);
remaining -= writable;
offset += writable;
buffer.Advance(writable);
}
}
private unsafe static void CopyToFast(this ReadOnlySpan<byte> source, Span<byte> destination)
{
if (destination.Length < source.Length)
{
throw new InvalidOperationException();
}
// Assume it fits
fixed (byte* pSource = &source.DangerousGetPinnableReference())
fixed (byte* pDest = &destination.DangerousGetPinnableReference())
{
Buffer.MemoryCopy(pSource, pDest, destination.Length, source.Length);
}
}
}
}

View File

@ -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 BenchmarkDotNet.Configs; using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Engines; using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Validators; using BenchmarkDotNet.Validators;
@ -13,6 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
public CoreConfig() public CoreConfig()
{ {
Add(JitOptimizationsValidator.FailOnError); Add(JitOptimizationsValidator.FailOnError);
Add(MemoryDiagnoser.Default);
Add(new RpsColumn()); Add(new RpsColumn());
Add(Job.Default Add(Job.Default

View File

@ -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 System;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
@ -14,27 +15,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
private void FullAsciiRangeSupported() private void FullAsciiRangeSupported()
{ {
var byteRange = Enumerable.Range(1, 127).Select(x => (byte)x).ToArray(); var byteRange = Enumerable.Range(1, 127).Select(x => (byte)x).ToArray();
using (var pool = new MemoryPool()) var s = new Span<byte>(byteRange).GetAsciiStringNonNullCharacters();
Assert.Equal(s.Length, byteRange.Length);
for (var i = 1; i < byteRange.Length; i++)
{ {
var mem = pool.Lease(); var sb = (byte)s[i];
mem.GetIterator().CopyFrom(byteRange); var b = byteRange[i];
var begin = mem.GetIterator(); Assert.Equal(sb, b);
var end = GetIterator(begin, byteRange.Length);
var s = begin.GetAsciiString(ref end);
Assert.Equal(s.Length, byteRange.Length);
for (var i = 1; i < byteRange.Length; i++)
{
var sb = (byte)s[i];
var b = byteRange[i];
Assert.Equal(sb, b);
}
pool.Return(mem);
} }
} }
@ -49,123 +39,29 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
var byteRange = Enumerable.Range(1, length).Select(x => (byte)x).ToArray(); var byteRange = Enumerable.Range(1, length).Select(x => (byte)x).ToArray();
byteRange[position] = b; byteRange[position] = b;
using (var pool = new MemoryPool()) Assert.Throws<InvalidOperationException>(() => new Span<byte>(byteRange).GetAsciiStringNonNullCharacters());
{
var mem = pool.Lease();
mem.GetIterator().CopyFrom(byteRange);
var begin = mem.GetIterator();
var end = GetIterator(begin, byteRange.Length);
Assert.Throws<BadHttpRequestException>(() => begin.GetAsciiString(ref end));
pool.Return(mem);
}
} }
} }
} }
[Fact]
private void MultiBlockProducesCorrectResults()
{
var byteRange = Enumerable.Range(0, 512 + 64).Select(x => (byte)((x & 0x7f) | 0x01)).ToArray();
var expectedByteRange = byteRange
.Concat(byteRange)
.Concat(byteRange)
.Concat(byteRange)
.ToArray();
using (var pool = new MemoryPool())
{
var mem0 = pool.Lease();
var mem1 = pool.Lease();
var mem2 = pool.Lease();
var mem3 = pool.Lease();
mem0.GetIterator().CopyFrom(byteRange);
mem1.GetIterator().CopyFrom(byteRange);
mem2.GetIterator().CopyFrom(byteRange);
mem3.GetIterator().CopyFrom(byteRange);
mem0.Next = mem1;
mem1.Next = mem2;
mem2.Next = mem3;
var begin = mem0.GetIterator();
var end = GetIterator(begin, expectedByteRange.Length);
var s = begin.GetAsciiString(ref end);
Assert.Equal(s.Length, expectedByteRange.Length);
for (var i = 0; i < expectedByteRange.Length; i++)
{
var sb = (byte)((s[i] & 0x7f) | 0x01);
var b = expectedByteRange[i];
Assert.Equal(sb, b);
}
pool.Return(mem0);
pool.Return(mem1);
pool.Return(mem2);
pool.Return(mem3);
}
}
[Fact] [Fact]
private void LargeAllocationProducesCorrectResults() private void LargeAllocationProducesCorrectResults()
{ {
var byteRange = Enumerable.Range(0, 16384 + 64).Select(x => (byte)((x & 0x7f) | 0x01)).ToArray(); var byteRange = Enumerable.Range(0, 16384 + 64).Select(x => (byte)((x & 0x7f) | 0x01)).ToArray();
var expectedByteRange = byteRange.Concat(byteRange).ToArray(); var expectedByteRange = byteRange.Concat(byteRange).ToArray();
using (var pool = new MemoryPool())
var s = new Span<byte>(expectedByteRange).GetAsciiStringNonNullCharacters();
Assert.Equal(expectedByteRange.Length, s.Length);
for (var i = 0; i < expectedByteRange.Length; i++)
{ {
var mem0 = pool.Lease(); var sb = (byte)((s[i] & 0x7f) | 0x01);
var mem1 = pool.Lease(); var b = expectedByteRange[i];
mem0.GetIterator().CopyFrom(byteRange);
mem1.GetIterator().CopyFrom(byteRange);
var lastBlock = mem0; Assert.Equal(sb, b);
while (lastBlock.Next != null)
{
lastBlock = lastBlock.Next;
}
lastBlock.Next = mem1;
var begin = mem0.GetIterator();
var end = GetIterator(begin, expectedByteRange.Length);
var s = begin.GetAsciiString(ref end);
Assert.Equal(expectedByteRange.Length, s.Length);
for (var i = 0; i < expectedByteRange.Length; i++)
{
var sb = (byte)((s[i] & 0x7f) | 0x01);
var b = expectedByteRange[i];
Assert.Equal(sb, b);
}
var block = mem0;
while (block != null)
{
var returnBlock = block;
block = block.Next;
pool.Return(returnBlock);
}
} }
} }
private MemoryPoolIterator GetIterator(MemoryPoolIterator begin, int displacement)
{
var result = begin;
for (int i = 0; i < displacement; ++i)
{
result.Take();
}
return result;
}
} }
} }

View File

@ -304,14 +304,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Fact] [Fact]
public void AppendThrowsWhenHeaderValueContainsNonASCIICharacters() public void AppendThrowsWhenHeaderNameContainsNonASCIICharacters()
{ {
var headers = new FrameRequestHeaders(); var headers = new FrameRequestHeaders();
const string key = "\u00141ód\017c"; const string key = "\u00141ód\017c";
var encoding = Encoding.GetEncoding("iso-8859-1"); var encoding = Encoding.GetEncoding("iso-8859-1");
var exception = Assert.Throws<BadHttpRequestException>( var exception = Assert.Throws<BadHttpRequestException>(
() => headers.Append(encoding.GetBytes(key), key)); () => headers.Append(encoding.GetBytes(key), "value"));
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
} }
} }

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
public class FrameTests : IDisposable public class FrameTests : IDisposable
{ {
private readonly IPipe _socketInput; private readonly IPipe _input;
private readonly TestFrame<object> _frame; private readonly TestFrame<object> _frame;
private readonly ServiceContext _serviceContext; private readonly ServiceContext _serviceContext;
private readonly ConnectionContext _connectionContext; private readonly ConnectionContext _connectionContext;
@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
var trace = new KestrelTrace(new TestKestrelTrace()); var trace = new KestrelTrace(new TestKestrelTrace());
_pipelineFactory = new PipeFactory(); _pipelineFactory = new PipeFactory();
_socketInput = _pipelineFactory.Create(); _input = _pipelineFactory.Create();
_serviceContext = new ServiceContext _serviceContext = new ServiceContext
{ {
@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}; };
_connectionContext = new ConnectionContext(listenerContext) _connectionContext = new ConnectionContext(listenerContext)
{ {
Input = _socketInput, Input = _input,
Output = new MockSocketOutput(), Output = new MockSocketOutput(),
ConnectionControl = Mock.Of<IConnectionControl>() ConnectionControl = Mock.Of<IConnectionControl>()
}; };
@ -76,130 +76,27 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
public void Dispose() public void Dispose()
{ {
_socketInput.Reader.Complete(); _input.Reader.Complete();
_socketInput.Writer.Complete(); _input.Writer.Complete();
_pipelineFactory.Dispose(); _pipelineFactory.Dispose();
} }
[Fact]
public async Task CanReadHeaderValueWithoutLeadingWhitespace()
{
_frame.InitializeHeaders();
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes("Header:value\r\n\r\n"));
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer;
var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined);
_socketInput.Reader.Advance(_consumed, _examined);
Assert.True(success);
Assert.Equal(1, _frame.RequestHeaders.Count);
Assert.Equal("value", _frame.RequestHeaders["Header"]);
Assert.Equal(readableBuffer.End, _consumed);
}
[Theory]
[InlineData("Header: value\r\n\r\n")]
[InlineData("Header: value\r\n\r\n")]
[InlineData("Header:\tvalue\r\n\r\n")]
[InlineData("Header: \tvalue\r\n\r\n")]
[InlineData("Header:\t value\r\n\r\n")]
[InlineData("Header:\t\tvalue\r\n\r\n")]
[InlineData("Header:\t\t value\r\n\r\n")]
[InlineData("Header: \t\tvalue\r\n\r\n")]
[InlineData("Header: \t\t value\r\n\r\n")]
[InlineData("Header: \t \t value\r\n\r\n")]
public async Task LeadingWhitespaceIsNotIncludedInHeaderValue(string rawHeaders)
{
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders));
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer;
var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined);
_socketInput.Reader.Advance(_consumed, _examined);
Assert.True(success);
Assert.Equal(1, _frame.RequestHeaders.Count);
Assert.Equal("value", _frame.RequestHeaders["Header"]);
Assert.Equal(readableBuffer.End, _consumed);
}
[Theory]
[InlineData("Header: value \r\n\r\n")]
[InlineData("Header: value\t\r\n\r\n")]
[InlineData("Header: value \t\r\n\r\n")]
[InlineData("Header: value\t \r\n\r\n")]
[InlineData("Header: value\t\t\r\n\r\n")]
[InlineData("Header: value\t\t \r\n\r\n")]
[InlineData("Header: value \t\t\r\n\r\n")]
[InlineData("Header: value \t\t \r\n\r\n")]
[InlineData("Header: value \t \t \r\n\r\n")]
public async Task TrailingWhitespaceIsNotIncludedInHeaderValue(string rawHeaders)
{
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders));
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer;
var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined);
_socketInput.Reader.Advance(_consumed, _examined);
Assert.True(success);
Assert.Equal(1, _frame.RequestHeaders.Count);
Assert.Equal("value", _frame.RequestHeaders["Header"]);
Assert.Equal(readableBuffer.End, _consumed);
}
[Theory]
[InlineData("Header: one two three\r\n\r\n", "one two three")]
[InlineData("Header: one two three\r\n\r\n", "one two three")]
[InlineData("Header: one\ttwo\tthree\r\n\r\n", "one\ttwo\tthree")]
[InlineData("Header: one two\tthree\r\n\r\n", "one two\tthree")]
[InlineData("Header: one\ttwo three\r\n\r\n", "one\ttwo three")]
[InlineData("Header: one \ttwo \tthree\r\n\r\n", "one \ttwo \tthree")]
[InlineData("Header: one\t two\t three\r\n\r\n", "one\t two\t three")]
[InlineData("Header: one \ttwo\t three\r\n\r\n", "one \ttwo\t three")]
public async Task WhitespaceWithinHeaderValueIsPreserved(string rawHeaders, string expectedValue)
{
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders));
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer;
var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined);
_socketInput.Reader.Advance(_consumed, _examined);
Assert.True(success);
Assert.Equal(1, _frame.RequestHeaders.Count);
Assert.Equal(expectedValue, _frame.RequestHeaders["Header"]);
Assert.Equal(readableBuffer.End, _consumed);
}
[Theory]
[MemberData(nameof(InvalidRequestHeaderData))]
public async Task TakeMessageHeadersThrowsOnInvalidRequestHeaders(string rawHeaders, string expectedExceptionMessage)
{
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders));
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer;
var exception = Assert.Throws<BadHttpRequestException>(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined));
_socketInput.Reader.Advance(_consumed, _examined);
Assert.Equal(expectedExceptionMessage, exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
}
[Fact] [Fact]
public async Task TakeMessageHeadersThrowsOnHeaderValueWithLineFolding_CharacterNotAvailableOnFirstAttempt() public async Task TakeMessageHeadersThrowsOnHeaderValueWithLineFolding_CharacterNotAvailableOnFirstAttempt()
{ {
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes("Header-1: value1\r\n")); await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes("Header-1: value1\r\n"));
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
Assert.False(_frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined)); Assert.False(_frame.TakeMessageHeaders(readableBuffer, out _consumed, out _examined));
_socketInput.Reader.Advance(_consumed, _examined); _input.Reader.Advance(_consumed, _examined);
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(" ")); await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes(" "));
readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
var exception = Assert.Throws<BadHttpRequestException>(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined)); var exception = Assert.Throws<BadHttpRequestException>(() => _frame.TakeMessageHeaders(readableBuffer, out _consumed, out _examined));
_socketInput.Reader.Advance(_consumed, _examined); _input.Reader.Advance(_consumed, _examined);
Assert.Equal("Header value line folding not supported.", exception.Message); Assert.Equal("Whitespace is not allowed in header name.", exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
} }
@ -210,11 +107,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
_serviceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize = headerLine.Length - 1; _serviceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize = headerLine.Length - 1;
_frame.Reset(); _frame.Reset();
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine}\r\n")); await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine}\r\n"));
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
var exception = Assert.Throws<BadHttpRequestException>(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined)); var exception = Assert.Throws<BadHttpRequestException>(() => _frame.TakeMessageHeaders(readableBuffer, out _consumed, out _examined));
_socketInput.Reader.Advance(_consumed, _examined); _input.Reader.Advance(_consumed, _examined);
Assert.Equal("Request headers too long.", exception.Message); Assert.Equal("Request headers too long.", exception.Message);
Assert.Equal(StatusCodes.Status431RequestHeaderFieldsTooLarge, exception.StatusCode); Assert.Equal(StatusCodes.Status431RequestHeaderFieldsTooLarge, exception.StatusCode);
@ -226,36 +123,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
const string headerLines = "Header-1: value1\r\nHeader-2: value2\r\n"; const string headerLines = "Header-1: value1\r\nHeader-2: value2\r\n";
_serviceContext.ServerOptions.Limits.MaxRequestHeaderCount = 1; _serviceContext.ServerOptions.Limits.MaxRequestHeaderCount = 1;
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes($"{headerLines}\r\n")); await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes($"{headerLines}\r\n"));
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
var exception = Assert.Throws<BadHttpRequestException>(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined)); var exception = Assert.Throws<BadHttpRequestException>(() => _frame.TakeMessageHeaders(readableBuffer, out _consumed, out _examined));
_socketInput.Reader.Advance(_consumed, _examined); _input.Reader.Advance(_consumed, _examined);
Assert.Equal("Request contains too many headers.", exception.Message); Assert.Equal("Request contains too many headers.", exception.Message);
Assert.Equal(StatusCodes.Status431RequestHeaderFieldsTooLarge, exception.StatusCode); Assert.Equal(StatusCodes.Status431RequestHeaderFieldsTooLarge, exception.StatusCode);
} }
[Theory]
[InlineData("Cookie: \r\n\r\n", 1)]
[InlineData("Cookie:\r\n\r\n", 1)]
[InlineData("Cookie: \r\nConnection: close\r\n\r\n", 2)]
[InlineData("Cookie:\r\nConnection: close\r\n\r\n", 2)]
[InlineData("Connection: close\r\nCookie: \r\n\r\n", 2)]
[InlineData("Connection: close\r\nCookie:\r\n\r\n", 2)]
public async Task EmptyHeaderValuesCanBeParsed(string rawHeaders, int numHeaders)
{
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders));
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer;
var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined);
_socketInput.Reader.Advance(_consumed, _examined);
Assert.True(success);
Assert.Equal(numHeaders, _frame.RequestHeaders.Count);
Assert.Equal(readableBuffer.End, _consumed);
}
[Fact] [Fact]
public void ResetResetsScheme() public void ResetResetsScheme()
{ {
@ -279,11 +156,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
options.Limits.MaxRequestHeaderCount = 1; options.Limits.MaxRequestHeaderCount = 1;
_serviceContext.ServerOptions = options; _serviceContext.ServerOptions = options;
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine1}\r\n")); await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine1}\r\n"));
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
var takeMessageHeaders = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined); var takeMessageHeaders = _frame.TakeMessageHeaders(readableBuffer, out _consumed, out _examined);
_socketInput.Reader.Advance(_consumed, _examined); _input.Reader.Advance(_consumed, _examined);
Assert.True(takeMessageHeaders); Assert.True(takeMessageHeaders);
Assert.Equal(1, _frame.RequestHeaders.Count); Assert.Equal(1, _frame.RequestHeaders.Count);
@ -291,11 +168,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
_frame.Reset(); _frame.Reset();
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine2}\r\n")); await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine2}\r\n"));
readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
takeMessageHeaders = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined); takeMessageHeaders = _frame.TakeMessageHeaders(readableBuffer, out _consumed, out _examined);
_socketInput.Reader.Advance(_consumed, _examined); _input.Reader.Advance(_consumed, _examined);
Assert.True(takeMessageHeaders); Assert.True(takeMessageHeaders);
Assert.Equal(1, _frame.RequestHeaders.Count); Assert.Equal(1, _frame.RequestHeaders.Count);
@ -390,86 +267,37 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
public async Task TakeStartLineSetsFrameProperties( public async Task TakeStartLineSetsFrameProperties(
string requestLine, string requestLine,
string expectedMethod, string expectedMethod,
string expectedPath, string expectedRawTarget,
string expectedRawPath,
string expectedDecodedPath,
string expectedQueryString, string expectedQueryString,
string expectedHttpVersion) string expectedHttpVersion)
{ {
var requestLineBytes = Encoding.ASCII.GetBytes(requestLine); var requestLineBytes = Encoding.ASCII.GetBytes(requestLine);
await _socketInput.Writer.WriteAsync(requestLineBytes); await _input.Writer.WriteAsync(requestLineBytes);
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
var returnValue = _frame.TakeStartLine(readableBuffer, out _consumed, out _examined); var returnValue = _frame.TakeStartLine(readableBuffer, out _consumed, out _examined);
_socketInput.Reader.Advance(_consumed, _examined); _input.Reader.Advance(_consumed, _examined);
Assert.True(returnValue); Assert.True(returnValue);
Assert.Equal(expectedMethod, _frame.Method); Assert.Equal(expectedMethod, _frame.Method);
Assert.Equal(expectedPath, _frame.Path); Assert.Equal(expectedRawTarget, _frame.RawTarget);
Assert.Equal(expectedDecodedPath, _frame.Path);
Assert.Equal(expectedQueryString, _frame.QueryString); Assert.Equal(expectedQueryString, _frame.QueryString);
Assert.Equal(expectedHttpVersion, _frame.HttpVersion); Assert.Equal(expectedHttpVersion, _frame.HttpVersion);
} }
[Fact] [Fact]
public async Task TakeStartLineCallsConsumingCompleteWithFurthestExamined() public async Task ParseRequestStartsRequestHeadersTimeoutOnFirstByteAvailable()
{
var requestLineBytes = Encoding.ASCII.GetBytes("GET / ");
await _socketInput.Writer.WriteAsync(requestLineBytes);
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer;
_frame.TakeStartLine(readableBuffer, out _consumed, out _examined);
_socketInput.Reader.Advance(_consumed, _examined);
Assert.Equal(readableBuffer.Start, _consumed);
Assert.Equal(readableBuffer.End, _examined);
requestLineBytes = Encoding.ASCII.GetBytes("HTTP/1.1\r\n");
await _socketInput.Writer.WriteAsync(requestLineBytes);
readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer;
_frame.TakeStartLine(readableBuffer, out _consumed, out _examined);
_socketInput.Reader.Advance(_consumed, _examined);
Assert.Equal(readableBuffer.End, _consumed);
Assert.Equal(readableBuffer.End, _examined);
}
[Theory]
[InlineData("G")]
[InlineData("GE")]
[InlineData("GET")]
[InlineData("GET ")]
[InlineData("GET /")]
[InlineData("GET / ")]
[InlineData("GET / H")]
[InlineData("GET / HT")]
[InlineData("GET / HTT")]
[InlineData("GET / HTTP")]
[InlineData("GET / HTTP/")]
[InlineData("GET / HTTP/1")]
[InlineData("GET / HTTP/1.")]
[InlineData("GET / HTTP/1.1")]
[InlineData("GET / HTTP/1.1\r")]
public async Task TakeStartLineReturnsWhenGivenIncompleteRequestLines(string requestLine)
{
var requestLineBytes = Encoding.ASCII.GetBytes(requestLine);
await _socketInput.Writer.WriteAsync(requestLineBytes);
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer;
var returnValue = _frame.TakeStartLine(readableBuffer, out _consumed, out _examined);
_socketInput.Reader.Advance(_consumed, _examined);
Assert.False(returnValue);
}
[Fact]
public async Task TakeStartLineStartsRequestHeadersTimeoutOnFirstByteAvailable()
{ {
var connectionControl = new Mock<IConnectionControl>(); var connectionControl = new Mock<IConnectionControl>();
_connectionContext.ConnectionControl = connectionControl.Object; _connectionContext.ConnectionControl = connectionControl.Object;
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes("G")); await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes("G"));
_frame.TakeStartLine((await _socketInput.Reader.ReadAsync()).Buffer, out _consumed, out _examined); _frame.ParseRequest((await _input.Reader.ReadAsync()).Buffer, out _consumed, out _examined);
_socketInput.Reader.Advance(_consumed, _examined); _input.Reader.Advance(_consumed, _examined);
var expectedRequestHeadersTimeout = (long)_serviceContext.ServerOptions.Limits.RequestHeadersTimeout.TotalMilliseconds; var expectedRequestHeadersTimeout = (long)_serviceContext.ServerOptions.Limits.RequestHeadersTimeout.TotalMilliseconds;
connectionControl.Verify(cc => cc.ResetTimeout(expectedRequestHeadersTimeout, TimeoutAction.SendTimeoutResponse)); connectionControl.Verify(cc => cc.ResetTimeout(expectedRequestHeadersTimeout, TimeoutAction.SendTimeoutResponse));
@ -481,93 +309,42 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
_serviceContext.ServerOptions.Limits.MaxRequestLineSize = "GET / HTTP/1.1\r\n".Length; _serviceContext.ServerOptions.Limits.MaxRequestLineSize = "GET / HTTP/1.1\r\n".Length;
var requestLineBytes = Encoding.ASCII.GetBytes("GET /a HTTP/1.1\r\n"); var requestLineBytes = Encoding.ASCII.GetBytes("GET /a HTTP/1.1\r\n");
await _socketInput.Writer.WriteAsync(requestLineBytes); await _input.Writer.WriteAsync(requestLineBytes);
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
var exception = Assert.Throws<BadHttpRequestException>(() => _frame.TakeStartLine(readableBuffer, out _consumed, out _examined)); var exception = Assert.Throws<BadHttpRequestException>(() => _frame.TakeStartLine(readableBuffer, out _consumed, out _examined));
_socketInput.Reader.Advance(_consumed, _examined); _input.Reader.Advance(_consumed, _examined);
Assert.Equal("Request line too long.", exception.Message); Assert.Equal("Request line too long.", exception.Message);
Assert.Equal(StatusCodes.Status414UriTooLong, exception.StatusCode); Assert.Equal(StatusCodes.Status414UriTooLong, exception.StatusCode);
} }
[Theory] [Theory]
[MemberData(nameof(InvalidRequestLineData))] [MemberData(nameof(RequestLineWithEncodedNullCharInTargetData))]
public async Task TakeStartLineThrowsOnInvalidRequestLine(string requestLine, Type expectedExceptionType, string expectedExceptionMessage) public async Task TakeStartLineThrowsOnEncodedNullCharInTarget(string requestLine)
{ {
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(requestLine)); await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes(requestLine));
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
var exception = Assert.Throws(expectedExceptionType, () => var exception = Assert.Throws<InvalidOperationException>(() =>
_frame.TakeStartLine(readableBuffer, out _consumed, out _examined)); _frame.TakeStartLine(readableBuffer, out _consumed, out _examined));
_socketInput.Reader.Advance(_consumed, _examined); _input.Reader.Advance(_consumed, _examined);
Assert.Equal(expectedExceptionMessage, exception.Message); Assert.Equal("The path contains null characters.", exception.Message);
if (expectedExceptionType == typeof(BadHttpRequestException))
{
Assert.Equal(StatusCodes.Status400BadRequest, (exception as BadHttpRequestException).StatusCode);
}
} }
[Theory] [Theory]
[MemberData(nameof(UnrecognizedHttpVersionData))] [MemberData(nameof(RequestLineWithNullCharInTargetData))]
public async Task TakeStartLineThrowsOnUnrecognizedHttpVersion(string httpVersion) public async Task TakeStartLineThrowsOnNullCharInTarget(string requestLine)
{ {
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes($"GET / {httpVersion}\r\n")); await _input.Writer.WriteAsync(Encoding.ASCII.GetBytes(requestLine));
var readableBuffer = (await _input.Reader.ReadAsync()).Buffer;
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; var exception = Assert.Throws<InvalidOperationException>(() =>
_frame.TakeStartLine(readableBuffer, out _consumed, out _examined));
_input.Reader.Advance(_consumed, _examined);
var exception = Assert.Throws<BadHttpRequestException>(() => _frame.TakeStartLine(readableBuffer, out _consumed, out _examined)); Assert.Equal(new InvalidOperationException().Message, exception.Message);
_socketInput.Reader.Advance(_consumed, _examined);
Assert.Equal($"Unrecognized HTTP version: {httpVersion}", exception.Message);
Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, exception.StatusCode);
}
[Fact]
public async Task TakeMessageHeadersCallsConsumingCompleteWithFurthestExamined()
{
foreach (var rawHeader in new[] { "Header: ", "value\r\n", "\r\n" })
{
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeader));
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer;
_frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined);
_socketInput.Reader.Advance(_consumed, _examined);
Assert.Equal(readableBuffer.End, _examined);
}
}
[Theory]
[InlineData("\r")]
[InlineData("H")]
[InlineData("He")]
[InlineData("Hea")]
[InlineData("Head")]
[InlineData("Heade")]
[InlineData("Header")]
[InlineData("Header:")]
[InlineData("Header: ")]
[InlineData("Header: v")]
[InlineData("Header: va")]
[InlineData("Header: val")]
[InlineData("Header: valu")]
[InlineData("Header: value")]
[InlineData("Header: value\r")]
[InlineData("Header: value\r\n")]
[InlineData("Header: value\r\n\r")]
public async Task TakeMessageHeadersReturnsWhenGivenIncompleteHeaders(string headers)
{
var headerBytes = Encoding.ASCII.GetBytes(headers);
await _socketInput.Writer.WriteAsync(headerBytes);
ReadCursor consumed;
ReadCursor examined;
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer;
Assert.Equal(false, _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined));
_socketInput.Reader.Advance(consumed, examined);
} }
[Fact] [Fact]
@ -582,7 +359,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
connectionControl.Verify(cc => cc.SetTimeout(expectedKeepAliveTimeout, TimeoutAction.CloseConnection)); connectionControl.Verify(cc => cc.SetTimeout(expectedKeepAliveTimeout, TimeoutAction.CloseConnection));
_frame.StopAsync(); _frame.StopAsync();
_socketInput.Writer.Complete(); _input.Writer.Complete();
requestProcessingTask.Wait(); requestProcessingTask.Wait();
} }
@ -664,13 +441,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
_frame.Start(); _frame.Start();
var data = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\n\r\n"); var data = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\n\r\n");
await _socketInput.Writer.WriteAsync(data); await _input.Writer.WriteAsync(data);
var requestProcessingTask = _frame.StopAsync(); var requestProcessingTask = _frame.StopAsync();
Assert.IsNotType(typeof(Task<Task>), requestProcessingTask); Assert.IsNotType(typeof(Task<Task>), requestProcessingTask);
await requestProcessingTask.TimeoutAfter(TimeSpan.FromSeconds(10)); await requestProcessingTask.TimeoutAfter(TimeSpan.FromSeconds(10));
_socketInput.Writer.Complete(); _input.Writer.Complete();
} }
[Fact] [Fact]
@ -741,12 +518,36 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.NotSame(original, _frame.RequestAborted.WaitHandle); Assert.NotSame(original, _frame.RequestAborted.WaitHandle);
} }
public static IEnumerable<object> ValidRequestLineData => HttpParsingData.ValidRequestLineData; public static IEnumerable<object> ValidRequestLineData => HttpParsingData.RequestLineValidData;
public static IEnumerable<object> InvalidRequestLineData => HttpParsingData.InvalidRequestLineData; public static TheoryData<string> RequestLineWithEncodedNullCharInTargetData
{
get
{
var data = new TheoryData<string>();
public static TheoryData<string> UnrecognizedHttpVersionData => HttpParsingData.UnrecognizedHttpVersionData; foreach (var requestLine in HttpParsingData.RequestLineWithEncodedNullCharInTargetData)
{
data.Add(requestLine);
}
public static IEnumerable<object[]> InvalidRequestHeaderData => HttpParsingData.InvalidRequestHeaderData; return data;
}
}
public static TheoryData<string> RequestLineWithNullCharInTargetData
{
get
{
var data = new TheoryData<string>();
foreach (var requestLine in HttpParsingData.RequestLineWithNullCharInTargetData)
{
data.Add(requestLine);
}
return data;
}
}
} }
} }

View File

@ -0,0 +1,383 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Http;
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.Extensions.Logging;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests
{
public class HttpParserTests
{
// Returns true when all headers parsed
// Return false otherwise
[Theory]
[MemberData(nameof(RequestLineValidData))]
public void ParsesRequestLine(
string requestLine,
string expectedMethod,
string expectedRawTarget,
string expectedRawPath,
string expectedDecodedPath,
string expectedQueryString,
string expectedVersion)
{
var parser = CreateParser(Mock.Of<IKestrelTrace>());
var buffer = ReadableBuffer.Create(Encoding.ASCII.GetBytes(requestLine));
string parsedMethod = null;
string parsedVersion = null;
string parsedRawTarget = null;
string parsedRawPath = null;
string parsedQuery = null;
var requestLineHandler = new Mock<IHttpRequestLineHandler>();
requestLineHandler
.Setup(handler => handler.OnStartLine(
It.IsAny<HttpMethod>(),
It.IsAny<HttpVersion>(),
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) =>
{
parsedMethod = method != HttpMethod.Custom ? HttpUtilities.MethodToString(method) : customMethod.GetAsciiStringNonNullCharacters();
parsedVersion = HttpUtilities.VersionToString(version);
parsedRawTarget = target.GetAsciiStringNonNullCharacters();
parsedRawPath = path.GetAsciiStringNonNullCharacters();
parsedQuery = query.GetAsciiStringNonNullCharacters();
});
Assert.True(parser.ParseRequestLine(requestLineHandler.Object, buffer, out var consumed, out var examined));
Assert.Equal(parsedMethod, expectedMethod);
Assert.Equal(parsedVersion, expectedVersion);
Assert.Equal(parsedRawTarget, expectedRawTarget);
Assert.Equal(parsedRawPath, expectedRawPath);
Assert.Equal(parsedVersion, expectedVersion);
Assert.Equal(buffer.End, consumed);
Assert.Equal(buffer.End, examined);
}
[Theory]
[MemberData(nameof(RequestLineIncompleteData))]
public void ParseRequestLineReturnsFalseWhenGivenIncompleteRequestLines(string requestLine)
{
var parser = CreateParser(Mock.Of<IKestrelTrace>());
var buffer = ReadableBuffer.Create(Encoding.ASCII.GetBytes(requestLine));
Assert.False(parser.ParseRequestLine(Mock.Of<IHttpRequestLineHandler>(), buffer, out var consumed, out var examined));
}
[Theory]
[MemberData(nameof(RequestLineIncompleteData))]
public void ParseRequestLineDoesNotConsumeIncompleteRequestLine(string requestLine)
{
var parser = CreateParser(Mock.Of<IKestrelTrace>());
var buffer = ReadableBuffer.Create(Encoding.ASCII.GetBytes(requestLine));
Assert.False(parser.ParseRequestLine(Mock.Of<IHttpRequestLineHandler>(), buffer, out var consumed, out var examined));
Assert.Equal(buffer.Start, consumed);
Assert.Equal(buffer.End, examined);
}
[Theory]
[MemberData(nameof(RequestLineInvalidData))]
public void ParseRequestLineThrowsOnInvalidRequestLine(string requestLine)
{
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(requestLine));
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(StatusCodes.Status400BadRequest, (exception as BadHttpRequestException).StatusCode);
}
[Theory]
[MemberData(nameof(UnrecognizedHttpVersionData))]
public void ParseRequestLineThrowsOnUnrecognizedHttpVersion(string httpVersion)
{
var requestLine = $"GET / {httpVersion}\r\n";
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(requestLine));
var exception = Assert.Throws<BadHttpRequestException>(() =>
parser.ParseRequestLine(Mock.Of<IHttpRequestLineHandler>(), buffer, out var consumed, out var examined));
Assert.Equal($"Unrecognized HTTP version: {httpVersion}", exception.Message);
Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, (exception as BadHttpRequestException).StatusCode);
}
[Theory]
[InlineData("\r")]
[InlineData("H")]
[InlineData("He")]
[InlineData("Hea")]
[InlineData("Head")]
[InlineData("Heade")]
[InlineData("Header")]
[InlineData("Header:")]
[InlineData("Header: ")]
[InlineData("Header: v")]
[InlineData("Header: va")]
[InlineData("Header: val")]
[InlineData("Header: valu")]
[InlineData("Header: value")]
[InlineData("Header: value\r")]
[InlineData("Header: value\r\n")]
[InlineData("Header: value\r\n\r")]
[InlineData("Header-1: value1\r\nH")]
[InlineData("Header-1: value1\r\nHe")]
[InlineData("Header-1: value1\r\nHea")]
[InlineData("Header-1: value1\r\nHead")]
[InlineData("Header-1: value1\r\nHeade")]
[InlineData("Header-1: value1\r\nHeader")]
[InlineData("Header-1: value1\r\nHeader-")]
[InlineData("Header-1: value1\r\nHeader-2")]
[InlineData("Header-1: value1\r\nHeader-2:")]
[InlineData("Header-1: value1\r\nHeader-2: ")]
[InlineData("Header-1: value1\r\nHeader-2: v")]
[InlineData("Header-1: value1\r\nHeader-2: va")]
[InlineData("Header-1: value1\r\nHeader-2: val")]
[InlineData("Header-1: value1\r\nHeader-2: valu")]
[InlineData("Header-1: value1\r\nHeader-2: value")]
[InlineData("Header-1: value1\r\nHeader-2: value2")]
[InlineData("Header-1: value1\r\nHeader-2: value2\r")]
[InlineData("Header-1: value1\r\nHeader-2: value2\r\n")]
[InlineData("Header-1: value1\r\nHeader-2: value2\r\n\r")]
public void ParseHeadersReturnsFalseWhenGivenIncompleteHeaders(string rawHeaders)
{
var parser = CreateParser(Mock.Of<IKestrelTrace>());
var buffer = ReadableBuffer.Create(Encoding.ASCII.GetBytes(rawHeaders));
Assert.False(parser.ParseHeaders(Mock.Of<IHttpHeadersHandler>(), buffer, out var consumed, out var examined, out var consumedBytes));
}
[Theory]
[InlineData("\r")]
[InlineData("H")]
[InlineData("He")]
[InlineData("Hea")]
[InlineData("Head")]
[InlineData("Heade")]
[InlineData("Header")]
[InlineData("Header:")]
[InlineData("Header: ")]
[InlineData("Header: v")]
[InlineData("Header: va")]
[InlineData("Header: val")]
[InlineData("Header: valu")]
[InlineData("Header: value")]
[InlineData("Header: value\r")]
public void ParseHeadersDoesNotConsumeIncompleteHeader(string rawHeaders)
{
var parser = CreateParser(Mock.Of<IKestrelTrace>());
var buffer = ReadableBuffer.Create(Encoding.ASCII.GetBytes(rawHeaders));
parser.ParseHeaders(Mock.Of<IHttpHeadersHandler>(), buffer, out var consumed, out var examined, out var consumedBytes);
Assert.Equal(buffer.Start, consumed);
Assert.Equal(buffer.End, examined);
Assert.Equal(0, consumedBytes);
}
[Fact]
public void ParseHeadersCanReadHeaderValueWithoutLeadingWhitespace()
{
VerifyHeader("Header", "value", "value");
}
[Theory]
[InlineData("Cookie: \r\n\r\n", "Cookie", "", null, null)]
[InlineData("Cookie:\r\n\r\n", "Cookie", "", null, null)]
[InlineData("Cookie: \r\nConnection: close\r\n\r\n", "Cookie", "", "Connection", "close")]
[InlineData("Cookie:\r\nConnection: close\r\n\r\n", "Cookie", "", "Connection", "close")]
[InlineData("Connection: close\r\nCookie: \r\n\r\n", "Connection", "close", "Cookie", "")]
[InlineData("Connection: close\r\nCookie:\r\n\r\n", "Connection", "close", "Cookie", "")]
public void ParseHeadersCanParseEmptyHeaderValues(
string rawHeaders,
string expectedHeaderName1,
string expectedHeaderValue1,
string expectedHeaderName2,
string expectedHeaderValue2)
{
var expectedHeaderNames = expectedHeaderName2 == null
? new[] { expectedHeaderName1 }
: new[] { expectedHeaderName1, expectedHeaderName2 };
var expectedHeaderValues = expectedHeaderValue2 == null
? new[] { expectedHeaderValue1 }
: new[] { expectedHeaderValue1, expectedHeaderValue2 };
VerifyRawHeaders(rawHeaders, expectedHeaderNames, expectedHeaderValues);
}
[Theory]
[InlineData(" value")]
[InlineData(" value")]
[InlineData("\tvalue")]
[InlineData(" \tvalue")]
[InlineData("\t value")]
[InlineData("\t\tvalue")]
[InlineData("\t\t value")]
[InlineData(" \t\tvalue")]
[InlineData(" \t\t value")]
[InlineData(" \t \t value")]
public void ParseHeadersDoesNotIncludeLeadingWhitespaceInHeaderValue(string rawHeaderValue)
{
VerifyHeader("Header", rawHeaderValue, "value");
}
[Theory]
[InlineData("value ")]
[InlineData("value\t")]
[InlineData("value \t")]
[InlineData("value\t ")]
[InlineData("value\t\t")]
[InlineData("value\t\t ")]
[InlineData("value \t\t")]
[InlineData("value \t\t ")]
[InlineData("value \t \t ")]
public void ParseHeadersDoesNotIncludeTrailingWhitespaceInHeaderValue(string rawHeaderValue)
{
VerifyHeader("Header", rawHeaderValue, "value");
}
[Theory]
[InlineData("one two three")]
[InlineData("one two three")]
[InlineData("one\ttwo\tthree")]
[InlineData("one two\tthree")]
[InlineData("one\ttwo three")]
[InlineData("one \ttwo \tthree")]
[InlineData("one\t two\t three")]
[InlineData("one \ttwo\t three")]
public void ParseHeadersPreservesWhitespaceWithinHeaderValue(string headerValue)
{
VerifyHeader("Header", headerValue, headerValue);
}
[Fact]
public void ParseHeadersConsumesBytesCorrectlyAtEnd()
{
var parser = CreateParser(Mock.Of<IKestrelTrace>());
const string headerLine = "Header: value\r\n\r";
var buffer1 = ReadableBuffer.Create(Encoding.ASCII.GetBytes(headerLine));
Assert.False(parser.ParseHeaders(Mock.Of<IHttpHeadersHandler>(), buffer1, out var consumed, out var examined, out var consumedBytes));
Assert.Equal(buffer1.Move(buffer1.Start, headerLine.Length - 1), consumed);
Assert.Equal(buffer1.End, examined);
Assert.Equal(headerLine.Length - 1, consumedBytes);
var buffer2 = ReadableBuffer.Create(Encoding.ASCII.GetBytes("\r\n"));
Assert.True(parser.ParseHeaders(Mock.Of<IHttpHeadersHandler>(), buffer2, out consumed, out examined, out consumedBytes));
Assert.Equal(buffer2.End, consumed);
Assert.Equal(buffer2.End, examined);
Assert.Equal(2, consumedBytes);
}
[Theory]
[MemberData(nameof(RequestHeaderInvalidData))]
public void ParseHeadersThrowsOnInvalidRequestHeaders(string rawHeaders, string expectedExceptionMessage)
{
var parser = CreateParser(Mock.Of<IKestrelTrace>());
var buffer = ReadableBuffer.Create(Encoding.ASCII.GetBytes(rawHeaders));
var exception = Assert.Throws<BadHttpRequestException>(() =>
parser.ParseHeaders(Mock.Of<IHttpHeadersHandler>(), buffer, out var consumed, out var examined, out var consumedBytes));
Assert.Equal(expectedExceptionMessage, exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
}
private void VerifyHeader(
string headerName,
string rawHeaderValue,
string expectedHeaderValue)
{
var parser = CreateParser(Mock.Of<IKestrelTrace>());
var buffer = ReadableBuffer.Create(Encoding.ASCII.GetBytes($"{headerName}:{rawHeaderValue}\r\n"));
string parsedHeaderName = "unexpected";
string parsedHeaderValue = "unexpected";
var headersHandler = new Mock<IHttpHeadersHandler>();
headersHandler
.Setup(handler => handler.OnHeader(It.IsAny<Span<byte>>(), It.IsAny<Span<byte>>()))
.Callback<Span<byte>, Span<byte>>((name, value) =>
{
parsedHeaderName = name.GetAsciiStringNonNullCharacters();
parsedHeaderValue = value.GetAsciiStringNonNullCharacters();
});
parser.ParseHeaders(headersHandler.Object, buffer, out var consumed, out var examined, out var consumedBytes);
Assert.Equal(headerName, parsedHeaderName);
Assert.Equal(expectedHeaderValue, parsedHeaderValue);
Assert.Equal(buffer.End, consumed);
Assert.Equal(buffer.End, examined);
}
private void VerifyRawHeaders(string rawHeaders, IEnumerable<string> expectedHeaderNames, IEnumerable<string> expectedHeaderValues)
{
Assert.True(expectedHeaderNames.Count() == expectedHeaderValues.Count(), $"{nameof(expectedHeaderNames)} and {nameof(expectedHeaderValues)} sizes must match");
var parser = CreateParser(Mock.Of<IKestrelTrace>());
var buffer = ReadableBuffer.Create(Encoding.ASCII.GetBytes(rawHeaders));
var parsedHeaders = new List<Tuple<string, string>>();
var headersHandler = new Mock<IHttpHeadersHandler>();
headersHandler
.Setup(handler => handler.OnHeader(It.IsAny<Span<byte>>(), It.IsAny<Span<byte>>()))
.Callback<Span<byte>, Span<byte>>((name, value) =>
{
parsedHeaders.Add(Tuple.Create(name.GetAsciiStringNonNullCharacters(), value.GetAsciiStringNonNullCharacters()));
});
parser.ParseHeaders(headersHandler.Object, buffer, out var consumed, out var examined, out var consumedBytes);
Assert.Equal(expectedHeaderNames.Count(), parsedHeaders.Count);
Assert.Equal(expectedHeaderNames, parsedHeaders.Select(t => t.Item1));
Assert.Equal(expectedHeaderValues, parsedHeaders.Select(t => t.Item2));
Assert.Equal(buffer.End, consumed);
Assert.Equal(buffer.End, examined);
}
private IHttpParser CreateParser(IKestrelTrace log) => new KestrelHttpParser(log);
public static IEnumerable<string[]> RequestLineValidData => HttpParsingData.RequestLineValidData;
public static IEnumerable<object[]> RequestLineIncompleteData => HttpParsingData.RequestLineIncompleteData.Select(requestLine => new[] { requestLine });
public static IEnumerable<object[]> RequestLineInvalidData => HttpParsingData.RequestLineInvalidData.Select(requestLine => new[] { requestLine });
public static TheoryData<string> UnrecognizedHttpVersionData => HttpParsingData.UnrecognizedHttpVersionData;
public static IEnumerable<object[]> RequestHeaderInvalidData => HttpParsingData.RequestHeaderInvalidData;
}
}

View File

@ -8,105 +8,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
public class MemoryPoolBlockTests public class MemoryPoolBlockTests
{ {
[Fact]
public void SeekWorks()
{
using (var pool = new MemoryPool())
{
var block = pool.Lease();
foreach (var ch in Enumerable.Range(0, 256).Select(x => (byte)x))
{
block.Array[block.End++] = ch;
}
var iterator = block.GetIterator();
foreach (var ch in Enumerable.Range(0, 256).Select(x => (byte)x))
{
var hit = iterator;
hit.Seek(ch);
Assert.Equal(ch, iterator.GetLength(hit));
hit = iterator;
hit.Seek(ch, byte.MaxValue);
Assert.Equal(ch, iterator.GetLength(hit));
hit = iterator;
hit.Seek(byte.MaxValue, ch);
Assert.Equal(ch, iterator.GetLength(hit));
hit = iterator;
hit.Seek(ch, byte.MaxValue, byte.MaxValue);
Assert.Equal(ch, iterator.GetLength(hit));
hit = iterator;
hit.Seek(byte.MaxValue, ch, byte.MaxValue);
Assert.Equal(ch, iterator.GetLength(hit));
hit = iterator;
hit.Seek(ch, byte.MaxValue, byte.MaxValue);
Assert.Equal(ch, iterator.GetLength(hit));
}
pool.Return(block);
}
}
[Fact]
public void SeekWorksAcrossBlocks()
{
using (var pool = new MemoryPool())
{
var block1 = pool.Lease();
var block2 = block1.Next = pool.Lease();
var block3 = block2.Next = pool.Lease();
foreach (var ch in Enumerable.Range(0, 34).Select(x => (byte)x))
{
block1.Array[block1.End++] = ch;
}
foreach (var ch in Enumerable.Range(34, 25).Select(x => (byte)x))
{
block2.Array[block2.End++] = ch;
}
foreach (var ch in Enumerable.Range(59, 197).Select(x => (byte)x))
{
block3.Array[block3.End++] = ch;
}
var iterator = block1.GetIterator();
foreach (var ch in Enumerable.Range(0, 256).Select(x => (byte)x))
{
var hit = iterator;
hit.Seek(ch);
Assert.Equal(ch, iterator.GetLength(hit));
hit = iterator;
hit.Seek(ch, byte.MaxValue);
Assert.Equal(ch, iterator.GetLength(hit));
hit = iterator;
hit.Seek(byte.MaxValue, ch);
Assert.Equal(ch, iterator.GetLength(hit));
hit = iterator;
hit.Seek(ch, byte.MaxValue, byte.MaxValue);
Assert.Equal(ch, iterator.GetLength(hit));
hit = iterator;
hit.Seek(byte.MaxValue, ch, byte.MaxValue);
Assert.Equal(ch, iterator.GetLength(hit));
hit = iterator;
hit.Seek(byte.MaxValue, byte.MaxValue, ch);
Assert.Equal(ch, iterator.GetLength(hit));
}
pool.Return(block1);
pool.Return(block2);
pool.Return(block3);
}
}
[Fact] [Fact]
public void GetLengthBetweenIteratorsWorks() public void GetLengthBetweenIteratorsWorks()
{ {
@ -192,87 +94,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
pool.Return(block2); pool.Return(block2);
} }
} }
[Fact]
public void CopyToCorrectlyTraversesBlocks()
{
using (var pool = new MemoryPool())
{
var block1 = pool.Lease();
var block2 = block1.Next = pool.Lease();
for (int i = 0; i < 128; i++)
{
block1.Array[block1.End++] = (byte)i;
}
for (int i = 128; i < 256; i++)
{
block2.Array[block2.End++] = (byte)i;
}
var beginIterator = block1.GetIterator();
var array = new byte[256];
int actual;
var endIterator = beginIterator.CopyTo(array, 0, 256, out actual);
Assert.Equal(256, actual);
for (int i = 0; i < 256; i++)
{
Assert.Equal(i, array[i]);
}
endIterator.CopyTo(array, 0, 256, out actual);
Assert.Equal(0, actual);
pool.Return(block1);
pool.Return(block2);
}
}
[Fact]
public void CopyFromCorrectlyTraversesBlocks()
{
using (var pool = new MemoryPool())
{
var block1 = pool.Lease();
var start = block1.GetIterator();
var end = start;
var bufferSize = block1.Data.Count * 3;
var buffer = new byte[bufferSize];
for (int i = 0; i < bufferSize; i++)
{
buffer[i] = (byte)(i % 73);
}
Assert.Null(block1.Next);
end.CopyFrom(new ArraySegment<byte>(buffer));
Assert.NotNull(block1.Next);
for (int i = 0; i < bufferSize; i++)
{
Assert.Equal(i % 73, start.Take());
}
Assert.Equal(-1, start.Take());
Assert.Equal(start.Block, end.Block);
Assert.Equal(start.Index, end.Index);
var block = block1;
while (block != null)
{
var returnBlock = block;
block = block.Next;
pool.Return(returnBlock);
}
}
}
[Fact] [Fact]
public void IsEndCorrectlyTraversesBlocks() public void IsEndCorrectlyTraversesBlocks()
{ {

View File

@ -6,8 +6,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
public static MemoryPoolIterator Add(this MemoryPoolIterator iterator, int count) public static MemoryPoolIterator Add(this MemoryPoolIterator iterator, int count)
{ {
int actual; for (int i = 0; i < count; i++)
return iterator.CopyTo(new byte[count], 0, count, out actual); {
iterator.Take();
}
return iterator;
} }
} }
} }

View File

@ -30,80 +30,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
_pool.Dispose(); _pool.Dispose();
} }
[Fact]
public void TestFindFirstEqualByte()
{
var bytes = Enumerable.Repeat<byte>(0xff, Vector<byte>.Count).ToArray();
for (int i = 0; i < Vector<byte>.Count; i++)
{
Vector<byte> vector = new Vector<byte>(bytes);
Assert.Equal(i, MemoryPoolIterator.LocateFirstFoundByte(vector));
bytes[i] = 0;
}
for (int i = 0; i < Vector<byte>.Count; i++)
{
bytes[i] = 1;
Vector<byte> vector = new Vector<byte>(bytes);
Assert.Equal(i, MemoryPoolIterator.LocateFirstFoundByte(vector));
bytes[i] = 0;
}
}
[Theory]
[InlineData("a", "a", 'a', 0)]
[InlineData("ab", "a", 'a', 0)]
[InlineData("aab", "a", 'a', 0)]
[InlineData("acab", "a", 'a', 0)]
[InlineData("acab", "c", 'c', 1)]
[InlineData("abcdefghijklmnopqrstuvwxyz", "lo", 'l', 11)]
[InlineData("abcdefghijklmnopqrstuvwxyz", "ol", 'l', 11)]
[InlineData("abcdefghijklmnopqrstuvwxyz", "ll", 'l', 11)]
[InlineData("abcdefghijklmnopqrstuvwxyz", "lmr", 'l', 11)]
[InlineData("abcdefghijklmnopqrstuvwxyz", "rml", 'l', 11)]
[InlineData("abcdefghijklmnopqrstuvwxyz", "mlr", 'l', 11)]
[InlineData("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", "lmr", 'l', 11)]
[InlineData("aaaaaaaaaaalmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", "lmr", 'l', 11)]
[InlineData("aaaaaaaaaaacmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", "lmr", 'm', 12)]
[InlineData("aaaaaaaaaaarmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", "lmr", 'r', 11)]
[InlineData("/localhost:5000/PATH/%2FPATH2/ HTTP/1.1", " %?", '%', 21)]
[InlineData("/localhost:5000/PATH/%2FPATH2/?key=value HTTP/1.1", " %?", '%', 21)]
[InlineData("/localhost:5000/PATH/PATH2/?key=value HTTP/1.1", " %?", '?', 27)]
[InlineData("/localhost:5000/PATH/PATH2/ HTTP/1.1", " %?", ' ', 27)]
public void MemorySeek(string raw, string search, char expectResult, int expectIndex)
{
var block = _pool.Lease();
var chars = raw.ToCharArray().Select(c => (byte)c).ToArray();
Buffer.BlockCopy(chars, 0, block.Array, block.Start, chars.Length);
block.End += chars.Length;
var begin = block.GetIterator();
var searchFor = search.ToCharArray();
int found = -1;
if (searchFor.Length == 1)
{
found = begin.Seek((byte)searchFor[0]);
}
else if (searchFor.Length == 2)
{
found = begin.Seek((byte)searchFor[0], (byte)searchFor[1]);
}
else if (searchFor.Length == 3)
{
found = begin.Seek((byte)searchFor[0], (byte)searchFor[1], (byte)searchFor[2]);
}
else
{
Assert.False(true, "Invalid test sample.");
}
Assert.Equal(expectResult, found);
Assert.Equal(expectIndex, begin.Index - block.Start);
_pool.Return(block);
}
[Fact] [Fact]
public void Put() public void Put()
{ {
@ -241,728 +167,20 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Fact]
public void PeekLong()
{
// Arrange
var block = _pool.Lease();
var bytes = BitConverter.GetBytes(0x0102030405060708UL);
Buffer.BlockCopy(bytes, 0, block.Array, block.Start, bytes.Length);
block.End += bytes.Length;
var scan = block.GetIterator();
var originalIndex = scan.Index;
// Assert
ulong result;
Assert.True(scan.TryPeekLong(out result));
Assert.Equal(0x0102030405060708UL, result);
Assert.Equal(originalIndex, scan.Index);
_pool.Return(block);
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
[InlineData(6)]
[InlineData(7)]
public void PeekLongNotEnoughBytes(int totalBytes)
{
// Arrange
var block = _pool.Lease();
var bytes = BitConverter.GetBytes(0x0102030405060708UL);
var bytesLength = totalBytes;
Buffer.BlockCopy(bytes, 0, block.Array, block.Start, bytesLength);
block.End += bytesLength;
var scan = block.GetIterator();
var originalIndex = scan.Index;
// Assert
ulong result;
Assert.False(scan.TryPeekLong(out result));
Assert.Equal(originalIndex, scan.Index);
_pool.Return(block);
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
[InlineData(6)]
[InlineData(7)]
public void PeekLongNotEnoughBytesAtBlockBoundary(int firstBlockBytes)
{
// Arrange
var expectedResult = 0x0102030405060708UL;
var nextBlockBytes = 7 - firstBlockBytes;
var block = _pool.Lease();
block.End += firstBlockBytes;
var nextBlock = _pool.Lease();
nextBlock.End += nextBlockBytes;
block.Next = nextBlock;
var bytes = BitConverter.GetBytes(expectedResult);
Buffer.BlockCopy(bytes, 0, block.Array, block.Start, firstBlockBytes);
Buffer.BlockCopy(bytes, firstBlockBytes, nextBlock.Array, nextBlock.Start, nextBlockBytes);
var scan = block.GetIterator();
var originalIndex = scan.Index;
// Assert
ulong result;
Assert.False(scan.TryPeekLong(out result));
Assert.Equal(originalIndex, scan.Index);
_pool.Return(block);
_pool.Return(nextBlock);
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
[InlineData(6)]
[InlineData(7)]
[InlineData(8)]
public void PeekLongAtBlockBoundary(int firstBlockBytes)
{
// Arrange
var expectedResult = 0x0102030405060708UL;
var nonZeroData = 0xFF00FFFF0000FFFFUL;
var nextBlockBytes = 8 - firstBlockBytes;
var block = _pool.Lease();
block.Start += 8;
block.End = block.Start + firstBlockBytes;
var nextBlock = _pool.Lease();
nextBlock.Start += 8;
nextBlock.End = nextBlock.Start + nextBlockBytes;
block.Next = nextBlock;
var bytes = BitConverter.GetBytes(expectedResult);
Buffer.BlockCopy(bytes, 0, block.Array, block.Start, firstBlockBytes);
Buffer.BlockCopy(bytes, firstBlockBytes, nextBlock.Array, nextBlock.Start, nextBlockBytes);
// Fill in surrounding bytes with non-zero data
var nonZeroBytes = BitConverter.GetBytes(nonZeroData);
Buffer.BlockCopy(nonZeroBytes, 0, block.Array, block.Start - 8, 8);
Buffer.BlockCopy(nonZeroBytes, 0, block.Array, block.End, 8);
Buffer.BlockCopy(nonZeroBytes, 0, nextBlock.Array, nextBlock.Start - 8, 8);
Buffer.BlockCopy(nonZeroBytes, 0, nextBlock.Array, nextBlock.End, 8);
var scan = block.GetIterator();
var originalIndex = scan.Index;
// Assert
ulong result;
Assert.True(scan.TryPeekLong(out result));
Assert.Equal(expectedResult, result);
Assert.Equal(originalIndex, scan.Index);
_pool.Return(block);
_pool.Return(nextBlock);
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
[InlineData(6)]
[InlineData(7)]
[InlineData(8)]
public void PeekLongAtBlockBoundarayWithMostSignificatBitsSet(int firstBlockBytes)
{
// Arrange
var expectedResult = 0xFF02030405060708UL;
var nonZeroData = 0xFF00FFFF0000FFFFUL;
var nextBlockBytes = 8 - firstBlockBytes;
var block = _pool.Lease();
block.Start += 8;
block.End = block.Start + firstBlockBytes;
var nextBlock = _pool.Lease();
nextBlock.Start += 8;
nextBlock.End = nextBlock.Start + nextBlockBytes;
block.Next = nextBlock;
var expectedBytes = BitConverter.GetBytes(expectedResult);
Buffer.BlockCopy(expectedBytes, 0, block.Array, block.Start, firstBlockBytes);
Buffer.BlockCopy(expectedBytes, firstBlockBytes, nextBlock.Array, nextBlock.Start, nextBlockBytes);
// Fill in surrounding bytes with non-zero data
var nonZeroBytes = BitConverter.GetBytes(nonZeroData);
Buffer.BlockCopy(nonZeroBytes, 0, block.Array, block.Start - 8, 8);
Buffer.BlockCopy(nonZeroBytes, 0, block.Array, block.End, 8);
Buffer.BlockCopy(nonZeroBytes, 0, nextBlock.Array, nextBlock.Start - 8, 8);
Buffer.BlockCopy(nonZeroBytes, 0, nextBlock.Array, nextBlock.End, 8);
var scan = block.GetIterator();
var originalIndex = scan.Index;
// Assert
ulong result;
Assert.True(scan.TryPeekLong(out result));
Assert.Equal(expectedResult, result);
Assert.Equal(originalIndex, scan.Index);
_pool.Return(block);
_pool.Return(nextBlock);
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
[InlineData(6)]
[InlineData(7)]
[InlineData(8)]
[InlineData(9)]
public void SkipAtBlockBoundary(int blockBytes)
{
// Arrange
var nextBlockBytes = 10 - blockBytes;
var block = _pool.Lease();
block.End += blockBytes;
var nextBlock = _pool.Lease();
nextBlock.End += nextBlockBytes;
block.Next = nextBlock;
var bytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Buffer.BlockCopy(bytes, 0, block.Array, block.Start, blockBytes);
Buffer.BlockCopy(bytes, blockBytes, nextBlock.Array, nextBlock.Start, nextBlockBytes);
var scan = block.GetIterator();
var originalIndex = scan.Index;
// Act
scan.Skip(8);
var result = scan.Take();
// Assert
Assert.Equal(0x08, result);
Assert.NotEqual(originalIndex, scan.Index);
_pool.Return(block);
_pool.Return(nextBlock);
}
[Fact]
public void SkipThrowsWhenSkippingMoreBytesThanAvailableInSingleBlock()
{
// Arrange
var block = _pool.Lease();
block.End += 5;
var scan = block.GetIterator();
// Act/Assert
Assert.ThrowsAny<InvalidOperationException>(() => scan.Skip(8));
_pool.Return(block);
}
[Fact]
public void SkipThrowsWhenSkippingMoreBytesThanAvailableInMultipleBlocks()
{
// Arrange
var firstBlock = _pool.Lease();
firstBlock.End += 3;
var middleBlock = _pool.Lease();
middleBlock.End += 1;
firstBlock.Next = middleBlock;
var finalBlock = _pool.Lease();
finalBlock.End += 2;
middleBlock.Next = finalBlock;
var scan = firstBlock.GetIterator();
// Act/Assert
Assert.ThrowsAny<InvalidOperationException>(() => scan.Skip(8));
_pool.Return(firstBlock);
_pool.Return(middleBlock);
_pool.Return(finalBlock);
}
[Theory]
[MemberData(nameof(SeekByteLimitData))]
public void TestSeekByteLimitWithinSameBlock(string input, char seek, int limit, int expectedBytesScanned, int expectedReturnValue)
{
MemoryPoolBlock block = null;
try
{
// Arrange
block = _pool.Lease();
var chars = input.ToString().ToCharArray().Select(c => (byte) c).ToArray();
Buffer.BlockCopy(chars, 0, block.Array, block.Start, chars.Length);
block.End += chars.Length;
var scan = block.GetIterator();
// Act
int bytesScanned;
var returnValue = scan.Seek((byte)seek, out bytesScanned, limit);
// Assert
Assert.Equal(expectedBytesScanned, bytesScanned);
Assert.Equal(expectedReturnValue, returnValue);
Assert.Same(block, scan.Block);
var expectedEndIndex = expectedReturnValue != -1 ?
block.Start + input.IndexOf(seek) :
block.Start + expectedBytesScanned;
Assert.Equal(expectedEndIndex, scan.Index);
}
finally
{
// Cleanup
if (block != null) _pool.Return(block);
}
}
[Theory]
[MemberData(nameof(SeekByteLimitData))]
public void TestSeekByteLimitAcrossBlocks(string input, char seek, int limit, int expectedBytesScanned, int expectedReturnValue)
{
MemoryPoolBlock block1 = null;
MemoryPoolBlock block2 = null;
MemoryPoolBlock emptyBlock = null;
try
{
// Arrange
var input1 = input.Substring(0, input.Length / 2);
block1 = _pool.Lease();
var chars1 = input1.ToCharArray().Select(c => (byte)c).ToArray();
Buffer.BlockCopy(chars1, 0, block1.Array, block1.Start, chars1.Length);
block1.End += chars1.Length;
emptyBlock = _pool.Lease();
block1.Next = emptyBlock;
var input2 = input.Substring(input.Length / 2);
block2 = _pool.Lease();
var chars2 = input2.ToCharArray().Select(c => (byte)c).ToArray();
Buffer.BlockCopy(chars2, 0, block2.Array, block2.Start, chars2.Length);
block2.End += chars2.Length;
emptyBlock.Next = block2;
var scan = block1.GetIterator();
// Act
int bytesScanned;
var returnValue = scan.Seek((byte)seek, out bytesScanned, limit);
// Assert
Assert.Equal(expectedBytesScanned, bytesScanned);
Assert.Equal(expectedReturnValue, returnValue);
var seekCharIndex = input.IndexOf(seek);
var expectedEndBlock = limit <= input.Length / 2 ?
block1 :
(seekCharIndex != -1 && seekCharIndex < input.Length / 2 ? block1 : block2);
Assert.Same(expectedEndBlock, scan.Block);
var expectedEndIndex = expectedReturnValue != -1 ?
expectedEndBlock.Start + (expectedEndBlock == block1 ? input1.IndexOf(seek) : input2.IndexOf(seek)) :
expectedEndBlock.Start + (expectedEndBlock == block1 ? expectedBytesScanned : expectedBytesScanned - (input.Length / 2));
Assert.Equal(expectedEndIndex, scan.Index);
}
finally
{
// Cleanup
if (block1 != null) _pool.Return(block1);
if (emptyBlock != null) _pool.Return(emptyBlock);
if (block2 != null) _pool.Return(block2);
}
}
[Theory]
[MemberData(nameof(SeekIteratorLimitData))]
public void TestSeekIteratorLimitWithinSameBlock(string input, char seek, char limitAt, int expectedReturnValue)
{
MemoryPoolBlock block = null;
try
{
// Arrange
var afterSeek = (byte)'B';
block = _pool.Lease();
var chars = input.ToCharArray().Select(c => (byte)c).ToArray();
Buffer.BlockCopy(chars, 0, block.Array, block.Start, chars.Length);
block.End += chars.Length;
var scan1 = block.GetIterator();
var scan2_1 = scan1;
var scan2_2 = scan1;
var scan3_1 = scan1;
var scan3_2 = scan1;
var scan3_3 = scan1;
var end = scan1;
// Act
var endReturnValue = end.Seek((byte)limitAt);
var returnValue1 = scan1.Seek((byte)seek, ref end);
var returnValue2_1 = scan2_1.Seek((byte)seek, afterSeek, ref end);
var returnValue2_2 = scan2_2.Seek(afterSeek, (byte)seek, ref end);
var returnValue3_1 = scan3_1.Seek((byte)seek, afterSeek, afterSeek, ref end);
var returnValue3_2 = scan3_2.Seek(afterSeek, (byte)seek, afterSeek, ref end);
var returnValue3_3 = scan3_3.Seek(afterSeek, afterSeek, (byte)seek, ref end);
// Assert
Assert.Equal(input.Contains(limitAt) ? limitAt : -1, endReturnValue);
Assert.Equal(expectedReturnValue, returnValue1);
Assert.Equal(expectedReturnValue, returnValue2_1);
Assert.Equal(expectedReturnValue, returnValue2_2);
Assert.Equal(expectedReturnValue, returnValue3_1);
Assert.Equal(expectedReturnValue, returnValue3_2);
Assert.Equal(expectedReturnValue, returnValue3_3);
Assert.Same(block, scan1.Block);
Assert.Same(block, scan2_1.Block);
Assert.Same(block, scan2_2.Block);
Assert.Same(block, scan3_1.Block);
Assert.Same(block, scan3_2.Block);
Assert.Same(block, scan3_3.Block);
var expectedEndIndex = expectedReturnValue != -1 ? block.Start + input.IndexOf(seek) : end.Index;
Assert.Equal(expectedEndIndex, scan1.Index);
Assert.Equal(expectedEndIndex, scan2_1.Index);
Assert.Equal(expectedEndIndex, scan2_2.Index);
Assert.Equal(expectedEndIndex, scan3_1.Index);
Assert.Equal(expectedEndIndex, scan3_2.Index);
Assert.Equal(expectedEndIndex, scan3_3.Index);
}
finally
{
// Cleanup
if (block != null) _pool.Return(block);
}
}
[Theory]
[MemberData(nameof(SeekIteratorLimitData))]
public void TestSeekIteratorLimitAcrossBlocks(string input, char seek, char limitAt, int expectedReturnValue)
{
MemoryPoolBlock block1 = null;
MemoryPoolBlock block2 = null;
MemoryPoolBlock emptyBlock = null;
try
{
// Arrange
var afterSeek = (byte)'B';
var input1 = input.Substring(0, input.Length / 2);
block1 = _pool.Lease();
var chars1 = input1.ToCharArray().Select(c => (byte)c).ToArray();
Buffer.BlockCopy(chars1, 0, block1.Array, block1.Start, chars1.Length);
block1.End += chars1.Length;
emptyBlock = _pool.Lease();
block1.Next = emptyBlock;
var input2 = input.Substring(input.Length / 2);
block2 = _pool.Lease();
var chars2 = input2.ToCharArray().Select(c => (byte)c).ToArray();
Buffer.BlockCopy(chars2, 0, block2.Array, block2.Start, chars2.Length);
block2.End += chars2.Length;
emptyBlock.Next = block2;
var scan1 = block1.GetIterator();
var scan2_1 = scan1;
var scan2_2 = scan1;
var scan3_1 = scan1;
var scan3_2 = scan1;
var scan3_3 = scan1;
var end = scan1;
// Act
var endReturnValue = end.Seek((byte)limitAt);
var returnValue1 = scan1.Seek((byte)seek, ref end);
var returnValue2_1 = scan2_1.Seek((byte)seek, afterSeek, ref end);
var returnValue2_2 = scan2_2.Seek(afterSeek, (byte)seek, ref end);
var returnValue3_1 = scan3_1.Seek((byte)seek, afterSeek, afterSeek, ref end);
var returnValue3_2 = scan3_2.Seek(afterSeek, (byte)seek, afterSeek, ref end);
var returnValue3_3 = scan3_3.Seek(afterSeek, afterSeek, (byte)seek, ref end);
// Assert
Assert.Equal(input.Contains(limitAt) ? limitAt : -1, endReturnValue);
Assert.Equal(expectedReturnValue, returnValue1);
Assert.Equal(expectedReturnValue, returnValue2_1);
Assert.Equal(expectedReturnValue, returnValue2_2);
Assert.Equal(expectedReturnValue, returnValue3_1);
Assert.Equal(expectedReturnValue, returnValue3_2);
Assert.Equal(expectedReturnValue, returnValue3_3);
var seekCharIndex = input.IndexOf(seek);
var limitAtIndex = input.IndexOf(limitAt);
var expectedEndBlock = seekCharIndex != -1 && seekCharIndex < input.Length / 2 ?
block1 :
(limitAtIndex != -1 && limitAtIndex < input.Length / 2 ? block1 : block2);
Assert.Same(expectedEndBlock, scan1.Block);
Assert.Same(expectedEndBlock, scan2_1.Block);
Assert.Same(expectedEndBlock, scan2_2.Block);
Assert.Same(expectedEndBlock, scan3_1.Block);
Assert.Same(expectedEndBlock, scan3_2.Block);
Assert.Same(expectedEndBlock, scan3_3.Block);
var expectedEndIndex = expectedReturnValue != -1 ?
expectedEndBlock.Start + (expectedEndBlock == block1 ? input1.IndexOf(seek) : input2.IndexOf(seek)) :
end.Index;
Assert.Equal(expectedEndIndex, scan1.Index);
Assert.Equal(expectedEndIndex, scan2_1.Index);
Assert.Equal(expectedEndIndex, scan2_2.Index);
Assert.Equal(expectedEndIndex, scan3_1.Index);
Assert.Equal(expectedEndIndex, scan3_2.Index);
Assert.Equal(expectedEndIndex, scan3_3.Index);
}
finally
{
// Cleanup
if (block1 != null) _pool.Return(block1);
if (emptyBlock != null) _pool.Return(emptyBlock);
if (block2 != null) _pool.Return(block2);
}
}
[Fact] [Fact]
public void EmptyIteratorBehaviourIsValid() public void EmptyIteratorBehaviourIsValid()
{ {
const byte byteCr = (byte)'\n'; const byte byteCr = (byte)'\n';
ulong longValue;
var end = default(MemoryPoolIterator); var end = default(MemoryPoolIterator);
Assert.False(default(MemoryPoolIterator).TryPeekLong(out longValue));
Assert.Null(default(MemoryPoolIterator).GetAsciiString(ref end));
Assert.Null(default(MemoryPoolIterator).GetUtf8String(ref end));
// Assert.Equal doesn't work for default(ArraySegments)
Assert.True(default(MemoryPoolIterator).GetArraySegment(end).Equals(default(ArraySegment<byte>)));
Assert.True(default(MemoryPoolIterator).IsDefault); Assert.True(default(MemoryPoolIterator).IsDefault);
Assert.True(default(MemoryPoolIterator).IsEnd); Assert.True(default(MemoryPoolIterator).IsEnd);
Assert.Equal(default(MemoryPoolIterator).Take(), -1);
Assert.Equal(default(MemoryPoolIterator).Peek(), -1);
Assert.Equal(default(MemoryPoolIterator).Seek(byteCr), -1);
Assert.Equal(default(MemoryPoolIterator).Seek(byteCr, ref end), -1);
Assert.Equal(default(MemoryPoolIterator).Seek(byteCr, byteCr), -1);
Assert.Equal(default(MemoryPoolIterator).Seek(byteCr, byteCr, byteCr), -1);
default(MemoryPoolIterator).CopyFrom(default(ArraySegment<byte>)); default(MemoryPoolIterator).CopyFrom(default(ArraySegment<byte>));
default(MemoryPoolIterator).CopyFromAscii(""); default(MemoryPoolIterator).CopyFromAscii("");
Assert.ThrowsAny<InvalidOperationException>(() => default(MemoryPoolIterator).Put(byteCr)); Assert.ThrowsAny<InvalidOperationException>(() => default(MemoryPoolIterator).Put(byteCr));
Assert.ThrowsAny<InvalidOperationException>(() => default(MemoryPoolIterator).GetLength(end)); Assert.ThrowsAny<InvalidOperationException>(() => default(MemoryPoolIterator).GetLength(end));
Assert.ThrowsAny<InvalidOperationException>(() => default(MemoryPoolIterator).Skip(1));
}
[Fact]
public void TestGetArraySegment()
{
MemoryPoolBlock block0 = null;
MemoryPoolBlock block1 = null;
var byteRange = Enumerable.Range(1, 127).Select(x => (byte)x).ToArray();
try
{
// Arrange
block0 = _pool.Lease();
block1 = _pool.Lease();
block0.GetIterator().CopyFrom(byteRange);
block1.GetIterator().CopyFrom(byteRange);
block0.Next = block1;
var begin = block0.GetIterator();
var end0 = begin;
var end1 = begin;
end0.Skip(byteRange.Length);
end1.Skip(byteRange.Length * 2);
// Act
var as0 = begin.GetArraySegment(end0);
var as1 = begin.GetArraySegment(end1);
// Assert
Assert.Equal(as0.Count, byteRange.Length);
Assert.Equal(as1.Count, byteRange.Length * 2);
for (var i = 1; i < byteRange.Length; i++)
{
var asb0 = as0.Array[i + as0.Offset];
var asb1 = as1.Array[i + as1.Offset];
var b = byteRange[i];
Assert.Equal(asb0, b);
Assert.Equal(asb1, b);
}
for (var i = 1 + byteRange.Length; i < byteRange.Length * 2; i++)
{
var asb1 = as1.Array[i + as1.Offset];
var b = byteRange[i - byteRange.Length];
Assert.Equal(asb1, b);
}
}
finally
{
if (block0 != null) _pool.Return(block0);
if (block1 != null) _pool.Return(block1);
}
}
[Fact]
public void TestTake()
{
MemoryPoolBlock block0 = null;
MemoryPoolBlock block1 = null;
MemoryPoolBlock block2 = null;
MemoryPoolBlock emptyBlock0 = null;
MemoryPoolBlock emptyBlock1 = null;
var byteRange = Enumerable.Range(1, 127).Select(x => (byte)x).ToArray();
try
{
// Arrange
block0 = _pool.Lease();
block1 = _pool.Lease();
block2 = _pool.Lease();
emptyBlock0 = _pool.Lease();
emptyBlock1 = _pool.Lease();
block0.GetIterator().CopyFrom(byteRange);
block1.GetIterator().CopyFrom(byteRange);
block2.GetIterator().CopyFrom(byteRange);
var begin = block0.GetIterator();
// Single block
for (var i = 0; i < byteRange.Length; i++)
{
var t = begin.Take();
var b = byteRange[i];
Assert.Equal(t, b);
}
Assert.Equal(begin.Take(), -1);
// Dual block
block0.Next = block1;
begin = block0.GetIterator();
for (var block = 0; block < 2; block++)
{
for (var i = 0; i < byteRange.Length; i++)
{
var t = begin.Take();
var b = byteRange[i];
Assert.Equal(t, b);
}
}
Assert.Equal(begin.Take(), -1);
// Multi block
block1.Next = emptyBlock0;
emptyBlock0.Next = emptyBlock1;
emptyBlock1.Next = block2;
begin = block0.GetIterator();
for (var block = 0; block < 3; block++)
{
for (var i = 0; i < byteRange.Length; i++)
{
var t = begin.Take();
var b = byteRange[i];
Assert.Equal(t, b);
}
}
Assert.Equal(begin.Take(), -1);
}
finally
{
if (block0 != null) _pool.Return(block0);
if (block1 != null) _pool.Return(block1);
if (block2 != null) _pool.Return(block2);
if (emptyBlock0 != null) _pool.Return(emptyBlock0);
if (emptyBlock1 != null) _pool.Return(emptyBlock1);
}
}
[Fact]
public void TestTakeEmptyBlocks()
{
MemoryPoolBlock emptyBlock0 = null;
MemoryPoolBlock emptyBlock1 = null;
MemoryPoolBlock emptyBlock2 = null;
try
{
// Arrange
emptyBlock0 = _pool.Lease();
emptyBlock1 = _pool.Lease();
emptyBlock2 = _pool.Lease();
var beginEmpty = emptyBlock0.GetIterator();
// Assert
// No blocks
Assert.Equal(default(MemoryPoolIterator).Take(), -1);
// Single empty block
Assert.Equal(beginEmpty.Take(), -1);
// Dual empty block
emptyBlock0.Next = emptyBlock1;
beginEmpty = emptyBlock0.GetIterator();
Assert.Equal(beginEmpty.Take(), -1);
// Multi empty block
emptyBlock1.Next = emptyBlock2;
beginEmpty = emptyBlock0.GetIterator();
Assert.Equal(beginEmpty.Take(), -1);
}
finally
{
if (emptyBlock0 != null) _pool.Return(emptyBlock0);
if (emptyBlock1 != null) _pool.Return(emptyBlock1);
if (emptyBlock2 != null) _pool.Return(emptyBlock2);
}
} }
[Theory] [Theory]
@ -973,23 +191,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[InlineData("abcde", "abcde", 6)] [InlineData("abcde", "abcde", 6)]
public void TestGetAsciiStringEscaped(string input, string expected, int maxChars) public void TestGetAsciiStringEscaped(string input, string expected, int maxChars)
{ {
MemoryPoolBlock block = null;
try
{
// Arrange // Arrange
var buffer = new Span<byte>(Encoding.ASCII.GetBytes(input)); var buffer = new Span<byte>(Encoding.ASCII.GetBytes(input));
// Act // Act
var result = buffer.GetAsciiStringEscaped(maxChars); var result = buffer.GetAsciiStringEscaped(maxChars);
// Assert // Assert
Assert.Equal(expected, result); Assert.Equal(expected, result);
}
finally
{
if (block != null) _pool.Return(block);
}
} }
[Fact] [Fact]

View File

@ -1,226 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests
{
public class UrlPathDecoderTests
{
[Fact]
public void Empty()
{
using (var pool = new MemoryPool())
{
var mem = pool.Lease();
PositiveAssert(mem, string.Empty, string.Empty);
pool.Return(mem);
}
}
[Fact]
public void WhiteSpace()
{
using (var pool = new MemoryPool())
{
var mem = pool.Lease();
PositiveAssert(mem, " ", " ");
pool.Return(mem);
}
}
[Theory]
[InlineData("/foo/bar", "/foo/bar")]
[InlineData("/foo/BAR", "/foo/BAR")]
[InlineData("/foo/", "/foo/")]
[InlineData("/", "/")]
public void NormalCases(string raw, string expect)
{
using (var pool = new MemoryPool())
{
var mem = pool.Lease();
PositiveAssert(mem, raw, expect);
pool.Return(mem);
}
}
[Theory]
[InlineData("%2F", "%2F")]
[InlineData("/foo%2Fbar", "/foo%2Fbar")]
[InlineData("/foo%2F%20bar", "/foo%2F bar")]
public void SkipForwardSlash(string raw, string expect)
{
using (var pool = new MemoryPool())
{
var mem = pool.Lease();
PositiveAssert(mem, raw, expect);
pool.Return(mem);
}
}
[Theory]
[InlineData("%D0%A4", "Ф")]
[InlineData("%d0%a4", "Ф")]
[InlineData("%E0%A4%AD", "भ")]
[InlineData("%e0%A4%Ad", "भ")]
[InlineData("%F0%A4%AD%A2", "𤭢")]
[InlineData("%F0%a4%Ad%a2", "𤭢")]
[InlineData("%48%65%6C%6C%6F%20%57%6F%72%6C%64", "Hello World")]
[InlineData("%48%65%6C%6C%6F%2D%C2%B5%40%C3%9F%C3%B6%C3%A4%C3%BC%C3%A0%C3%A1", "Hello-µ@ßöäüàá")]
// Test the borderline cases of overlong UTF8.
[InlineData("%C2%80", "\u0080")]
[InlineData("%E0%A0%80", "\u0800")]
[InlineData("%F0%90%80%80", "\U00010000")]
[InlineData("%63", "c")]
[InlineData("%32", "2")]
[InlineData("%20", " ")]
public void ValidUTF8(string raw, string expect)
{
using (var pool = new MemoryPool())
{
var mem = pool.Lease();
PositiveAssert(mem, raw, expect);
pool.Return(mem);
}
}
[Theory]
[InlineData("%C3%84ra%20Benetton", "Ära Benetton")]
[InlineData("%E6%88%91%E8%87%AA%E6%A8%AA%E5%88%80%E5%90%91%E5%A4%A9%E7%AC%91%E5%8E%BB%E7%95%99%E8%82%9D%E8%83%86%E4%B8%A4%E6%98%86%E4%BB%91", "我自横刀向天笑去留肝胆两昆仑")]
public void Internationalized(string raw, string expect)
{
using (var pool = new MemoryPool())
{
var mem = pool.Lease();
PositiveAssert(mem, raw, expect);
pool.Return(mem);
}
}
[Theory]
// Overlong ASCII
[InlineData("%C0%A4", "%C0%A4")]
[InlineData("%C1%BF", "%C1%BF")]
[InlineData("%E0%80%AF", "%E0%80%AF")]
[InlineData("%E0%9F%BF", "%E0%9F%BF")]
[InlineData("%F0%80%80%AF", "%F0%80%80%AF")]
[InlineData("%F0%8F%8F%BF", "%F0%8F%8F%BF")]
// Incomplete
[InlineData("%", "%")]
[InlineData("%%", "%%")]
[InlineData("%A", "%A")]
[InlineData("%Y", "%Y")]
// Mixed
[InlineData("%%32", "%2")]
[InlineData("%%20", "% ")]
[InlineData("%C0%A4%32", "%C0%A42")]
[InlineData("%32%C0%A4%32", "2%C0%A42")]
[InlineData("%C0%32%A4", "%C02%A4")]
public void InvalidUTF8(string raw, string expect)
{
using (var pool = new MemoryPool())
{
var mem = pool.Lease();
PositiveAssert(mem, raw, expect);
pool.Return(mem);
}
}
[Theory]
[InlineData("/foo%2Fbar", 10, "/foo%2Fbar", 10)]
[InlineData("/foo%2Fbar", 9, "/foo%2Fba", 9)]
[InlineData("/foo%2Fbar", 8, "/foo%2Fb", 8)]
[InlineData("%D0%A4", 6, "Ф", 1)]
[InlineData("%D0%A4", 5, "%D0%A", 5)]
[InlineData("%D0%A4", 4, "%D0%", 4)]
[InlineData("%D0%A4", 3, "%D0", 3)]
[InlineData("%D0%A4", 2, "%D", 2)]
[InlineData("%D0%A4", 1, "%", 1)]
[InlineData("%D0%A4", 0, "", 0)]
[InlineData("%C2%B5%40%C3%9F%C3%B6%C3%A4%C3%BC%C3%A0%C3%A1", 45, "µ@ßöäüàá", 8)]
[InlineData("%C2%B5%40%C3%9F%C3%B6%C3%A4%C3%BC%C3%A0%C3%A1", 44, "µ@ßöäüà%C3%A", 12)]
public void DecodeWithBoundary(string raw, int rawLength, string expect, int expectLength)
{
using (var pool = new MemoryPool())
{
var mem = pool.Lease();
var begin = BuildSample(mem, raw);
var end = GetIterator(begin, rawLength);
var end2 = UrlPathDecoder.Unescape(begin, end);
var result = begin.GetUtf8String(ref end2);
Assert.Equal(expectLength, result.Length);
Assert.Equal(expect, result);
pool.Return(mem);
}
}
private MemoryPoolIterator BuildSample(MemoryPoolBlock mem, string data)
{
var store = data.Select(c => (byte)c).ToArray();
mem.GetIterator().CopyFrom(new ArraySegment<byte>(store));
return mem.GetIterator();
}
private MemoryPoolIterator GetIterator(MemoryPoolIterator begin, int displacement)
{
var result = begin;
for (int i = 0; i < displacement; ++i)
{
result.Take();
}
return result;
}
private void PositiveAssert(MemoryPoolBlock mem, string raw, string expect)
{
var begin = BuildSample(mem, raw);
var end = GetIterator(begin, raw.Length);
var result = UrlPathDecoder.Unescape(begin, end);
Assert.Equal(expect, begin.GetUtf8String(ref result));
}
private void PositiveAssert(MemoryPoolBlock mem, string raw)
{
var begin = BuildSample(mem, raw);
var end = GetIterator(begin, raw.Length);
var result = UrlPathDecoder.Unescape(begin, end);
Assert.NotEqual(raw.Length, begin.GetUtf8String(ref result).Length);
}
private void NegativeAssert(MemoryPoolBlock mem, string raw)
{
var begin = BuildSample(mem, raw);
var end = GetIterator(begin, raw.Length);
var resultEnd = UrlPathDecoder.Unescape(begin, end);
var result = begin.GetUtf8String(ref resultEnd);
Assert.Equal(raw, result);
}
}
}

View File

@ -4,14 +4,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Server.Kestrel;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Testing namespace Microsoft.AspNetCore.Testing
{ {
public class HttpParsingData public class HttpParsingData
{ {
public static IEnumerable<string[]> ValidRequestLineData public static IEnumerable<string[]> RequestLineValidData
{ {
get get
{ {
@ -20,7 +19,7 @@ namespace Microsoft.AspNetCore.Testing
"GET", "GET",
"CUSTOM", "CUSTOM",
}; };
var targets = new[] var paths = new[]
{ {
Tuple.Create("/", "/"), Tuple.Create("/", "/"),
Tuple.Create("/abc", "/abc"), Tuple.Create("/abc", "/abc"),
@ -57,166 +56,154 @@ namespace Microsoft.AspNetCore.Testing
}; };
return from method in methods return from method in methods
from target in targets from path in paths
from queryString in queryStrings from queryString in queryStrings
from httpVersion in httpVersions from httpVersion in httpVersions
select new[] select new[]
{ {
$"{method} {target.Item1}{queryString} {httpVersion}\r\n", $"{method} {path.Item1}{queryString} {httpVersion}\r\n",
method, method,
$"{target.Item2}", $"{path.Item1}{queryString}",
$"{path.Item1}",
$"{path.Item2}",
queryString, queryString,
httpVersion httpVersion
}; };
} }
} }
// All these test cases must end in '\n', otherwise the server will spin forever public static IEnumerable<string> RequestLineIncompleteData => new[]
public static IEnumerable<object[]> InvalidRequestLineData
{ {
get "G",
{ "GE",
var invalidRequestLines = new[] "GET",
{ "GET ",
"G\r\n", "GET /",
"GE\r\n", "GET / ",
"GET\r\n", "GET / H",
"GET \r\n", "GET / HT",
"GET /\r\n", "GET / HTT",
"GET / \r\n", "GET / HTTP",
"GET/HTTP/1.1\r\n", "GET / HTTP/",
"GET /HTTP/1.1\r\n", "GET / HTTP/1",
" \r\n", "GET / HTTP/1.",
" \r\n", "GET / HTTP/1.1",
"/ HTTP/1.1\r\n", "GET / HTTP/1.1\r",
" / HTTP/1.1\r\n", };
"/ \r\n",
"GET \r\n",
"GET HTTP/1.0\r\n",
"GET HTTP/1.1\r\n",
"GET / \n",
"GET / HTTP/1.0\n",
"GET / HTTP/1.1\n",
"GET / HTTP/1.0\rA\n",
"GET / HTTP/1.1\ra\n",
"GET? / HTTP/1.1\r\n",
"GET ? HTTP/1.1\r\n",
"GET /a?b=cHTTP/1.1\r\n",
"GET /a%20bHTTP/1.1\r\n",
"GET /a%20b?c=dHTTP/1.1\r\n",
"GET %2F HTTP/1.1\r\n",
"GET %00 HTTP/1.1\r\n",
"CUSTOM \r\n",
"CUSTOM /\r\n",
"CUSTOM / \r\n",
"CUSTOM /HTTP/1.1\r\n",
"CUSTOM \r\n",
"CUSTOM HTTP/1.0\r\n",
"CUSTOM HTTP/1.1\r\n",
"CUSTOM / \n",
"CUSTOM / HTTP/1.0\n",
"CUSTOM / HTTP/1.1\n",
"CUSTOM / HTTP/1.0\rA\n",
"CUSTOM / HTTP/1.1\ra\n",
"CUSTOM ? HTTP/1.1\r\n",
"CUSTOM /a?b=cHTTP/1.1\r\n",
"CUSTOM /a%20bHTTP/1.1\r\n",
"CUSTOM /a%20b?c=dHTTP/1.1\r\n",
"CUSTOM %2F HTTP/1.1\r\n",
"CUSTOM %00 HTTP/1.1\r\n",
// Bad HTTP Methods (invalid according to RFC)
"( / HTTP/1.0\r\n",
") / HTTP/1.0\r\n",
"< / HTTP/1.0\r\n",
"> / HTTP/1.0\r\n",
"@ / HTTP/1.0\r\n",
", / HTTP/1.0\r\n",
"; / HTTP/1.0\r\n",
": / HTTP/1.0\r\n",
"\\ / HTTP/1.0\r\n",
"\" / HTTP/1.0\r\n",
"/ / HTTP/1.0\r\n",
"[ / HTTP/1.0\r\n",
"] / HTTP/1.0\r\n",
"? / HTTP/1.0\r\n",
"= / HTTP/1.0\r\n",
"{ / HTTP/1.0\r\n",
"} / HTTP/1.0\r\n",
"get@ / HTTP/1.0\r\n",
"post= / HTTP/1.0\r\n",
};
var encodedNullCharInTargetRequestLines = new[] public static IEnumerable<string> RequestLineInvalidData => new[]
{
"GET /%00 HTTP/1.1\r\n",
"GET /%00%00 HTTP/1.1\r\n",
"GET /%E8%00%84 HTTP/1.1\r\n",
"GET /%E8%85%00 HTTP/1.1\r\n",
"GET /%F3%00%82%86 HTTP/1.1\r\n",
"GET /%F3%85%00%82 HTTP/1.1\r\n",
"GET /%F3%85%82%00 HTTP/1.1\r\n",
"GET /%E8%85%00 HTTP/1.1\r\n",
"GET /%E8%01%00 HTTP/1.1\r\n",
};
var nullCharInTargetRequestLines = new[]
{
"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",
};
return invalidRequestLines.Select(requestLine => new object[]
{
requestLine,
typeof(BadHttpRequestException),
$"Invalid request line: {requestLine.Replace("\r", "<0x0D>").Replace("\n", "<0x0A>")}"
})
.Concat(encodedNullCharInTargetRequestLines.Select(requestLine => new object[]
{
requestLine,
typeof(InvalidOperationException),
$"The path contains null characters."
}))
.Concat(nullCharInTargetRequestLines.Select(requestLine => new object[]
{
requestLine,
typeof(InvalidOperationException),
new InvalidOperationException().Message
}));
}
}
public static TheoryData<string> UnrecognizedHttpVersionData
{ {
get "G\r\n",
{ "GE\r\n",
return new TheoryData<string> "GET\r\n",
{ "GET \r\n",
"H", "GET /\r\n",
"HT", "GET / \r\n",
"HTT", "GET/HTTP/1.1\r\n",
"HTTP", "GET /HTTP/1.1\r\n",
"HTTP/", " \r\n",
"HTTP/1", " \r\n",
"HTTP/1.", "/ HTTP/1.1\r\n",
"http/1.0", " / HTTP/1.1\r\n",
"http/1.1", "/ \r\n",
"HTTP/1.1 ", "GET \r\n",
"HTTP/1.0a", "GET HTTP/1.0\r\n",
"HTTP/1.0ab", "GET HTTP/1.1\r\n",
"HTTP/1.1a", "GET / \n",
"HTTP/1.1ab", "GET / HTTP/1.0\n",
"HTTP/1.2", "GET / HTTP/1.1\n",
"HTTP/3.0", "GET / HTTP/1.0\rA\n",
"hello", "GET / HTTP/1.1\ra\n",
"8charact", "GET? / HTTP/1.1\r\n",
}; "GET ? HTTP/1.1\r\n",
} "GET /a?b=cHTTP/1.1\r\n",
} "GET /a%20bHTTP/1.1\r\n",
"GET /a%20b?c=dHTTP/1.1\r\n",
"GET %2F HTTP/1.1\r\n",
"GET %00 HTTP/1.1\r\n",
"CUSTOM \r\n",
"CUSTOM /\r\n",
"CUSTOM / \r\n",
"CUSTOM /HTTP/1.1\r\n",
"CUSTOM \r\n",
"CUSTOM HTTP/1.0\r\n",
"CUSTOM HTTP/1.1\r\n",
"CUSTOM / \n",
"CUSTOM / HTTP/1.0\n",
"CUSTOM / HTTP/1.1\n",
"CUSTOM / HTTP/1.0\rA\n",
"CUSTOM / HTTP/1.1\ra\n",
"CUSTOM ? HTTP/1.1\r\n",
"CUSTOM /a?b=cHTTP/1.1\r\n",
"CUSTOM /a%20bHTTP/1.1\r\n",
"CUSTOM /a%20b?c=dHTTP/1.1\r\n",
"CUSTOM %2F HTTP/1.1\r\n",
"CUSTOM %00 HTTP/1.1\r\n",
// Bad HTTP Methods (invalid according to RFC)
"( / HTTP/1.0\r\n",
") / HTTP/1.0\r\n",
"< / HTTP/1.0\r\n",
"> / HTTP/1.0\r\n",
"@ / HTTP/1.0\r\n",
", / HTTP/1.0\r\n",
"; / HTTP/1.0\r\n",
": / HTTP/1.0\r\n",
"\\ / HTTP/1.0\r\n",
"\" / HTTP/1.0\r\n",
"/ / HTTP/1.0\r\n",
"[ / HTTP/1.0\r\n",
"] / HTTP/1.0\r\n",
"? / HTTP/1.0\r\n",
"= / HTTP/1.0\r\n",
"{ / HTTP/1.0\r\n",
"} / HTTP/1.0\r\n",
"get@ / HTTP/1.0\r\n",
"post= / HTTP/1.0\r\n",
};
public static IEnumerable<object[]> InvalidRequestHeaderData public static IEnumerable<string> RequestLineWithEncodedNullCharInTargetData => new[]
{
"GET /%00 HTTP/1.1\r\n",
"GET /%00%00 HTTP/1.1\r\n",
"GET /%E8%00%84 HTTP/1.1\r\n",
"GET /%E8%85%00 HTTP/1.1\r\n",
"GET /%F3%00%82%86 HTTP/1.1\r\n",
"GET /%F3%85%00%82 HTTP/1.1\r\n",
"GET /%F3%85%82%00 HTTP/1.1\r\n",
"GET /%E8%01%00 HTTP/1.1\r\n",
};
public static IEnumerable<string> RequestLineWithNullCharInTargetData => new[]
{
"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",
};
public static TheoryData<string> UnrecognizedHttpVersionData => new TheoryData<string>
{
"H",
"HT",
"HTT",
"HTTP",
"HTTP/",
"HTTP/1",
"HTTP/1.",
"http/1.0",
"http/1.1",
"HTTP/1.1 ",
"HTTP/1.0a",
"HTTP/1.0ab",
"HTTP/1.1a",
"HTTP/1.1ab",
"HTTP/1.2",
"HTTP/3.0",
"hello",
"8charact",
};
public static IEnumerable<object[]> RequestHeaderInvalidData
{ {
get get
{ {
@ -245,6 +232,14 @@ namespace Microsoft.AspNetCore.Testing
"Header-1: value1\rHeader-2: value2\r\n\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: value2\r\r\n",
"Header-1: value1\r\nHeader-2: v\ralue2\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",
}; };
// Missing colon // Missing colon
@ -253,6 +248,7 @@ namespace Microsoft.AspNetCore.Testing
"Header-1 value1\r\n\r\n", "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",
"Header-1: value1\r\nHeader-2 value2\r\n\r\n", "Header-1: value1\r\nHeader-2 value2\r\n\r\n",
"\n"
}; };
// Starting with whitespace // Starting with whitespace
@ -269,9 +265,20 @@ namespace Microsoft.AspNetCore.Testing
{ {
"Header : value\r\n\r\n", "Header : value\r\n\r\n",
"Header\t: 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 : 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\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 : 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", "Header-1: value1\r\nHeader-2\t: value2\r\n\r\n",
@ -287,11 +294,11 @@ namespace Microsoft.AspNetCore.Testing
return new[] return new[]
{ {
Tuple.Create(headersWithLineFolding,"Header value line folding not supported."), Tuple.Create(headersWithLineFolding, "Whitespace is not allowed in header name."),
Tuple.Create(headersWithCRInValue,"Header value must not contain CR characters."), Tuple.Create(headersWithCRInValue, "Header value must not contain CR characters."),
Tuple.Create(headersWithMissingColon,"No ':' character found in header line."), Tuple.Create(headersWithMissingColon, "No ':' character found in header line."),
Tuple.Create(headersStartingWithWhitespace, "Header line must not start with whitespace."), Tuple.Create(headersStartingWithWhitespace, "Whitespace is not allowed in header name."),
Tuple.Create(headersWithWithspaceInName,"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.") Tuple.Create(headersNotEndingInCrLfLine, "Headers corrupted, invalid header sequence.")
} }
.SelectMany(t => t.Item1.Select(headers => new[] { headers, t.Item2 })); .SelectMany(t => t.Item1.Select(headers => new[] { headers, t.Item2 }));