Convert HTTP parsing FrameTests to IHttpParser tests (#1416).

- Also fix an issue in KestrelHttpParser where "Header: \r\n" is
  parsed with a value of " " instead of "".
This commit is contained in:
Cesar Blum Silveira 2017-03-06 17:15:59 -08:00
parent e2f8c226ef
commit 5743d740b4
5 changed files with 531 additions and 355 deletions

View File

@ -459,7 +459,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
// Ignore end whitespace // Ignore end whitespace
for (; valueEnd > valueStart; valueEnd--) for (; valueEnd >= valueStart; valueEnd--)
{ {
var ch = headerLine[valueEnd]; var ch = headerLine[valueEnd];
if (ch != ByteTab && ch != ByteSpace) if (ch != ByteTab && ch != ByteSpace)

View File

@ -1,9 +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.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.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
@ -127,25 +125,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
""); "");
} }
public static IEnumerable<object> InvalidRequestLineData => HttpParsingData.InvalidRequestLineData public static TheoryData<string, string> InvalidRequestLineData
.Select(requestLine => new object[]
{ {
requestLine, get
$"Invalid request line: {requestLine.Replace("\r", "<0x0D>").Replace("\n", "<0x0A>")}",
})
.Concat(HttpParsingData.EncodedNullCharInTargetRequestLines.Select(requestLine => new object[]
{ {
requestLine, var data = new TheoryData<string, string>();
"Invalid request line."
})) foreach (var requestLine in HttpParsingData.RequestLineInvalidData)
.Concat(HttpParsingData.NullCharInTargetRequestLines.Select(requestLine => new object[]
{ {
requestLine, data.Add(requestLine, $"Invalid request line: {requestLine.Replace("\r", "<0x0D>").Replace("\n", "<0x0A>")}");
"Invalid request line." }
}));
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; public static IEnumerable<object[]> InvalidRequestHeaderData => HttpParsingData.RequestHeaderInvalidData;
} }
} }

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Linq;
using System.Net; using System.Net;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@ -25,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;
@ -50,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
{ {
@ -65,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>()
}; };
@ -77,149 +76,30 @@ 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, 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, 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, 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, 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, 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, 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, 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("Whitespace is not allowed in header name.", 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);
} }
[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()
{ {
@ -227,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, 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);
@ -243,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, 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, 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()
{ {
@ -296,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, 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);
@ -308,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, 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);
@ -407,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]
public async Task TakeStartLineCallsConsumingCompleteWithFurthestExamined()
{
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] [Fact]
public async Task ParseRequestStartsRequestHeadersTimeoutOnFirstByteAvailable() public async Task ParseRequestStartsRequestHeadersTimeoutOnFirstByteAvailable()
{ {
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.ParseRequest((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));
@ -498,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, 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, out consumed, out examined));
_socketInput.Reader.Advance(consumed, examined);
} }
[Fact] [Fact]
@ -599,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();
} }
@ -681,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]
@ -758,30 +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
.Select(requestLine => new object[]
{ {
requestLine, get
typeof(BadHttpRequestException),
$"Invalid request line: {requestLine.Replace("\r", "<0x0D>").Replace("\n", "<0x0A>")}",
})
.Concat(HttpParsingData.EncodedNullCharInTargetRequestLines.Select(requestLine => new object[]
{ {
requestLine, var data = new TheoryData<string>();
typeof(InvalidOperationException),
"The path contains null characters."
}))
.Concat(HttpParsingData.NullCharInTargetRequestLines.Select(requestLine => new object[]
{
requestLine,
typeof(InvalidOperationException),
new InvalidOperationException().Message
}));
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

@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Testing
{ {
public class HttpParsingData public class HttpParsingData
{ {
public static IEnumerable<string[]> ValidRequestLineData public static IEnumerable<string[]> RequestLineValidData
{ {
get get
{ {
@ -19,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"),
@ -56,21 +56,42 @@ 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
}; };
} }
} }
public static IEnumerable<string> InvalidRequestLineData => new[] public static IEnumerable<string> RequestLineIncompleteData => new[]
{
"G",
"GE",
"GET",
"GET ",
"GET /",
"GET / ",
"GET / H",
"GET / HT",
"GET / HTT",
"GET / HTTP",
"GET / HTTP/",
"GET / HTTP/1",
"GET / HTTP/1.",
"GET / HTTP/1.1",
"GET / HTTP/1.1\r",
};
public static IEnumerable<string> RequestLineInvalidData => new[]
{ {
"G\r\n", "G\r\n",
"GE\r\n", "GE\r\n",
@ -140,7 +161,7 @@ namespace Microsoft.AspNetCore.Testing
"post= / HTTP/1.0\r\n", "post= / HTTP/1.0\r\n",
}; };
public static IEnumerable<string> EncodedNullCharInTargetRequestLines => new[] public static IEnumerable<string> RequestLineWithEncodedNullCharInTargetData => new[]
{ {
"GET /%00 HTTP/1.1\r\n", "GET /%00 HTTP/1.1\r\n",
"GET /%00%00 HTTP/1.1\r\n", "GET /%00%00 HTTP/1.1\r\n",
@ -149,11 +170,10 @@ namespace Microsoft.AspNetCore.Testing
"GET /%F3%00%82%86 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%00%82 HTTP/1.1\r\n",
"GET /%F3%85%82%00 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", "GET /%E8%01%00 HTTP/1.1\r\n",
}; };
public static IEnumerable<string> NullCharInTargetRequestLines => new[] public static IEnumerable<string> RequestLineWithNullCharInTargetData => new[]
{ {
"GET \0 HTTP/1.1\r\n", "GET \0 HTTP/1.1\r\n",
"GET /\0 HTTP/1.1\r\n", "GET /\0 HTTP/1.1\r\n",
@ -183,7 +203,7 @@ namespace Microsoft.AspNetCore.Testing
"8charact", "8charact",
}; };
public static IEnumerable<object[]> InvalidRequestHeaderData public static IEnumerable<object[]> RequestHeaderInvalidData
{ {
get get
{ {
@ -228,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