Use one SequenceReader+Rewind rather than 2 (#8076)

This commit is contained in:
Ben Adams 2019-06-07 21:15:27 +01:00 committed by Justin Kotalik
parent 3dc2be22c5
commit f10680a37a
8 changed files with 210 additions and 186 deletions

View File

@ -228,9 +228,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
public HttpParser() { }
public HttpParser(bool showErrorDetails) { }
bool Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpParser<TRequestHandler>.ParseHeaders(TRequestHandler handler, in System.Buffers.ReadOnlySequence<byte> buffer, out System.SequencePosition consumed, out System.SequencePosition examined, out int consumedBytes) { throw null; }
bool Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpParser<TRequestHandler>.ParseRequestLine(TRequestHandler handler, in System.Buffers.ReadOnlySequence<byte> buffer, out System.SequencePosition consumed, out System.SequencePosition examined) { throw null; }
public bool ParseHeaders(TRequestHandler handler, in System.Buffers.ReadOnlySequence<byte> buffer, out System.SequencePosition consumed, out System.SequencePosition examined, out int consumedBytes) { throw null; }
public bool ParseHeaders(TRequestHandler handler, ref System.Buffers.SequenceReader<byte> reader) { throw null; }
public bool ParseRequestLine(TRequestHandler handler, in System.Buffers.ReadOnlySequence<byte> buffer, out System.SequencePosition consumed, out System.SequencePosition examined) { throw null; }
}
public enum HttpScheme
@ -253,7 +252,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
public partial interface IHttpParser<TRequestHandler> where TRequestHandler : Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpHeadersHandler, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpRequestLineHandler
{
bool ParseHeaders(TRequestHandler handler, in System.Buffers.ReadOnlySequence<byte> buffer, out System.SequencePosition consumed, out System.SequencePosition examined, out int consumedBytes);
bool ParseHeaders(TRequestHandler handler, ref System.Buffers.SequenceReader<byte> reader);
bool ParseRequestLine(TRequestHandler handler, in System.Buffers.ReadOnlySequence<byte> buffer, out System.SequencePosition consumed, out System.SequencePosition examined);
}
public partial interface IHttpRequestLineHandler

View File

@ -189,32 +189,77 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return result;
}
public bool TakeMessageHeaders(ReadOnlySequence<byte> buffer, bool trailers, out SequencePosition consumed, out SequencePosition examined)
public bool TakeMessageHeaders(in ReadOnlySequence<byte> buffer, bool trailers, out SequencePosition consumed, out SequencePosition examined)
{
// Make sure the buffer is limited
bool overLength = false;
if (buffer.Length >= _remainingRequestHeadersBytesAllowed)
if (buffer.Length > _remainingRequestHeadersBytesAllowed)
{
buffer = buffer.Slice(buffer.Start, _remainingRequestHeadersBytesAllowed);
// If we sliced it means the current buffer bigger than what we're
// allowed to look at
overLength = true;
return TrimAndTakeMessageHeaders(buffer, trailers, out consumed, out examined);
}
var result = _parser.ParseHeaders(new Http1ParsingHandler(this, trailers), buffer, out consumed, out examined, out var consumedBytes);
_remainingRequestHeadersBytesAllowed -= consumedBytes;
var reader = new SequenceReader<byte>(buffer);
var result = false;
try
{
result = _parser.ParseHeaders(new Http1ParsingHandler(this, trailers), ref reader);
if (!result && overLength)
{
BadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize);
if (result)
{
TimeoutControl.CancelTimeout();
}
return result;
}
if (result)
finally
{
TimeoutControl.CancelTimeout();
consumed = reader.Position;
_remainingRequestHeadersBytesAllowed -= (int)reader.Consumed;
if (result)
{
examined = consumed;
}
else
{
examined = buffer.End;
}
}
return result;
bool TrimAndTakeMessageHeaders(in ReadOnlySequence<byte> buffer, bool trailers, out SequencePosition consumed, out SequencePosition examined)
{
var trimmedBuffer = buffer.Slice(buffer.Start, _remainingRequestHeadersBytesAllowed);
var reader = new SequenceReader<byte>(trimmedBuffer);
var result = false;
try
{
result = _parser.ParseHeaders(new Http1ParsingHandler(this, trailers), ref reader);
if (!result)
{
// We read the maximum allowed but didn't complete the headers.
BadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize);
}
TimeoutControl.CancelTimeout();
return result;
}
finally
{
consumed = reader.Position;
_remainingRequestHeadersBytesAllowed -= (int)reader.Consumed;
if (result)
{
examined = consumed;
}
else
{
examined = trimmedBuffer.End;
}
}
}
}
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)

View File

@ -5,6 +5,7 @@ using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
@ -174,149 +175,128 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod, pathEncoded);
}
public unsafe bool ParseHeaders(TRequestHandler handler, in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined, out int consumedBytes)
public unsafe bool ParseHeaders(TRequestHandler handler, ref SequenceReader<byte> reader)
{
consumed = buffer.Start;
examined = buffer.End;
consumedBytes = 0;
var bufferEnd = buffer.End;
var reader = new SequenceReader<byte>(buffer);
var start = default(SequenceReader<byte>);
var done = false;
try
while (!reader.End)
{
while (!reader.End)
var span = reader.UnreadSpan;
while (span.Length > 0)
{
var span = reader.CurrentSpan;
var remaining = span.Length - reader.CurrentSpanIndex;
var ch1 = (byte)0;
var ch2 = (byte)0;
var readAhead = 0;
fixed (byte* pBuffer = span)
// Fast path, we're still looking at the same span
if (span.Length >= 2)
{
while (remaining > 0)
ch1 = span[0];
ch2 = span[1];
}
else if (reader.TryRead(out ch1)) // Possibly split across spans
{
// Note if we read ahead by 1 or 2 bytes
readAhead = (reader.TryRead(out ch2)) ? 2 : 1;
}
if (ch1 == ByteCR)
{
// Check for final CRLF.
if (ch2 == ByteLF)
{
var index = reader.CurrentSpanIndex;
byte ch1;
byte ch2;
var readAhead = false;
var readSecond = true;
// Fast path, we're still looking at the same span
if (remaining >= 2)
// If we got 2 bytes from the span directly so skip ahead 2 so that
// the reader's state matches what we expect
if (readAhead == 0)
{
ch1 = pBuffer[index];
ch2 = pBuffer[index + 1];
}
else
{
// Store the reader before we look ahead 2 bytes (probably straddling
// spans)
start = reader;
// Possibly split across spans
reader.TryRead(out ch1);
readSecond = reader.TryRead(out ch2);
readAhead = true;
reader.Advance(2);
}
if (ch1 == ByteCR)
// Double CRLF found, so end of headers.
handler.OnHeadersComplete();
return true;
}
else if (readAhead == 1)
{
// Didn't read 2 bytes, reset the reader so we don't consume anything
reader.Rewind(1);
return false;
}
Debug.Assert(readAhead == 0 || readAhead == 2);
// Headers don't end in CRLF line.
BadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestHeadersNoCRLF);
}
var length = 0;
// We only need to look for the end if we didn't read ahead; otherwise there isn't enough in
// in the span to contain a header.
if (readAhead == 0)
{
length = span.IndexOf(ByteLF) + 1;
if (length > 0)
{
// Potentially found the end, or an invalid header.
fixed (byte* pHeader = span)
{
// Check for final CRLF.
if (!readSecond)
{
// 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 (!readAhead)
{
reader.Advance(2);
}
done = true;
handler.OnHeadersComplete();
return true;
}
// Headers don't end in CRLF line.
BadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestHeadersNoCRLF);
}
// We moved the reader so look ahead 2 bytes so reset both the reader
// and the index
if (readAhead)
{
reader = start;
index = reader.CurrentSpanIndex;
}
var endIndex = new Span<byte>(pBuffer + index, remaining).IndexOf(ByteLF);
var length = 0;
if (endIndex != -1)
{
length = endIndex + 1;
var pHeader = pBuffer + index;
TakeSingleHeader(pHeader, length, handler);
}
else
{
var current = reader.Position;
var currentSlice = buffer.Slice(current, bufferEnd);
var lineEndPosition = currentSlice.PositionOf(ByteLF);
// Split buffers
if (lineEndPosition == null)
{
// Not there
return false;
}
var lineEnd = lineEndPosition.Value;
// Make sure LF is included in lineEnd
lineEnd = buffer.GetPosition(1, lineEnd);
var headerSpan = buffer.Slice(current, lineEnd).ToSpan();
length = headerSpan.Length;
fixed (byte* pHeader = headerSpan)
{
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
// Read the header sucessfully, skip the reader forward past the header line.
reader.Advance(length);
remaining -= length;
span = span.Slice(length);
}
}
}
return false;
// End not found in current span
if (length <= 0)
{
// We moved the reader to look ahead 2 bytes so rewind the reader
if (readAhead > 0)
{
reader.Rewind(readAhead);
}
length = ParseMultiSpanHeader(handler, ref reader);
if (length < 0)
{
// Not there
return false;
}
reader.Advance(length);
// As we crossed spans set the current span to default
// so we move to the next span on the next iteration
span = default;
}
}
}
finally
return false;
}
private unsafe int ParseMultiSpanHeader(TRequestHandler handler, ref SequenceReader<byte> reader)
{
var buffer = reader.Sequence;
var currentSlice = buffer.Slice(reader.Position, reader.Remaining);
var lineEndPosition = currentSlice.PositionOf(ByteLF);
// Split buffers
if (lineEndPosition == null)
{
consumed = reader.Position;
consumedBytes = (int)reader.Consumed;
if (done)
{
examined = consumed;
}
// Not there
return -1;
}
var lineEnd = lineEndPosition.Value;
// Make sure LF is included in lineEnd
lineEnd = buffer.GetPosition(1, lineEnd);
var headerSpan = buffer.Slice(reader.Position, lineEnd).ToSpan();
var length = headerSpan.Length;
fixed (byte* pHeader = headerSpan)
{
TakeSingleHeader(pHeader, length, handler);
}
return length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -339,7 +319,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
if (index == length || sawWhitespace)
{
RejectRequestHeader(headerLine, length);
// Set to -1 to indicate invalid.
index = -1;
}
return index;
@ -352,17 +333,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
var valueEnd = length - 3;
var nameEnd = FindEndOfName(headerLine, length);
// Header name is empty
if (nameEnd == 0)
{
RejectRequestHeader(headerLine, length);
}
if (headerLine[valueEnd + 2] != ByteLF)
{
RejectRequestHeader(headerLine, length);
}
if (headerLine[valueEnd + 1] != ByteCR)
// Header name is empty, invalid, or doesn't end in CRLF
if (nameEnd <= 0 || headerLine[valueEnd + 2] != ByteLF || headerLine[valueEnd + 1] != ByteCR)
{
RejectRequestHeader(headerLine, length);
}

View File

@ -10,6 +10,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
bool ParseRequestLine(TRequestHandler handler, in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined);
bool ParseHeaders(TRequestHandler handler, in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined, out int consumedBytes);
bool ParseHeaders(TRequestHandler handler, ref SequenceReader<byte> reader);
}
}

View File

@ -182,7 +182,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(rawHeaders));
var requestHandler = new RequestHandler();
Assert.False(parser.ParseHeaders(requestHandler, buffer, out var consumed, out var examined, out var consumedBytes));
var reader = new SequenceReader<byte>(buffer);
Assert.False(parser.ParseHeaders(requestHandler, ref reader));
}
[Theory]
@ -207,11 +208,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(rawHeaders));
var requestHandler = new RequestHandler();
parser.ParseHeaders(requestHandler, buffer, out var consumed, out var examined, out var consumedBytes);
var reader = new SequenceReader<byte>(buffer);
Assert.Equal(buffer.Length, buffer.Slice(consumed).Length);
Assert.True(buffer.Slice(examined).IsEmpty);
Assert.Equal(0, consumedBytes);
Assert.False(parser.ParseHeaders(requestHandler, ref reader));
Assert.Equal(buffer.Length, buffer.Slice(reader.Consumed).Length);
Assert.Equal(0, reader.Consumed);
}
[Fact]
@ -297,18 +299,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
const string headerLine = "Header: value\r\n\r";
var buffer1 = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(headerLine));
var requestHandler = new RequestHandler();
Assert.False(parser.ParseHeaders(requestHandler, buffer1, out var consumed, out var examined, out var consumedBytes));
var reader1 = new SequenceReader<byte>(buffer1);
Assert.False(parser.ParseHeaders(requestHandler, ref reader1));
Assert.Equal(buffer1.GetPosition(headerLine.Length - 1), consumed);
Assert.Equal(buffer1.End, examined);
Assert.Equal(headerLine.Length - 1, consumedBytes);
Assert.Equal(buffer1.GetPosition(headerLine.Length - 1), reader1.Position);
Assert.Equal(headerLine.Length - 1, reader1.Consumed);
var buffer2 = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes("\r\n"));
Assert.True(parser.ParseHeaders(requestHandler, buffer2, out consumed, out examined, out consumedBytes));
var reader2 = new SequenceReader<byte>(buffer2);
Assert.True(parser.ParseHeaders(requestHandler, ref reader2));
Assert.True(buffer2.Slice(consumed).IsEmpty);
Assert.True(buffer2.Slice(examined).IsEmpty);
Assert.Equal(2, consumedBytes);
Assert.True(buffer2.Slice(reader2.Position).IsEmpty);
Assert.Equal(2, reader2.Consumed);
}
[Theory]
@ -325,7 +327,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var requestHandler = new RequestHandler();
var exception = Assert.Throws<BadHttpRequestException>(() =>
parser.ParseHeaders(requestHandler, buffer, out var consumed, out var examined, out var consumedBytes));
{
var reader = new SequenceReader<byte>(buffer);
parser.ParseHeaders(requestHandler, ref reader);
});
Assert.Equal(expectedExceptionMessage, exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
@ -364,7 +369,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes("Header: value\n\r\n"));
exception = Assert.Throws<BadHttpRequestException>(() =>
parser.ParseHeaders(requestHandler, buffer, out var consumed, out var examined, out var consumedBytes));
{
var reader = new SequenceReader<byte>(buffer);
parser.ParseHeaders(requestHandler, ref reader);
});
Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(string.Empty), exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
@ -393,7 +401,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent("Host:\r\nConnection: keep-alive\r\n\r\n");
var requestHandler = new RequestHandler();
var result = parser.ParseHeaders(requestHandler, buffer, out var consumed, out var examined, out _);
var reader = new SequenceReader<byte>(buffer);
var result = parser.ParseHeaders(requestHandler, ref reader);
Assert.True(result);
}
@ -405,7 +414,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent("A:B\r\nB: C\r\n\r\n");
var requestHandler = new RequestHandler();
var result = parser.ParseHeaders(requestHandler, buffer, out var consumed, out var examined, out _);
var reader = new SequenceReader<byte>(buffer);
var result = parser.ParseHeaders(requestHandler, ref reader);
Assert.True(result);
}
@ -419,14 +429,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"{headerName}:{rawHeaderValue}\r\n"));
var requestHandler = new RequestHandler();
parser.ParseHeaders(requestHandler, buffer, out var consumed, out var examined, out var consumedBytes);
var reader = new SequenceReader<byte>(buffer);
Assert.False(parser.ParseHeaders(requestHandler, ref reader));
var pairs = requestHandler.Headers.ToArray();
Assert.Single(pairs);
Assert.Equal(headerName, pairs[0].Key);
Assert.Equal(expectedHeaderValue, pairs[0].Value);
Assert.True(buffer.Slice(consumed).IsEmpty);
Assert.True(buffer.Slice(examined).IsEmpty);
Assert.True(buffer.Slice(reader.Position).IsEmpty);
}
private void VerifyRawHeaders(string rawHeaders, IEnumerable<string> expectedHeaderNames, IEnumerable<string> expectedHeaderValues)
@ -437,15 +447,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(rawHeaders));
var requestHandler = new RequestHandler();
parser.ParseHeaders(requestHandler, buffer, out var consumed, out var examined, out var consumedBytes);
var reader = new SequenceReader<byte>(buffer);
Assert.True(parser.ParseHeaders(requestHandler,ref reader));
var parsedHeaders = requestHandler.Headers.ToArray();
Assert.Equal(expectedHeaderNames.Count(), parsedHeaders.Length);
Assert.Equal(expectedHeaderNames, parsedHeaders.Select(t => t.Key));
Assert.Equal(expectedHeaderValues, parsedHeaders.Select(t => t.Value));
Assert.True(buffer.Slice(consumed).IsEmpty);
Assert.True(buffer.Slice(examined).IsEmpty);
Assert.True(buffer.Slice(reader.Position).IsEmpty);
}
private IHttpParser<RequestHandler> CreateParser(IKestrelTrace log) => new HttpParser<RequestHandler>(log.IsEnabled(LogLevel.Information));

View File

@ -83,8 +83,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
}
_buffer = _buffer.Slice(consumed, _buffer.End);
var reader = new SequenceReader<byte>(_buffer);
if (!_parser.ParseHeaders(new Adapter(this), _buffer, out consumed, out examined, out var consumedBytes))
if (!_parser.ParseHeaders(new Adapter(this), ref reader))
{
ErrorUtilities.ThrowInvalidRequestHeaders();
}

View File

@ -58,7 +58,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
_buffer = _buffer.Slice(consumed, _buffer.End);
if (!_parser.ParseHeaders(new Adapter(this), _buffer, out consumed, out examined, out var consumedBytes))
var reader = new SequenceReader<byte>(_buffer);
if (!_parser.ParseHeaders(new Adapter(this), ref reader))
{
ErrorUtilities.ThrowInvalidRequestHeaders();
}

View File

@ -21,17 +21,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
public static readonly NullParser<Http1ParsingHandler> Instance = new NullParser<Http1ParsingHandler>();
public bool ParseHeaders(TRequestHandler handler, in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined, out int consumedBytes)
public bool ParseHeaders(TRequestHandler handler, ref SequenceReader<byte> reader)
{
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));
handler.OnHeadersComplete();
consumedBytes = 0;
consumed = buffer.Start;
examined = buffer.End;
return true;
}