Interleave multispan and single span code path (#1442)

- Attempt at making the reader better for multispan parsing
- Try tighter inner loop
- Fix boundary case and clean code up
- Update the cursor once instead of after every header
- Fix errors with not updating consumed state on incomplete header payload
- Filled a test hole, removed a condition that should never happen
- Avoid struct copies every iteration when parsing headers
This commit is contained in:
David Fowler 2017-03-06 09:06:28 -08:00 committed by GitHub
parent 0ce111d9f1
commit 537b06f025
2 changed files with 148 additions and 145 deletions

View File

@ -253,31 +253,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
examined = buffer.End; examined = buffer.End;
consumedBytes = 0; consumedBytes = 0;
if (buffer.IsSingleSpan)
{
var result = TakeMessageHeadersSingleSpan(handler, buffer.First.Span, out consumedBytes);
consumed = buffer.Move(consumed, consumedBytes);
if (result)
{
examined = consumed;
}
return result;
}
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;
var start = reader;
int ch1 = reader.Take();
var ch2 = reader.Take();
if (ch1 == -1) try
{ {
return false; while (!reader.End)
{
var span = reader.Span;
var remaining = span.Length;
fixed (byte* pBuffer = &span.DangerousGetPinnableReference())
{
while (remaining > 0)
{
var index = reader.Index;
int ch1;
int ch2;
// Fast path, we're still looking at the same span
if (remaining >= 2)
{
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
ch1 = reader.Take();
ch2 = reader.Take();
} }
if (ch1 == ByteCR) if (ch1 == ByteCR)
@ -285,15 +296,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
// Check for final CRLF. // Check for final CRLF.
if (ch2 == -1) if (ch2 == -1)
{ {
// Reset the reader so we don't consume anything
reader = start;
return false; return false;
} }
else if (ch2 == ByteLF) else if (ch2 == ByteLF)
{ {
// REVIEW: Removed usage of ReadableBufferReader.Cursor, because it's broken when the buffer is // If we got 2 bytes from the span directly so skip ahead 2 so that
// sliced and doesn't start at the start of a segment. We should probably fix this. // the reader's state matches what we expect
//consumed = reader.Cursor; if (index == reader.Index)
consumed = buffer.Move(consumed, 2); {
examined = consumed; reader.Skip(2);
}
done = true;
return true; return true;
} }
@ -305,111 +321,82 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
RejectRequest(RequestRejectionReason.HeaderLineMustNotStartWithWhitespace); RejectRequest(RequestRejectionReason.HeaderLineMustNotStartWithWhitespace);
} }
// Reset the reader since we're not at the end of headers // We moved the reader so look ahead 2 bytes so reset both the reader
reader = start; // and the index
if (index != reader.Index)
if (ReadCursorOperations.Seek(consumed, bufferEnd, out var lineEnd, ByteLF) == -1)
{ {
reader = start;
index = reader.Index;
}
Span<byte> headerSpan;
var endIndex = IndexOf(pBuffer, index, remaining, ByteLF);
if (endIndex != -1)
{
headerSpan = new Span<byte>(pBuffer + index, (endIndex - index + 1));
}
else
{
var current = reader.Cursor;
// Split buffers
if (ReadCursorOperations.Seek(current, bufferEnd, out var lineEnd, ByteLF) == -1)
{
// Not there
return false; return false;
} }
// Make sure LF is included in lineEnd // Make sure LF is included in lineEnd
lineEnd = buffer.Move(lineEnd, 1); lineEnd = buffer.Move(lineEnd, 1);
headerSpan = buffer.Slice(current, lineEnd).ToSpan();
var headerBuffer = buffer.Slice(consumed, lineEnd); // We're going to the next span after this since we know we crossed spans here
var span = headerBuffer.ToSpan(); // so mark the remaining as equal to the headerSpan so that we end up at 0
// on the next iteration
remaining = headerSpan.Length;
}
if (!TakeSingleHeader(span, out var nameStart, out var nameEnd, out var valueStart, out var valueEnd)) if (!TakeSingleHeader(headerSpan, out var nameStart, out var nameEnd, out var valueStart, out var valueEnd))
{ {
return false; return false;
} }
// Skip the reader forward past the header line // Skip the reader forward past the header line
reader.Skip(span.Length); reader.Skip(headerSpan.Length);
// REVIEW: Removed usage of ReadableBufferReader.Cursor, because it's broken when the buffer is
// sliced and doesn't start at the start of a segment. We should probably fix this.
//consumed = reader.Cursor;
consumed = buffer.Move(consumed, span.Length);
consumedBytes += span.Length;
var nameBuffer = span.Slice(nameStart, nameEnd - nameStart); remaining -= headerSpan.Length;
var valueBuffer = span.Slice(valueStart, valueEnd - valueStart);
var nameBuffer = headerSpan.Slice(nameStart, nameEnd - nameStart);
var valueBuffer = headerSpan.Slice(valueStart, valueEnd - valueStart);
handler.OnHeader(nameBuffer, valueBuffer); handler.OnHeader(nameBuffer, valueBuffer);
} }
} }
private unsafe bool TakeMessageHeadersSingleSpan<T>(T handler, Span<byte> headersSpan, out int consumedBytes) where T : IHttpHeadersHandler
{
consumedBytes = 0;
var length = headersSpan.Length;
fixed (byte* data = &headersSpan.DangerousGetPinnableReference())
{
while (consumedBytes < length)
{
var ch1 = data[consumedBytes];
var ch2 = -1;
if (consumedBytes + 1 < length)
{
ch2 = data[consumedBytes + 1];
}
if (ch1 == ByteCR)
{
// Check for final CRLF.
if (ch2 == -1)
{
return false;
}
else if (ch2 == ByteLF)
{
consumedBytes += 2;
return true;
}
// Headers don't end in CRLF line.
RejectRequest(RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence);
}
else if (ch1 == ByteSpace || ch1 == ByteTab)
{
RejectRequest(RequestRejectionReason.HeaderLineMustNotStartWithWhitespace);
}
var lineEnd = IndexOf(data, consumedBytes, headersSpan.Length, ByteLF);
if (lineEnd == -1)
{
return false;
}
var span = new Span<byte>(data + consumedBytes, (lineEnd - consumedBytes + 1));
if (!TakeSingleHeader(span, out var nameStart, out var nameEnd, out var valueStart, out var valueEnd))
{
return false;
}
consumedBytes += span.Length;
var nameBuffer = span.Slice(nameStart, nameEnd - nameStart);
var valueBuffer = span.Slice(valueStart, valueEnd - valueStart);
handler.OnHeader(nameBuffer, valueBuffer);
}
} }
return false; return false;
} }
finally
{
consumed = reader.Cursor;
consumedBytes = reader.ConsumedBytes;
private unsafe int IndexOf(byte* data, int index, int length, byte value) if (done)
{ {
for (int i = index; i < length; i++) examined = consumed;
}
}
}
private unsafe int IndexOf(byte* pBuffer, int index, int length, byte value)
{ {
if (data[i] == value) for (int i = 0; i < length; i++, index++)
{ {
return i; if (pBuffer[index] == value)
{
return index;
} }
} }
return -1; return -1;
@ -427,14 +414,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
int i = 0; int i = 0;
var done = false; var done = false;
fixed (byte* data = &span.DangerousGetPinnableReference()) fixed (byte* pHeader = &span.DangerousGetPinnableReference())
{ {
switch (HeaderState.Name) switch (HeaderState.Name)
{ {
case HeaderState.Name: case HeaderState.Name:
for (; i < headerLineLength; i++) for (; i < headerLineLength; i++)
{ {
var ch = data[i]; var ch = pHeader[i];
if (ch == ByteColon) if (ch == ByteColon)
{ {
if (nameHasWhitespace) if (nameHasWhitespace)
@ -460,7 +447,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
case HeaderState.Whitespace: case HeaderState.Whitespace:
for (; i < headerLineLength; i++) for (; i < headerLineLength; i++)
{ {
var ch = data[i]; var ch = pHeader[i];
var whitespace = ch == ByteTab || ch == ByteSpace || ch == ByteCR; var whitespace = ch == ByteTab || ch == ByteSpace || ch == ByteCR;
if (!whitespace) if (!whitespace)
@ -484,7 +471,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
case HeaderState.ExpectValue: case HeaderState.ExpectValue:
for (; i < headerLineLength; i++) for (; i < headerLineLength; i++)
{ {
var ch = data[i]; var ch = pHeader[i];
var whitespace = ch == ByteTab || ch == ByteSpace; var whitespace = ch == ByteTab || ch == ByteSpace;
if (whitespace) if (whitespace)
@ -527,7 +514,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
RejectRequest(RequestRejectionReason.MissingCRInHeaderLine); RejectRequest(RequestRejectionReason.MissingCRInHeaderLine);
break; break;
case HeaderState.ExpectNewLine: case HeaderState.ExpectNewLine:
if (data[i] != ByteLF) if (pHeader[i] != ByteLF)
{ {
RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR); RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR);
} }

View File

@ -203,6 +203,22 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
} }
[Fact]
public async Task TakeMessageHeadersConsumesBytesCorrectlyAtEnd()
{
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes("Header-1: value1\r\n\r"));
var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer;
Assert.False(_frame.TakeMessageHeaders(readableBuffer, out _consumed, out _examined));
_socketInput.Reader.Advance(_consumed, _examined);
await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes("\n"));
readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer;
Assert.True(_frame.TakeMessageHeaders(readableBuffer, out _consumed, out _examined));
_socketInput.Reader.Advance(_consumed, _examined);
}
[Fact] [Fact]
public async Task TakeMessageHeadersThrowsWhenHeadersExceedTotalSizeLimit() public async Task TakeMessageHeadersThrowsWhenHeadersExceedTotalSizeLimit()
{ {