|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Buffers;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using HttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests;
public class HttpParserTests : LoggedTest
{
private static readonly KestrelTrace _nullTrace = new KestrelTrace(NullLoggerFactory.Instance);
private KestrelTrace CreateEnabledTrace() => new KestrelTrace(LoggerFactory);
[Theory]
[MemberData(nameof(RequestLineValidData))]
public void ParsesRequestLine(
string requestLine,
string expectedMethod,
string expectedRawTarget,
string expectedRawPath,
// This warns that theory methods should use all of their parameters,
// but this method is using a shared data collection with Http1ConnectionTests.TakeStartLineSetsHttpProtocolProperties and others.
#pragma warning disable xUnit1026
string expectedDecodedPath,
string expectedQueryString,
#pragma warning restore xUnit1026
string expectedVersion)
{
var parser = CreateParser(_nullTrace, false);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
Assert.True(ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined));
Assert.Equal(requestHandler.Method, expectedMethod);
Assert.Equal(requestHandler.Version, expectedVersion);
Assert.Equal(requestHandler.RawTarget, expectedRawTarget);
Assert.Equal(requestHandler.RawPath, expectedRawPath);
Assert.Equal(requestHandler.Version, expectedVersion);
Assert.True(buffer.Slice(consumed).IsEmpty);
Assert.True(buffer.Slice(examined).IsEmpty);
}
[Theory]
[MemberData(nameof(RequestLineIncompleteData))]
public void ParseRequestLineReturnsFalseWhenGivenIncompleteRequestLines(string requestLine)
{
var parser = CreateParser(_nullTrace, false);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
Assert.False(ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined));
}
[Theory]
[MemberData(nameof(RequestLineIncompleteData))]
public void ParseRequestLineDoesNotConsumeIncompleteRequestLine(string requestLine)
{
var parser = CreateParser(_nullTrace, false);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
Assert.False(ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined));
Assert.Equal(buffer.Start, consumed);
Assert.True(buffer.Slice(examined).IsEmpty);
}
[Theory]
[MemberData(nameof(RequestLineInvalidData))]
public void ParseRequestLineThrowsOnInvalidRequestLine(string requestLine)
{
var parser = CreateParser(CreateEnabledTrace());
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
#pragma warning disable CS0618 // Type or member is obsolete
var exception = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined));
var line = requestLine;
var nullIndex = line.IndexOf('\0');
if (nullIndex >= 0)
{
line = line.AsSpan().Slice(0, nullIndex + 2).ToString();
}
Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(line[..^1].EscapeNonPrintable()), exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
}
[Theory]
[MemberData(nameof(RequestLineInvalidDataLineFeedTerminator))]
public void ParseRequestSucceedsOnInvalidRequestLineLineFeedTerminator(string requestLine)
{
var parser = CreateParser(CreateEnabledTrace(), disableHttp1LineFeedTerminators: false);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
Assert.True(ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined));
}
[Theory]
[MemberData(nameof(RequestLineInvalidDataLineFeedTerminator))]
public void ParseRequestLineThrowsOnInvalidRequestLineLineFeedTerminator(string requestLine)
{
var parser = CreateParser(CreateEnabledTrace());
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
#pragma warning disable CS0618 // Type or member is obsolete
var exception = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined));
Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(requestLine[..^1].EscapeNonPrintable()), exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
}
[Theory]
[MemberData(nameof(MethodWithNonTokenCharData))]
public void ParseRequestLineThrowsOnNonTokenCharsInCustomMethod(string method)
{
var requestLine = $"{method} / HTTP/1.1\r\n";
var parser = CreateParser(CreateEnabledTrace(), false);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
#pragma warning disable CS0618 // Type or member is obsolete
var exception = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined));
var nullIndex = method.IndexOf('\0');
if (nullIndex >= 0)
{
var line = method.AsSpan().Slice(0, nullIndex + 1).ToString();
Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(line.EscapeNonPrintable()), exception.Message);
}
else
{
Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(method.EscapeNonPrintable() + @" / HTTP/1.1\x0D"), exception.Message);
}
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
}
[Theory]
[MemberData(nameof(UnrecognizedHttpVersionData))]
public void ParseRequestLineThrowsOnUnrecognizedHttpVersion(string httpVersion)
{
var requestLine = $"GET / {httpVersion}\r\n";
var parser = CreateParser(CreateEnabledTrace(), false);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
#pragma warning disable CS0618 // Type or member is obsolete
var exception = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined));
Assert.Equal(CoreStrings.FormatBadRequest_UnrecognizedHTTPVersion(httpVersion), exception.Message);
Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, exception.StatusCode);
}
[Fact]
public void StartOfPathNotFound()
{
var requestLine = $"GET \n";
var parser = CreateParser(CreateEnabledTrace(), false);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
#pragma warning disable CS0618 // Type or member is obsolete
var exception = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined));
Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail("GET "), exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.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(_nullTrace, false);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(rawHeaders));
var requestHandler = new RequestHandler();
var reader = new SequenceReader<byte>(buffer);
Assert.False(parser.ParseHeaders(requestHandler, ref reader));
}
[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(_nullTrace, false);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(rawHeaders));
var requestHandler = new RequestHandler();
var reader = new SequenceReader<byte>(buffer);
Assert.False(parser.ParseHeaders(requestHandler, ref reader));
Assert.Equal(buffer.Length, buffer.Slice(reader.Consumed).Length);
Assert.Equal(0, reader.Consumed);
}
[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", "")]
[InlineData("a:b\r\n\r\n", "a", "b", null, null)]
[InlineData("a: b\r\n\r\n", "a", "b", null, null)]
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, disableHttp1LineFeedTerminators: false);
}
[Theory]
[InlineData("Cookie: \n\r\n", "Cookie", "", null, null)]
[InlineData("Cookie:\n\r\n", "Cookie", "", null, null)]
[InlineData("Cookie: \nConnection: close\r\n\r\n", "Cookie", "", "Connection", "close")]
[InlineData("Cookie: \r\nConnection: close\n\r\n", "Cookie", "", "Connection", "close")]
[InlineData("Cookie:\nConnection: close\r\n\r\n", "Cookie", "", "Connection", "close")]
[InlineData("Cookie:\r\nConnection: close\n\r\n", "Cookie", "", "Connection", "close")]
[InlineData("Connection: close\nCookie: \r\n\r\n", "Connection", "close", "Cookie", "")]
[InlineData("Connection: close\r\nCookie: \n\r\n", "Connection", "close", "Cookie", "")]
[InlineData("Connection: close\nCookie:\r\n\r\n", "Connection", "close", "Cookie", "")]
[InlineData("Connection: close\r\nCookie:\n\r\n", "Connection", "close", "Cookie", "")]
[InlineData("a:b\n\r\n", "a", "b", null, null)]
[InlineData("a: b\n\r\n", "a", "b", null, null)]
[InlineData("a:b\n\n", "a", "b", null, null)]
[InlineData("a: b\n\n", "a", "b", null, null)]
public void ParseHeadersCantParseSingleLineFeedWihtoutLineFeedTerminatorEnabled(
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 };
#pragma warning disable CS0618 // Type or member is obsolete
Assert.Throws<BadHttpRequestException>(() => VerifyRawHeaders(rawHeaders, expectedHeaderNames, expectedHeaderValues, disableHttp1LineFeedTerminators: true));
#pragma warning restore CS0618 // Type or member is obsolete
}
[Theory]
[InlineData("Cookie: \n\r\n", "Cookie", "", null, null)]
[InlineData("Cookie:\n\r\n", "Cookie", "", null, null)]
[InlineData("Cookie: \nConnection: close\r\n\r\n", "Cookie", "", "Connection", "close")]
[InlineData("Cookie: \r\nConnection: close\n\r\n", "Cookie", "", "Connection", "close")]
[InlineData("Cookie:\nConnection: close\r\n\r\n", "Cookie", "", "Connection", "close")]
[InlineData("Cookie:\r\nConnection: close\n\r\n", "Cookie", "", "Connection", "close")]
[InlineData("Connection: close\nCookie: \r\n\r\n", "Connection", "close", "Cookie", "")]
[InlineData("Connection: close\r\nCookie: \n\r\n", "Connection", "close", "Cookie", "")]
[InlineData("Connection: close\nCookie:\r\n\r\n", "Connection", "close", "Cookie", "")]
[InlineData("Connection: close\r\nCookie:\n\r\n", "Connection", "close", "Cookie", "")]
public void ParseHeadersCanParseSingleLineFeedWithLineFeedTerminatorEnabled(
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, disableHttp1LineFeedTerminators: false);
}
[Theory]
[InlineData("a: b\r\n\n", "a", "b", null, null)]
[InlineData("a: b\n\n", "a", "b", null, null)]
[InlineData("a: b\nc: d\r\n\n", "a", "b", "c", "d")]
[InlineData("a: b\nc: d\n\n", "a", "b", "c", "d")]
public void ParseHeadersCantEndWithLineFeedTerminator(
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 };
#pragma warning disable CS0618 // Type or member is obsolete
Assert.Throws<BadHttpRequestException>(() => VerifyRawHeaders(rawHeaders, expectedHeaderNames, expectedHeaderValues, disableHttp1LineFeedTerminators: true));
#pragma warning restore CS0618 // Type or member is obsolete
}
[Theory]
[InlineData("a:b\n\r\n", "a", "b", null, null)]
[InlineData("a: b\n\r\n", "a", "b", null, null)]
[InlineData("a: b\nc: d\n\r\n", "a", "b", "c", "d")]
[InlineData("a: b\nc: d\n\n", "a", "b", "c", "d")]
[InlineData("a: b\n\n", "a", "b", null, null)]
public void ParseHeadersCanEndAfterLineFeedTerminator(
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, disableHttp1LineFeedTerminators: false);
}
[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(_nullTrace, false);
const string headerLine = "Header: value\r\n\r";
var buffer1 = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(headerLine));
var requestHandler = new RequestHandler();
var reader1 = new SequenceReader<byte>(buffer1);
Assert.False(parser.ParseHeaders(requestHandler, ref reader1));
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"));
var reader2 = new SequenceReader<byte>(buffer2);
Assert.True(parser.ParseHeaders(requestHandler, ref reader2));
Assert.True(buffer2.Slice(reader2.Position).IsEmpty);
Assert.Equal(2, reader2.Consumed);
}
[Theory]
[MemberData(nameof(RequestHeaderInvalidData))]
public void ParseHeadersThrowsOnInvalidRequestHeaders(string rawHeaders, string expectedExceptionMessage)
{
var parser = CreateParser(CreateEnabledTrace(), false);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(rawHeaders));
var requestHandler = new RequestHandler();
#pragma warning disable CS0618 // Type or member is obsolete
var exception = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
{
var reader = new SequenceReader<byte>(buffer);
parser.ParseHeaders(requestHandler, ref reader);
});
Assert.Equal(expectedExceptionMessage, exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
}
[Theory]
[MemberData(nameof(RequestHeaderInvalidDataLineFeedTerminator))]
public void ParseHeadersThrowsOnInvalidRequestHeadersLineFeedTerminator(string rawHeaders, string expectedExceptionMessage)
{
var parser = CreateParser(CreateEnabledTrace(), true);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(rawHeaders));
var requestHandler = new RequestHandler();
#pragma warning disable CS0618 // Type or member is obsolete
var exception = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
{
var reader = new SequenceReader<byte>(buffer);
parser.ParseHeaders(requestHandler, ref reader);
});
Assert.Equal(expectedExceptionMessage, exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
}
[Fact]
public void ExceptionDetailNotIncludedWhenLogLevelInformationNotEnabled()
{
var parser = CreateParser(_nullTrace);
// Invalid request line
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes("GET % HTTP/1.1\r\n"));
var requestHandler = new RequestHandler();
#pragma warning disable CS0618 // Type or member is obsolete
var exception = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined));
Assert.Equal("Invalid request line: ''", exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
// Unrecognized HTTP version
buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes("GET / HTTP/1.2\r\n"));
#pragma warning disable CS0618 // Type or member is obsolete
exception = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined));
Assert.Equal(CoreStrings.FormatBadRequest_UnrecognizedHTTPVersion(string.Empty), exception.Message);
Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, exception.StatusCode);
// Invalid request header
buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes("Header: value\n\r\n"));
#pragma warning disable CS0618 // Type or member is obsolete
exception = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
{
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);
}
[Fact]
public void ParseRequestLineSplitBufferWithoutNewLineDoesNotUpdateConsumed()
{
var parser = CreateParser(_nullTrace, false);
var buffer = ReadOnlySequenceFactory.CreateSegments(
Encoding.ASCII.GetBytes("GET "),
Encoding.ASCII.GetBytes("/"));
var requestHandler = new RequestHandler();
var result = ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined);
Assert.False(result);
Assert.Equal(buffer.Start, consumed);
Assert.Equal(buffer.End, examined);
}
[Fact]
public void ParseRequestLineTlsOverHttp()
{
var parser = CreateParser(_nullTrace, false);
var buffer = ReadOnlySequenceFactory.CreateSegments(new byte[] { 0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0xfc, 0x03, 0x03, 0x03, 0xca, 0xe0, 0xfd, 0x0a });
var requestHandler = new RequestHandler();
#pragma warning disable CS0618 // Type or member is obsolete
var badHttpRequestException = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
{
ParseRequestLine(parser, requestHandler, buffer, out var consumed, out var examined);
});
Assert.Equal(StatusCodes.Status400BadRequest, badHttpRequestException.StatusCode);
Assert.Equal(RequestRejectionReason.TlsOverHttpError, badHttpRequestException.Reason);
}
[Theory]
[MemberData(nameof(RequestHeaderInvalidData))]
public void ParseHeadersThrowsOnInvalidRequestHeadersWithGratuitouslySplitBuffers(string rawHeaders, string expectedExceptionMessage)
{
var parser = CreateParser(CreateEnabledTrace(), false);
var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(rawHeaders);
var requestHandler = new RequestHandler();
#pragma warning disable CS0618 // Type or member is obsolete
var exception = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
{
var reader = new SequenceReader<byte>(buffer);
parser.ParseHeaders(requestHandler, ref reader);
});
Assert.Equal(expectedExceptionMessage, exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
}
[Theory]
[MemberData(nameof(RequestHeaderInvalidDataLineFeedTerminator))]
public void ParseHeadersThrowsOnInvalidRequestHeadersWithGratuitouslySplitBuffersLineFeedTerminator(string rawHeaders, string expectedExceptionMessage)
{
var parser = CreateParser(CreateEnabledTrace(), true);
var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(rawHeaders);
var requestHandler = new RequestHandler();
#pragma warning disable CS0618 // Type or member is obsolete
var exception = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
{
var reader = new SequenceReader<byte>(buffer);
parser.ParseHeaders(requestHandler, ref reader);
});
Assert.Equal(expectedExceptionMessage, exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
}
[Theory]
[InlineData("Host:\r\nConnection: keep-alive\r\n\r\n")]
[InlineData("A:B\r\nB: C\r\n\r\n")]
public void ParseHeadersWithGratuitouslySplitBuffers(string headers)
{
var parser = CreateParser(_nullTrace, false);
var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(headers);
var requestHandler = new RequestHandler();
var reader = new SequenceReader<byte>(buffer);
var result = parser.ParseHeaders(requestHandler, ref reader);
Assert.True(result);
}
[Theory]
[InlineData("Host: \r\nConnection: keep-alive\r")]
public void ParseHeaderLineIncompleteDataWithGratuitouslySplitBuffers(string headers)
{
var parser = CreateParser(_nullTrace, false);
var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(headers);
var requestHandler = new RequestHandler();
var reader = new SequenceReader<byte>(buffer);
var result = parser.ParseHeaders(requestHandler, ref reader);
Assert.False(result);
}
[Theory]
[InlineData("Host: \r\nConnection: keep-alive\r")]
public void ParseHeaderLineIncompleteData(string headers)
{
var parser = CreateParser(_nullTrace, false);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(headers));
var requestHandler = new RequestHandler();
var reader = new SequenceReader<byte>(buffer);
var result = parser.ParseHeaders(requestHandler, ref reader);
Assert.False(result);
}
[Theory]
[InlineData("Host:\nConnection: keep-alive\r\n\r\n")]
[InlineData("Host:\r\nConnection: keep-alive\n\r\n")]
[InlineData("A:B\nB: C\r\n\r\n")]
[InlineData("A:B\r\nB: C\n\r\n")]
[InlineData("Host:\r\nConnection: keep-alive\n\n")]
public void ParseHeadersWithGratuitouslySplitBuffersQuirkMode(string headers)
{
var parser = CreateParser(_nullTrace, disableHttp1LineFeedTerminators: false);
var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(headers);
var requestHandler = new RequestHandler();
var reader = new SequenceReader<byte>(buffer);
var result = parser.ParseHeaders(requestHandler, ref reader);
Assert.True(result);
}
[Fact]
public void ParseLargeHeaderLineWithGratuitouslySplitBuffers()
{
// Must be greater than the stackalloc we use (256) to test the array pool path
var stringLength = 300;
var headers = $"Host: {new string('a', stringLength)}\r\nConnection: keep-alive\r\n\r\n";
var parser = CreateParser(_nullTrace, false);
var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(headers);
var requestHandler = new RequestHandler();
var reader = new SequenceReader<byte>(buffer);
var result = parser.ParseHeaders(requestHandler, ref reader);
Assert.True(result);
}
[Theory]
[InlineData("Host:\nConnection: keep-alive", "\n\r\n", false)]
[InlineData("Host:\nConnection: keep-aliv", "e\n\r\n", false)]
[InlineData("Host:\nC", "onnection: keep-alive\n\r\n", false)]
[InlineData("Connection: keep-alive", "\nHost:\n\r\n", false)]
[InlineData("Host:\r\nConnection: keep-alive", "\r\n\r\n", true)]
[InlineData("Host:\r\nConnection: keep-aliv", "e\r\n\r\n", true)]
[InlineData("Host:\r\nC", "onnection: keep-alive\r\n\r\n", true)]
[InlineData("Connection: keep-alive", "\r\nHost:\r\n\r\n", true)]
public void ParseHeaderLineWithSplitBuffers(string firstHeadersSegment, string secondHeadersSegment, bool quirkMode)
{
var parser = CreateParser(_nullTrace, quirkMode);
var buffer = ReadOnlySequenceFactory.CreateSegments(
Encoding.ASCII.GetBytes(firstHeadersSegment),
Encoding.ASCII.GetBytes(secondHeadersSegment));
var requestHandler = new RequestHandler();
var reader = new SequenceReader<byte>(buffer);
var result = parser.ParseHeaders(requestHandler, ref reader);
Assert.True(result);
}
[Fact]
public void ParseHeadersWithSplitBufferReturnsFalseWhenGivenIncompleteHeaders()
{
var parser = CreateParser(_nullTrace, false);
var buffer = ReadOnlySequenceFactory.CreateSegments(
Encoding.ASCII.GetBytes("Host:\nConnection: keep-aliv"),
Encoding.ASCII.GetBytes("e\n"));
var requestHandler = new RequestHandler();
var reader = new SequenceReader<byte>(buffer);
var result = parser.ParseHeaders(requestHandler, ref reader);
Assert.False(result);
}
[Theory]
[InlineData("H", "e")]
[InlineData("Header", ":")]
[InlineData("Header:", " ")]
[InlineData("Header: ", "v")]
[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\nHeader-2", ":")]
[InlineData("Header-1: value1\r\nHeader-2: value", "2\r")]
[InlineData("Header-1: value1\r\nHeader-2: value2", "\r\n")]
[InlineData("Header-1: value1\r\nHeader-2: value2\r", "\n\r")]
public void ParseHeadersWithSplitBuffersReturnsFalseWhenGivenIncompleteHeaders(string firstHeadersSegment, string secondHeadersSegment)
{
var parser = CreateParser(_nullTrace, false);
var buffer = ReadOnlySequenceFactory.CreateSegments(
Encoding.ASCII.GetBytes(firstHeadersSegment),
Encoding.ASCII.GetBytes(secondHeadersSegment));
var requestHandler = new RequestHandler();
var reader = new SequenceReader<byte>(buffer);
Assert.False(parser.ParseHeaders(requestHandler, ref reader));
}
[Fact]
public void ParseHeadersWithSplitBuffersThrowsForSmallHeader()
{
var parser = CreateParser(CreateEnabledTrace(), false);
var buffer = ReadOnlySequenceFactory.CreateSegments(
Encoding.ASCII.GetBytes("a"),
Encoding.ASCII.GetBytes("b\n"));
var requestHandler = new RequestHandler();
#pragma warning disable CS0618 // Type or member is obsolete
var exception = Assert.Throws<BadHttpRequestException>(() =>
#pragma warning restore CS0618 // Type or member is obsolete
{
var reader = new SequenceReader<byte>(buffer);
parser.ParseHeaders(requestHandler, ref reader);
});
Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"ab\x0A"), exception.Message);
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
}
private bool ParseRequestLine(IHttpParser<RequestHandler> parser, RequestHandler requestHandler, ReadOnlySequence<byte> readableBuffer, out SequencePosition consumed, out SequencePosition examined)
{
var reader = new SequenceReader<byte>(readableBuffer);
if (parser.ParseRequestLine(requestHandler, ref reader))
{
consumed = reader.Position;
examined = reader.Position;
return true;
}
else
{
consumed = reader.Position;
examined = readableBuffer.End;
return false;
}
}
private void VerifyHeader(
string headerName,
string rawHeaderValue,
string expectedHeaderValue)
{
var parser = CreateParser(_nullTrace, false);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"{headerName}:{rawHeaderValue}\r\n"));
var requestHandler = new RequestHandler();
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(reader.Position).IsEmpty);
}
private void VerifyRawHeaders(string rawHeaders, IEnumerable<string> expectedHeaderNames, IEnumerable<string> expectedHeaderValues, bool disableHttp1LineFeedTerminators = true)
{
Assert.True(expectedHeaderNames.Count() == expectedHeaderValues.Count(), $"{nameof(expectedHeaderNames)} and {nameof(expectedHeaderValues)} sizes must match");
var parser = CreateParser(_nullTrace, disableHttp1LineFeedTerminators);
var buffer = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes(rawHeaders));
var requestHandler = new RequestHandler();
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(reader.Position).IsEmpty);
}
private IHttpParser<RequestHandler> CreateParser(KestrelTrace log, bool disableHttp1LineFeedTerminators = true) => new HttpParser<RequestHandler>(log.IsEnabled(LogLevel.Information), disableHttp1LineFeedTerminators);
public static IEnumerable<object[]> RequestLineValidData => HttpParsingData.RequestLineValidData;
public static IEnumerable<object[]> RequestLineIncompleteData => HttpParsingData.RequestLineIncompleteData.Select(requestLine => new[] { requestLine });
public static IEnumerable<object[]> RequestLineInvalidDataLineFeedTerminator => HttpParsingData.RequestLineInvalidDataLineFeedTerminator.Select(requestLine => new[] { requestLine });
public static IEnumerable<object[]> RequestLineInvalidData => HttpParsingData.RequestLineInvalidData.Select(requestLine => new[] { requestLine });
public static IEnumerable<object[]> MethodWithNonTokenCharData => HttpParsingData.MethodWithNonTokenCharData.Select(method => new[] { method });
public static TheoryData<string> UnrecognizedHttpVersionData => HttpParsingData.UnrecognizedHttpVersionData;
public static IEnumerable<object[]> RequestHeaderInvalidData => HttpParsingData.RequestHeaderInvalidData;
public static IEnumerable<object[]> RequestHeaderInvalidDataLineFeedTerminator => HttpParsingData.RequestHeaderInvalidDataLineFeedTerminator;
private class RequestHandler : IHttpRequestLineHandler, IHttpHeadersHandler
{
public string Method { get; set; }
public string Version { get; set; }
public string RawTarget { get; set; }
public string RawPath { get; set; }
public string Query { get; set; }
public bool PathEncoded { get; set; }
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>();
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
Headers[name.GetAsciiString()] = value.GetAsciiString();
}
void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
{
Method = method != HttpMethod.Custom ? HttpUtilities.MethodToString(method) : customMethod.GetAsciiString();
Version = HttpUtilities.VersionToString(version);
RawTarget = target.GetAsciiString();
RawPath = path.GetAsciiString();
Query = query.GetAsciiString();
PathEncoded = pathEncoded;
}
public void OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathLength targetPath, Span<byte> startLine)
{
var method = versionAndMethod.Method;
var version = versionAndMethod.Version;
var customMethod = startLine[..versionAndMethod.MethodEnd];
var targetStart = targetPath.Offset;
var target = startLine[targetStart..];
var path = target[..targetPath.Length];
var query = target[targetPath.Length..];
Method = method != HttpMethod.Custom ? HttpUtilities.MethodToString(method) : customMethod.GetAsciiString();
Version = HttpUtilities.VersionToString(version);
RawTarget = target.GetAsciiString();
RawPath = path.GetAsciiString();
Query = query.GetAsciiString();
PathEncoded = targetPath.IsEncoded;
}
public void OnStaticIndexedHeader(int index)
{
throw new NotImplementedException();
}
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
throw new NotImplementedException();
}
}
// Doesn't put empty blocks in between every byte
internal class BytePerSegmentTestSequenceFactory : ReadOnlySequenceFactory
{
public static ReadOnlySequenceFactory Instance { get; } = new HttpParserTests.BytePerSegmentTestSequenceFactory();
public override ReadOnlySequence<byte> CreateOfSize(int size)
{
return CreateWithContent(new byte[size]);
}
public override ReadOnlySequence<byte> CreateWithContent(byte[] data)
{
var segments = new List<byte[]>();
foreach (var b in data)
{
segments.Add(new[] { b });
}
return CreateSegments(segments.ToArray());
}
}
}
|