Make TakeStartLine more robust (#683).

This commit is contained in:
Cesar Blum Silveira 2016-04-28 17:39:02 -07:00
parent 388841c1d8
commit 3186e1bd72
9 changed files with 397 additions and 173 deletions

View File

@ -35,6 +35,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
private static readonly byte[] _bytesContentLengthZero = Encoding.ASCII.GetBytes("\r\nContent-Length: 0"); private static readonly byte[] _bytesContentLengthZero = Encoding.ASCII.GetBytes("\r\nContent-Length: 0");
private static readonly byte[] _bytesSpace = Encoding.ASCII.GetBytes(" "); private static readonly byte[] _bytesSpace = Encoding.ASCII.GetBytes(" ");
private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n"); private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n");
private static readonly int _httpVersionLength = "HTTP/1.*".Length;
private static Vector<byte> _vectorCRs = new Vector<byte>((byte)'\r'); private static Vector<byte> _vectorCRs = new Vector<byte>((byte)'\r');
private static Vector<byte> _vectorColons = new Vector<byte>((byte)':'); private static Vector<byte> _vectorColons = new Vector<byte>((byte)':');
@ -45,7 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
private readonly object _onStartingSync = new Object(); private readonly object _onStartingSync = new Object();
private readonly object _onCompletedSync = new Object(); private readonly object _onCompletedSync = new Object();
protected bool _corruptedRequest = false; private bool _requestRejected;
private Headers _frameHeaders; private Headers _frameHeaders;
private Streams _frameStreams; private Streams _frameStreams;
@ -60,7 +61,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
protected CancellationTokenSource _abortedCts; protected CancellationTokenSource _abortedCts;
protected CancellationToken? _manuallySetRequestAbortToken; protected CancellationToken? _manuallySetRequestAbortToken;
protected bool _responseStarted; protected RequestProcessingStatus _requestProcessingStatus;
protected bool _keepAlive; protected bool _keepAlive;
private bool _autoChunk; private bool _autoChunk;
protected Exception _applicationException; protected Exception _applicationException;
@ -96,7 +97,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
{ {
return "HTTP/1.0"; return "HTTP/1.0";
} }
return ""; return string.Empty;
} }
set set
{ {
@ -167,9 +168,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
return cts; return cts;
} }
} }
public bool HasResponseStarted public bool HasResponseStarted
{ {
get { return _responseStarted; } get { return _requestProcessingStatus == RequestProcessingStatus.ResponseStarted; }
} }
protected FrameRequestHeaders FrameRequestHeaders => _frameHeaders.RequestHeaders; protected FrameRequestHeaders FrameRequestHeaders => _frameHeaders.RequestHeaders;
@ -216,7 +218,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
_onStarting = null; _onStarting = null;
_onCompleted = null; _onCompleted = null;
_responseStarted = false; _requestProcessingStatus = RequestProcessingStatus.RequestPending;
_keepAlive = false; _keepAlive = false;
_autoChunk = false; _autoChunk = false;
_applicationException = null; _applicationException = null;
@ -446,7 +448,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public Task WriteAsync(ArraySegment<byte> data, CancellationToken cancellationToken) public Task WriteAsync(ArraySegment<byte> data, CancellationToken cancellationToken)
{ {
if (!_responseStarted) if (!HasResponseStarted)
{ {
return WriteAsyncAwaited(data, cancellationToken); return WriteAsyncAwaited(data, cancellationToken);
} }
@ -506,7 +508,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public void ProduceContinue() public void ProduceContinue()
{ {
if (_responseStarted) return; if (HasResponseStarted)
{
return;
}
StringValues expect; StringValues expect;
if (_httpVersion == HttpVersionType.Http1_1 && if (_httpVersion == HttpVersionType.Http1_1 &&
@ -519,7 +524,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public Task ProduceStartAndFireOnStarting() public Task ProduceStartAndFireOnStarting()
{ {
if (_responseStarted) return TaskUtilities.CompletedTask; if (HasResponseStarted)
{
return TaskUtilities.CompletedTask;
}
if (_onStarting != null) if (_onStarting != null)
{ {
@ -554,30 +562,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
private void ProduceStart(bool appCompleted) private void ProduceStart(bool appCompleted)
{ {
if (_responseStarted) return; if (HasResponseStarted)
_responseStarted = true; {
return;
}
_requestProcessingStatus = RequestProcessingStatus.ResponseStarted;
var statusBytes = ReasonPhrases.ToStatusBytes(StatusCode, ReasonPhrase); var statusBytes = ReasonPhrases.ToStatusBytes(StatusCode, ReasonPhrase);
CreateResponseHeader(statusBytes, appCompleted); CreateResponseHeader(statusBytes, appCompleted);
} }
protected Task TryProduceInvalidRequestResponse()
{
if (_requestProcessingStatus == RequestProcessingStatus.RequestStarted && _requestRejected)
{
if (_frameHeaders == null)
{
InitializeHeaders();
}
return ProduceEnd();
}
return TaskUtilities.CompletedTask;
}
protected Task ProduceEnd() protected Task ProduceEnd()
{ {
if (_corruptedRequest || _applicationException != null) if (_requestRejected || _applicationException != null)
{ {
if (_corruptedRequest) if (_requestRejected)
{ {
// 400 Bad Request // 400 Bad Request
StatusCode = 400; StatusCode = 400;
} }
else else
{ {
// 500 Internal Server Error // 500 Internal Server Error
StatusCode = 500; StatusCode = 500;
} }
if (_responseStarted) if (HasResponseStarted)
{ {
// We can no longer respond with a 500, so we simply close the connection. // We can no longer respond with a 500, so we simply close the connection.
_requestProcessingStopping = true; _requestProcessingStopping = true;
@ -601,7 +628,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
} }
} }
if (!_responseStarted) if (!HasResponseStarted)
{ {
return ProduceEndAwaited(); return ProduceEndAwaited();
} }
@ -709,31 +736,50 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
SocketOutput.ProducingComplete(end); SocketOutput.ProducingComplete(end);
} }
protected bool TakeStartLine(SocketInput input) protected RequestLineStatus TakeStartLine(SocketInput input)
{ {
var scan = input.ConsumingStart(); var scan = input.ConsumingStart();
var consumed = scan; var consumed = scan;
try try
{ {
// We may hit this when the client has stopped sending data but
// the connection hasn't closed yet, and therefore Frame.Stop()
// hasn't been called yet.
if (scan.Peek() == -1)
{
return RequestLineStatus.Empty;
}
_requestProcessingStatus = RequestProcessingStatus.RequestStarted;
string method; string method;
var begin = scan; var begin = scan;
if (!begin.GetKnownMethod(ref scan, out method)) if (!begin.GetKnownMethod(out method))
{ {
if (scan.Seek(ref _vectorSpaces) == -1) if (scan.Seek(ref _vectorSpaces) == -1)
{ {
return false; return RequestLineStatus.MethodIncomplete;
} }
method = begin.GetAsciiString(scan); method = begin.GetAsciiString(scan);
scan.Take(); if (method == null)
{
RejectRequest("Missing method.");
}
}
else
{
scan.Skip(method.Length);
} }
scan.Take();
begin = scan; begin = scan;
var needDecode = false; var needDecode = false;
var chFound = scan.Seek(ref _vectorSpaces, ref _vectorQuestionMarks, ref _vectorPercentages); var chFound = scan.Seek(ref _vectorSpaces, ref _vectorQuestionMarks, ref _vectorPercentages);
if (chFound == -1) if (chFound == -1)
{ {
return false; return RequestLineStatus.TargetIncomplete;
} }
else if (chFound == '%') else if (chFound == '%')
{ {
@ -741,7 +787,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
chFound = scan.Seek(ref _vectorSpaces, ref _vectorQuestionMarks); chFound = scan.Seek(ref _vectorSpaces, ref _vectorQuestionMarks);
if (chFound == -1) if (chFound == -1)
{ {
return false; return RequestLineStatus.TargetIncomplete;
} }
} }
@ -752,35 +798,61 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
if (chFound == '?') if (chFound == '?')
{ {
begin = scan; begin = scan;
if (scan.Seek(ref _vectorSpaces) != ' ') if (scan.Seek(ref _vectorSpaces) == -1)
{ {
return false; return RequestLineStatus.TargetIncomplete;
} }
queryString = begin.GetAsciiString(scan); queryString = begin.GetAsciiString(scan);
} }
if (pathBegin.Peek() == ' ')
{
RejectRequest("Missing request target.");
}
scan.Take(); scan.Take();
begin = scan; begin = scan;
if (scan.Seek(ref _vectorCRs) == -1)
{
return RequestLineStatus.VersionIncomplete;
}
string httpVersion; string httpVersion;
if (!begin.GetKnownVersion(ref scan, out httpVersion)) if (!begin.GetKnownVersion(out httpVersion))
{ {
scan = begin; // A slower fallback is necessary since the iterator's PeekLong() method
if (scan.Seek(ref _vectorCRs) == -1) // used in GetKnownVersion() only examines two memory blocks at most.
{ // Although unlikely, it is possible that the 8 bytes forming the version
return false; // could be spread out on more than two blocks, if the connection
} // happens to be unusually slow.
httpVersion = begin.GetAsciiString(scan); httpVersion = begin.GetAsciiString(scan);
scan.Take(); if (httpVersion == null)
} {
if (scan.Take() != '\n') RejectRequest("Missing HTTP version.");
{ }
return false; else if (httpVersion != "HTTP/1.0" && httpVersion != "HTTP/1.1")
{
RejectRequest("Malformed request.");
}
} }
// URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11 // HttpVersion must be set here to send correct response when request is rejected
// Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8; HttpVersion = httpVersion;
scan.Take();
var next = scan.Take();
if (next == -1)
{
return RequestLineStatus.Incomplete;
}
else if (next != '\n')
{
RejectRequest("Missing LF in request line.");
}
// URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11
// Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8;
// then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs" // then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
string requestUrlPath; string requestUrlPath;
if (needDecode) if (needDecode)
@ -802,7 +874,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
Method = method; Method = method;
RequestUri = requestUrlPath; RequestUri = requestUrlPath;
QueryString = queryString; QueryString = queryString;
HttpVersion = httpVersion;
bool caseMatches; bool caseMatches;
@ -818,7 +889,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
Path = requestUrlPath; Path = requestUrlPath;
} }
return true; return RequestLineStatus.Done;
} }
finally finally
{ {
@ -881,7 +952,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
return true; return true;
} }
ReportCorruptedHttpRequest(new BadHttpRequestException("Headers corrupted, invalid header sequence.")); RejectRequest("Headers corrupted, invalid header sequence.");
// Headers corrupted, parsing headers is complete // Headers corrupted, parsing headers is complete
return true; return true;
} }
@ -994,10 +1065,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
statusCode != 304; statusCode != 304;
} }
public void ReportCorruptedHttpRequest(BadHttpRequestException ex) public void RejectRequest(string message)
{ {
_corruptedRequest = true; _requestProcessingStopping = true;
_requestRejected = true;
var ex = new BadHttpRequestException(message);
Log.ConnectionBadRequest(ConnectionId, ex); Log.ConnectionBadRequest(ConnectionId, ex);
throw ex;
} }
protected void ReportApplicationError(Exception ex) protected void ReportApplicationError(Exception ex)
@ -1024,5 +1098,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
Http1_0 = 0, Http1_0 = 0,
Http1_1 = 1 Http1_1 = 1
} }
protected enum RequestLineStatus
{
Empty,
MethodIncomplete,
TargetIncomplete,
VersionIncomplete,
Incomplete,
Done
}
protected enum RequestProcessingStatus
{
RequestPending,
RequestStarted,
ResponseStarted
}
} }
} }

View File

@ -5,7 +5,6 @@ using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Server.Kestrel.Exceptions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Http namespace Microsoft.AspNetCore.Server.Kestrel.Http
@ -33,7 +32,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
{ {
while (!_requestProcessingStopping) while (!_requestProcessingStopping)
{ {
while (!_requestProcessingStopping && !TakeStartLine(SocketInput)) while (!_requestProcessingStopping && TakeStartLine(SocketInput) != RequestLineStatus.Done)
{ {
if (SocketInput.RemoteIntakeFin) if (SocketInput.RemoteIntakeFin)
{ {
@ -41,12 +40,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
// SocketInput.RemoteIntakeFin is set to true to ensure we don't close a // SocketInput.RemoteIntakeFin is set to true to ensure we don't close a
// connection without giving the application a chance to respond to a request // connection without giving the application a chance to respond to a request
// sent immediately before the a FIN from the client. // sent immediately before the a FIN from the client.
if (TakeStartLine(SocketInput)) var requestLineStatus = TakeStartLine(SocketInput);
if (requestLineStatus == RequestLineStatus.Empty)
{ {
break; return;
} }
return; if (requestLineStatus != RequestLineStatus.Done)
{
RejectRequest($"Malformed request: {requestLineStatus}");
}
break;
} }
await SocketInput; await SocketInput;
@ -62,12 +68,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
// SocketInput.RemoteIntakeFin is set to true to ensure we don't close a // SocketInput.RemoteIntakeFin is set to true to ensure we don't close a
// connection without giving the application a chance to respond to a request // connection without giving the application a chance to respond to a request
// sent immediately before the a FIN from the client. // sent immediately before the a FIN from the client.
if (TakeMessageHeaders(SocketInput, FrameRequestHeaders)) if (!TakeMessageHeaders(SocketInput, FrameRequestHeaders))
{ {
break; RejectRequest($"Malformed request: invalid headers.");
} }
return; break;
} }
await SocketInput; await SocketInput;
@ -83,66 +89,55 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
_abortedCts = null; _abortedCts = null;
_manuallySetRequestAbortToken = null; _manuallySetRequestAbortToken = null;
if (!_corruptedRequest) var context = _application.CreateContext(this);
try
{ {
var context = _application.CreateContext(this); await _application.ProcessRequestAsync(context).ConfigureAwait(false);
try }
catch (Exception ex)
{
ReportApplicationError(ex);
}
finally
{
// Trigger OnStarting if it hasn't been called yet and the app hasn't
// already failed. If an OnStarting callback throws we can go through
// our normal error handling in ProduceEnd.
// https://github.com/aspnet/KestrelHttpServer/issues/43
if (!HasResponseStarted && _applicationException == null && _onStarting != null)
{ {
await _application.ProcessRequestAsync(context).ConfigureAwait(false); await FireOnStarting();
}
catch (Exception ex)
{
ReportApplicationError(ex);
}
finally
{
// Trigger OnStarting if it hasn't been called yet and the app hasn't
// already failed. If an OnStarting callback throws we can go through
// our normal error handling in ProduceEnd.
// https://github.com/aspnet/KestrelHttpServer/issues/43
if (!_responseStarted && _applicationException == null && _onStarting != null)
{
await FireOnStarting();
}
PauseStreams();
if (_onCompleted != null)
{
await FireOnCompleted();
}
_application.DisposeContext(context, _applicationException);
} }
// If _requestAbort is set, the connection has already been closed. PauseStreams();
if (Volatile.Read(ref _requestAborted) == 0)
if (_onCompleted != null)
{ {
ResumeStreams(); await FireOnCompleted();
if (_keepAlive && !_corruptedRequest)
{
try
{
// Finish reading the request body in case the app did not.
await messageBody.Consume();
}
catch (BadHttpRequestException ex)
{
ReportCorruptedHttpRequest(ex);
}
}
await ProduceEnd();
} }
StopStreams(); _application.DisposeContext(context, _applicationException);
} }
if (!_keepAlive || _corruptedRequest) // If _requestAbort is set, the connection has already been closed.
if (Volatile.Read(ref _requestAborted) == 0)
{ {
// End the connection for non keep alive and Bad Requests ResumeStreams();
// as data incoming may have been thrown off
if (_keepAlive)
{
// Finish reading the request body in case the app did not.
await messageBody.Consume();
}
await ProduceEnd();
}
StopStreams();
if (!_keepAlive)
{
// End the connection for non keep alive as data incoming may have been thrown off
return; return;
} }
} }
@ -158,6 +153,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
{ {
try try
{ {
await TryProduceInvalidRequestResponse();
ResetComponents(); ResetComponents();
_abortedCts = null; _abortedCts = null;

View File

@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
return ConsumeAwaited(result.AsTask(), cancellationToken); return ConsumeAwaited(result.AsTask(), cancellationToken);
} }
// ValueTask uses .GetAwaiter().GetResult() if necessary // ValueTask uses .GetAwaiter().GetResult() if necessary
else if (result.Result == 0) else if (result.Result == 0)
{ {
// Completed Task, end of stream // Completed Task, end of stream
return TaskUtilities.CompletedTask; return TaskUtilities.CompletedTask;
@ -125,8 +125,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
long contentLength; long contentLength;
if (!long.TryParse(unparsedContentLength, out contentLength) || contentLength < 0) if (!long.TryParse(unparsedContentLength, out contentLength) || contentLength < 0)
{ {
context.ReportCorruptedHttpRequest(new BadHttpRequestException("Invalid content length.")); context.RejectRequest($"Invalid content length: {unparsedContentLength}");
return new ForContentLength(keepAlive, 0, context);
} }
else else
{ {
@ -142,15 +141,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
return new ForRemainingData(context); return new ForRemainingData(context);
} }
private int ThrowBadRequestException(string message)
{
// returns int so can be used as item non-void function
var ex = new BadHttpRequestException(message);
_context.ReportCorruptedHttpRequest(ex);
throw ex;
}
private class ForRemainingData : MessageBody private class ForRemainingData : MessageBody
{ {
public ForRemainingData(Frame context) public ForRemainingData(Frame context)
@ -197,7 +187,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
_inputLength -= actual; _inputLength -= actual;
if (actual == 0) if (actual == 0)
{ {
ThrowBadRequestException("Unexpected end of request content"); _context.RejectRequest("Unexpected end of request content");
} }
return actual; return actual;
} }
@ -213,7 +203,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
_inputLength -= actual; _inputLength -= actual;
if (actual == 0) if (actual == 0)
{ {
ThrowBadRequestException("Unexpected end of request content"); _context.RejectRequest("Unexpected end of request content");
} }
return actual; return actual;
@ -514,7 +504,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
} }
else else
{ {
ThrowBadRequestException("Bad chunk suffix"); _context.RejectRequest("Bad chunk suffix");
} }
} }
finally finally
@ -568,16 +558,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
{ {
return currentParsedSize * 0x10 + (extraHexDigit - ('a' - 10)); return currentParsedSize * 0x10 + (extraHexDigit - ('a' - 10));
} }
else
{
return ThrowBadRequestException("Bad chunk size data");
}
} }
_context.RejectRequest("Bad chunk size data");
return -1; // can't happen, but compiler complains
} }
private void ThrowChunkedRequestIncomplete() private void ThrowChunkedRequestIncomplete()
{ {
ThrowBadRequestException("Chunked request incomplete"); _context.RejectRequest("Chunked request incomplete");
} }
private enum Mode private enum Mode

View File

@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
{ {
if (wasLastBlock) if (wasLastBlock)
{ {
return; throw new InvalidOperationException("Attempted to skip more bytes than available.");
} }
else else
{ {
@ -248,7 +248,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
while (following > 0) while (following > 0)
{ {
// Need unit tests to test Vector path // Need unit tests to test Vector path
#if !DEBUG #if !DEBUG
// Check will be Jitted away https://github.com/dotnet/coreclr/issues/1079 // Check will be Jitted away https://github.com/dotnet/coreclr/issues/1079
if (Vector.IsHardwareAccelerated) if (Vector.IsHardwareAccelerated)
{ {
@ -269,7 +269,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
return byte0; return byte0;
} }
// Need unit tests to test Vector path // Need unit tests to test Vector path
#if !DEBUG #if !DEBUG
} }
#endif #endif
@ -330,7 +330,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
{ {
// Need unit tests to test Vector path // Need unit tests to test Vector path
#if !DEBUG #if !DEBUG
// Check will be Jitted away https://github.com/dotnet/coreclr/issues/1079 // Check will be Jitted away https://github.com/dotnet/coreclr/issues/1079
if (Vector.IsHardwareAccelerated) if (Vector.IsHardwareAccelerated)
{ {
@ -369,7 +369,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
return byte1; return byte1;
} }
// Need unit tests to test Vector path // Need unit tests to test Vector path
#if !DEBUG #if !DEBUG
} }
#endif #endif
var pCurrent = (block.DataFixedPtr + index); var pCurrent = (block.DataFixedPtr + index);
@ -436,7 +436,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
while (following > 0) while (following > 0)
{ {
// Need unit tests to test Vector path // Need unit tests to test Vector path
#if !DEBUG #if !DEBUG
// Check will be Jitted away https://github.com/dotnet/coreclr/issues/1079 // Check will be Jitted away https://github.com/dotnet/coreclr/issues/1079
if (Vector.IsHardwareAccelerated) if (Vector.IsHardwareAccelerated)
{ {
@ -502,7 +502,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
return toReturn; return toReturn;
} }
// Need unit tests to test Vector path // Need unit tests to test Vector path
#if !DEBUG #if !DEBUG
} }
#endif #endif
var pCurrent = (block.DataFixedPtr + index); var pCurrent = (block.DataFixedPtr + index);

View File

@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
private readonly static long _http10VersionLong = GetAsciiStringAsLong("HTTP/1.0"); private readonly static long _http10VersionLong = GetAsciiStringAsLong("HTTP/1.0");
private readonly static long _http11VersionLong = GetAsciiStringAsLong("HTTP/1.1"); private readonly static long _http11VersionLong = GetAsciiStringAsLong("HTTP/1.1");
private readonly static long _mask8Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }); private readonly static long _mask8Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff });
private readonly static long _mask7Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00 }); private readonly static long _mask7Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00 });
private readonly static long _mask6Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00 }); private readonly static long _mask6Chars = GetMaskAsLong(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00 });
@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
return null; return null;
} }
// Bytes out of the range of ascii are treated as "opaque data" // Bytes out of the range of ascii are treated as "opaque data"
// and kept in string as a char value that casts to same input byte value // and kept in string as a char value that casts to same input byte value
// https://tools.ietf.org/html/rfc7230#section-3.2.4 // https://tools.ietf.org/html/rfc7230#section-3.2.4
@ -283,16 +283,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
/// <remarks> /// <remarks>
/// A "known HTTP method" can be an HTTP method name defined in the HTTP/1.1 RFC. /// A "known HTTP method" can be an HTTP method name defined in the HTTP/1.1 RFC.
/// Since all of those fit in at most 8 bytes, they can be optimally looked up by reading those bytes as a long. Once /// Since all of those fit in at most 8 bytes, they can be optimally looked up by reading those bytes as a long. Once
/// in that format, it can be checked against the known method. /// in that format, it can be checked against the known method.
/// The Known Methods (CONNECT, DELETE, GET, HEAD, PATCH, POST, PUT, OPTIONS, TRACE) are all less than 8 bytes /// The Known Methods (CONNECT, DELETE, GET, HEAD, PATCH, POST, PUT, OPTIONS, TRACE) are all less than 8 bytes
/// and will be compared with the required space. A mask is used if the Known method is less than 8 bytes. /// and will be compared with the required space. A mask is used if the Known method is less than 8 bytes.
/// To optimize performance the GET method will be checked first. /// To optimize performance the GET method will be checked first.
/// </remarks> /// </remarks>
/// <param name="begin">The iterator from which to start the known string lookup.</param> /// <param name="begin">The iterator from which to start the known string lookup.</param>
/// <param name="scan">If we found a valid method, then scan will be updated to new position</param>
/// <param name="knownMethod">A reference to a pre-allocated known string, if the input matches any.</param> /// <param name="knownMethod">A reference to a pre-allocated known string, if the input matches any.</param>
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns> /// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
public static bool GetKnownMethod(this MemoryPoolIterator begin, ref MemoryPoolIterator scan, out string knownMethod) public static bool GetKnownMethod(this MemoryPoolIterator begin, out string knownMethod)
{ {
knownMethod = null; knownMethod = null;
var value = begin.PeekLong(); var value = begin.PeekLong();
@ -300,7 +299,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
if ((value & _mask4Chars) == _httpGetMethodLong) if ((value & _mask4Chars) == _httpGetMethodLong)
{ {
knownMethod = HttpGetMethod; knownMethod = HttpGetMethod;
scan.Skip(4);
return true; return true;
} }
foreach (var x in _knownMethods) foreach (var x in _knownMethods)
@ -308,7 +306,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
if ((value & x.Item1) == x.Item2) if ((value & x.Item1) == x.Item2)
{ {
knownMethod = x.Item3; knownMethod = x.Item3;
scan.Skip(knownMethod.Length + 1);
return true; return true;
} }
} }
@ -327,10 +324,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
/// To optimize performance the HTTP/1.1 will be checked first. /// To optimize performance the HTTP/1.1 will be checked first.
/// </remarks> /// </remarks>
/// <param name="begin">The iterator from which to start the known string lookup.</param> /// <param name="begin">The iterator from which to start the known string lookup.</param>
/// <param name="scan">If we found a valid method, then scan will be updated to new position</param>
/// <param name="knownVersion">A reference to a pre-allocated known string, if the input matches any.</param> /// <param name="knownVersion">A reference to a pre-allocated known string, if the input matches any.</param>
/// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns> /// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
public static bool GetKnownVersion(this MemoryPoolIterator begin, ref MemoryPoolIterator scan, out string knownVersion) public static bool GetKnownVersion(this MemoryPoolIterator begin, out string knownVersion)
{ {
knownVersion = null; knownVersion = null;
var value = begin.PeekLong(); var value = begin.PeekLong();
@ -338,24 +334,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
if (value == _http11VersionLong) if (value == _http11VersionLong)
{ {
knownVersion = Http11Version; knownVersion = Http11Version;
scan.Skip(8);
if (scan.Take() == '\r')
{
return true;
}
} }
else if (value == _http10VersionLong) else if (value == _http10VersionLong)
{ {
knownVersion = Http10Version; knownVersion = Http10Version;
scan.Skip(8); }
if (scan.Take() == '\r')
if (knownVersion != null)
{
begin.Skip(knownVersion.Length);
if (begin.Peek() != '\r')
{ {
return true; knownVersion = null;
} }
} }
knownVersion = null; return knownVersion != null;
return false;
} }
} }
} }

View File

@ -0,0 +1,72 @@
// 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.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests
{
public class BadHttpRequestTests
{
[Theory]
[InlineData("/ HTTP/1.1\r\n\r\n")]
[InlineData(" / HTTP/1.1\r\n\r\n")]
[InlineData(" / HTTP/1.1\r\n\r\n")]
[InlineData("GET / HTTP/1.1\r\n\r\n")]
[InlineData("GET / HTTP/1.1\r\n\r\n")]
[InlineData("GET HTTP/1.1\r\n\r\n")]
[InlineData("GET /")]
[InlineData("GET / ")]
[InlineData("GET / H")]
[InlineData("GET / HTTP/1.")]
[InlineData("GET /\r\n")]
[InlineData("GET / \r\n")]
[InlineData("GET / \n")]
[InlineData("GET / http/1.0\r\n\r\n")]
[InlineData("GET / http/1.1\r\n\r\n")]
[InlineData("GET / HTTP/1.1 \r\n\r\n")]
[InlineData("GET / HTTP/1.1a\r\n\r\n")]
[InlineData("GET / HTTP/1.0\n\r\n")]
[InlineData("GET / HTTP/3.0\r\n\r\n")]
[InlineData("GET / H\r\n\r\n")]
[InlineData("GET / HTTP/1.\r\n\r\n")]
[InlineData("GET / hello\r\n\r\n")]
[InlineData("GET / 8charact\r\n\r\n")]
public async Task TestBadRequests(string request)
{
using (var server = new TestServer(context => { return Task.FromResult(0); }))
{
using (var connection = new TestConnection(server.Port))
{
var receiveTask = Task.Run(async () =>
{
await connection.Receive(
"HTTP/1.0 400 Bad Request",
"");
await connection.ReceiveStartsWith("Date: ");
await connection.ReceiveForcedEnd(
"Content-Length: 0",
"Server: Kestrel",
"",
"");
});
try
{
await connection.SendEnd(request).ConfigureAwait(false);
}
catch (Exception)
{
// TestConnection.SendEnd will start throwing while sending characters
// in cases where the server rejects the request as soon as it
// determines the request line is malformed, even though there
// are more characters following.
}
await receiveTask;
}
}
}
}
}

View File

@ -217,7 +217,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
"POST / HTTP/1.1", "POST / HTTP/1.1",
"Transfer-Encoding: chunked", "Transfer-Encoding: chunked",
"", "",
"C", "C",
"HelloChunked", "HelloChunked",
"0", "0",
""}; ""};
@ -364,10 +364,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
using (var connection = new TestConnection(server.Port)) using (var connection = new TestConnection(server.Port))
{ {
await connection.Send( await connection.Send(
"POST / HTTP/1.1", "POST / HTTP/1.1",
"Transfer-Encoding: chunked", "Transfer-Encoding: chunked",
"", "",
"Cii"); "Cii");
await connection.Receive( await connection.Receive(
"HTTP/1.1 400 Bad Request", "HTTP/1.1 400 Bad Request",

View File

@ -728,18 +728,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
await connection.SendEnd( await connection.SendEnd(
"GET /"); "GET /");
await connection.ReceiveEnd(); await connection.Receive(
} "HTTP/1.0 400 Bad Request",
"");
using (var connection = new TestConnection(server.Port)) await connection.ReceiveStartsWith("Date:");
{ await connection.ReceiveForcedEnd(
await connection.SendEnd(
"GET / HTTP/1.1",
"",
"Post / HTTP/1.1");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
"Content-Length: 0", "Content-Length: 0",
"Server: Kestrel",
"", "",
""); "");
} }
@ -749,12 +744,40 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
await connection.SendEnd( await connection.SendEnd(
"GET / HTTP/1.1", "GET / HTTP/1.1",
"", "",
"Post / HTTP/1.1", "POST / HTTP/1.1");
"Content-Length: 7"); await connection.Receive(
await connection.ReceiveEnd(
"HTTP/1.1 200 OK", "HTTP/1.1 200 OK",
"Content-Length: 0", "Content-Length: 0",
"", "",
"HTTP/1.0 400 Bad Request",
"");
await connection.ReceiveStartsWith("Date:");
await connection.ReceiveForcedEnd(
"Content-Length: 0",
"Server: Kestrel",
"",
"");
}
using (var connection = new TestConnection(server.Port))
{
await connection.SendEnd(
"GET / HTTP/1.1",
"",
"POST / HTTP/1.1",
"Content-Length: 7");
await connection.Receive(
"HTTP/1.1 200 OK",
"Content-Length: 0",
"",
"HTTP/1.1 400 Bad Request",
"Connection: close",
"");
await connection.ReceiveStartsWith("Date:");
await connection.ReceiveForcedEnd(
"Content-Length: 0",
"Server: Kestrel",
"",
""); "");
} }
} }
@ -1017,7 +1040,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.True(registrationWh.Wait(1000)); Assert.True(registrationWh.Wait(1000));
} }
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionFilterData))]
public async Task NoErrorsLoggedWhenServerEndsConnectionBeforeClient(ServiceContext testContext) public async Task NoErrorsLoggedWhenServerEndsConnectionBeforeClient(ServiceContext testContext)
@ -1049,5 +1072,30 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Equal(0, testLogger.TotalErrorsLogged); Assert.Equal(0, testLogger.TotalErrorsLogged);
} }
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task NoResponseSentWhenConnectionIsClosedByServerBeforeClientFinishesSendingRequest(ServiceContext testContext)
{
var testLogger = new TestApplicationErrorLogger();
testContext.Log = new KestrelTrace(testLogger);
using (var server = new TestServer(httpContext =>
{
httpContext.Abort();
return Task.FromResult(0);
}, testContext))
{
using (var connection = new TestConnection(server.Port))
{
await connection.Send(
"POST / HTTP/1.0",
"Content-Length: 1",
"",
"");
await connection.ReceiveEnd();
}
}
}
} }
} }

View File

@ -285,6 +285,40 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
_pool.Return(nextBlock); _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 block = _pool.Lease();
block.End += 3;
var nextBlock = _pool.Lease();
nextBlock.End += 2;
block.Next = nextBlock;
var scan = block.GetIterator();
// Act/Assert
Assert.ThrowsAny<InvalidOperationException>(() => scan.Skip(8));
_pool.Return(block);
}
[Theory] [Theory]
[InlineData("CONNECT / HTTP/1.1", ' ', true, MemoryPoolIteratorExtensions.HttpConnectMethod)] [InlineData("CONNECT / HTTP/1.1", ' ', true, MemoryPoolIteratorExtensions.HttpConnectMethod)]
[InlineData("DELETE / HTTP/1.1", ' ', true, MemoryPoolIteratorExtensions.HttpDeleteMethod)] [InlineData("DELETE / HTTP/1.1", ' ', true, MemoryPoolIteratorExtensions.HttpDeleteMethod)]
@ -309,12 +343,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
var chars = input.ToCharArray().Select(c => (byte)c).ToArray(); var chars = input.ToCharArray().Select(c => (byte)c).ToArray();
Buffer.BlockCopy(chars, 0, block.Array, block.Start, chars.Length); Buffer.BlockCopy(chars, 0, block.Array, block.Start, chars.Length);
block.End += chars.Length; block.End += chars.Length;
var scan = block.GetIterator(); var begin = block.GetIterator();
var begin = scan;
string knownString; string knownString;
// Act // Act
var result = begin.GetKnownMethod(ref scan, out knownString); var result = begin.GetKnownMethod(out knownString);
// Assert // Assert
Assert.Equal(expectedResult, result); Assert.Equal(expectedResult, result);
@ -337,12 +370,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
var chars = input.ToCharArray().Select(c => (byte)c).ToArray(); var chars = input.ToCharArray().Select(c => (byte)c).ToArray();
Buffer.BlockCopy(chars, 0, block.Array, block.Start, chars.Length); Buffer.BlockCopy(chars, 0, block.Array, block.Start, chars.Length);
block.End += chars.Length; block.End += chars.Length;
var scan = block.GetIterator(); var begin = block.GetIterator();
var begin = scan;
string knownString; string knownString;
// Act // Act
var result = begin.GetKnownVersion(ref scan, out knownString); var result = begin.GetKnownVersion(out knownString);
// Assert // Assert
Assert.Equal(expectedResult, result); Assert.Equal(expectedResult, result);
Assert.Equal(expectedKnownString, knownString); Assert.Equal(expectedKnownString, knownString);