diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs index 5e18ba08616d..bb9c7825d5e2 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs @@ -21,12 +21,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; public class HttpParser : IHttpParser where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler { private readonly bool _showErrorDetails; + private readonly bool _allowLineFeedTerminator; /// /// This API supports framework infrastructure and is not intended to be used /// directly from application code. /// - public HttpParser() : this(showErrorDetails: true) + public HttpParser() : this(showErrorDetails: true, allowLineFeedTerminator: false) { } @@ -34,11 +35,16 @@ public HttpParser() : this(showErrorDetails: true) /// This API supports framework infrastructure and is not intended to be used /// directly from application code. /// - public HttpParser(bool showErrorDetails) + public HttpParser(bool showErrorDetails) : this(showErrorDetails, allowLineFeedTerminator: false) { - _showErrorDetails = showErrorDetails; } + internal HttpParser(bool showErrorDetails, bool allowLineFeedTerminator) + { + _showErrorDetails = showErrorDetails; + _allowLineFeedTerminator = allowLineFeedTerminator; + } + // byte types don't have a data type annotation so we pre-cast them; to avoid in-place casts private const byte ByteCR = (byte)'\r'; private const byte ByteLF = (byte)'\n'; @@ -141,11 +147,35 @@ private void ParseRequestLine(TRequestHandler handler, ReadOnlySpan reques // Consume space offset++; - // Version + CR is 9 bytes which should take us to .Length - // LF should have been dropped prior to method call - if ((uint)offset + 9 != (uint)requestLine.Length || requestLine[offset + sizeof(ulong)] != ByteCR) + if (!_allowLineFeedTerminator) { - RejectRequestLine(requestLine); + // Version + CR is 9 bytes which should take us to .Length + // LF should have been dropped prior to method call + if ((uint)offset + 9 != (uint)requestLine.Length || requestLine[offset + 8] != ByteCR) + { + RejectRequestLine(requestLine); + } + } + else + { + // LF should have been dropped prior to method call + // If offset + 8 is .Length then requestLine is valid since it mean LF was the next char + if ((uint)offset + 8 != (uint)requestLine.Length) + { + // Version + CR is 9 bytes which should take us to .Length + if ((uint)offset + 9 != (uint)requestLine.Length || requestLine[offset + 8] != ByteCR) + { + RejectRequestLine(requestLine); + } + } + else + { + // e.g., GET / HTTP1.\r\n + if (requestLine[offset + 7] == ByteCR) + { + RejectRequestLine(requestLine); + } + } } // Version @@ -188,7 +218,7 @@ public bool ParseHeaders(TRequestHandler handler, ref SequenceReader reade else if (reader.TryRead(out ch1)) // Possibly split across spans { // Note if we read ahead by 1 or 2 bytes - readAhead = (reader.TryRead(out ch2)) ? 2 : 1; + readAhead = reader.TryRead(out ch2) ? 2 : 1; } if (ch1 == ByteCR) @@ -231,44 +261,60 @@ public bool ParseHeaders(TRequestHandler handler, ref SequenceReader reade // the bounds check for the next lookup of span[length] if ((uint)length < (uint)span.Length) { - // Early memory read to hide latency - var expectedCR = span[length]; - // Correctly has a CR, move to next - length++; + var headerSpan = span[..length]; - if (expectedCR != ByteCR) + if (length < 4) { - // Sequence needs to be CRLF not LF first. - RejectRequestHeader(span[..length]); + // Less than min possible headerSpan of 3 bytes a:b + RejectRequestHeader(headerSpan); } - if ((uint)length < (uint)span.Length) + // Early memory read to hide latency + var lineTerminator = span[length]; + // Correctly has a CR/LF, move to next + length++; + + if (lineTerminator == ByteLF) { - // Early memory read to hide latency - var expectedLF = span[length]; - // Correctly has a LF, move to next - length++; - - if (expectedLF != ByteLF || - length < 5 || - // Exclude the CRLF from the headerLine and parse the header name:value pair - !TryTakeSingleHeader(handler, span[..(length - 2)])) + if (!_allowLineFeedTerminator) { - // Sequence needs to be CRLF and not contain an inner CR not part of terminator. - // Less than min possible headerSpan of 5 bytes a:b\r\n - // Not parsable as a valid name:value header pair. + // Sequence needs to be CRLF not LF first. RejectRequestHeader(span[..length]); } - // Read the header successfully, skip the reader forward past the headerSpan. - span = span.Slice(length); - reader.Advance(length); } else { - // No enough data, set length to 0. - length = 0; + if ((uint)length < (uint)span.Length) + { + // Early memory read to hide latency + var expectedLF = span[length]; + // Correctly has a LF, move to next + length++; + + if (expectedLF != ByteLF) + { + // Sequence needs to be CRLF not LF first. + RejectRequestHeader(span[..length]); + } + } + else + { + // No enough data, set length to 0. + length = 0; + } + } + + if (length != 0 && !TryTakeSingleHeader(handler, headerSpan)) + { + // Sequence needs to be CRLF and not contain an inner CR not part of terminator. + // Not parsable as a valid name:value header pair. + RejectRequestHeader(span[..length]); } + + // Read the header successfully, skip the reader forward past the headerSpan. + span = span[length..]; + reader.Advance(length); } } @@ -317,14 +363,17 @@ private int ParseMultiSpanHeader(TRequestHandler handler, ref SequenceReader(trace.IsEnabled(LogLevel.Information)), + HttpParser = new HttpParser(trace.IsEnabled(LogLevel.Information), serverOptions.AllowLineFeedTerminator), SystemClock = heartbeatManager, DateHeaderValueManager = dateHeaderValueManager, ConnectionManager = connectionManager, diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index 1bf0ebf264c6..04abfedb537d 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -91,6 +91,15 @@ public class KestrelServerOptions /// public bool DisableStringReuse { get; set; } + /// + /// Gets or sets a value that controls whether the request lines + /// can end with LF only instead of CR/LF. + /// + /// + /// Defaults to false. + /// + public bool AllowLineFeedTerminator { get; set; } + /// /// Controls whether to return the "Alt-Svc" header from an HTTP/2 or lower response for HTTP/3. /// diff --git a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt index d8986b9f2ac5..d3827aa82024 100644 --- a/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Core/src/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ #nullable enable *REMOVED*~Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer.KestrelServer(Microsoft.Extensions.Options.IOptions! options, Microsoft.AspNetCore.Connections.IConnectionListenerFactory! transportFactory, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer.KestrelServer(Microsoft.Extensions.Options.IOptions! options, Microsoft.AspNetCore.Connections.IConnectionListenerFactory! transportFactory, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void +Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.AllowLineFeedTerminator.get -> bool +Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.AllowLineFeedTerminator.set -> void diff --git a/src/Servers/Kestrel/Core/test/HttpParserTests.cs b/src/Servers/Kestrel/Core/test/HttpParserTests.cs index e6898927d644..eb5d1148029d 100644 --- a/src/Servers/Kestrel/Core/test/HttpParserTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpParserTests.cs @@ -22,8 +22,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; public class HttpParserTests : LoggedTest { private static readonly KestrelTrace _nullTrace = new KestrelTrace(NullLoggerFactory.Instance); + private readonly bool _allowLineFeedTerminator; private KestrelTrace CreateEnabledTrace() => new KestrelTrace(LoggerFactory); + public HttpParserTests() : this(false) + { + } + + protected HttpParserTests(bool allowLineFeedTerminator) + { + _allowLineFeedTerminator = allowLineFeedTerminator; + } + [Theory] [MemberData(nameof(RequestLineValidData))] public void ParsesRequestLine( @@ -83,6 +93,48 @@ public void ParseRequestLineDoesNotConsumeIncompleteRequestLine(string requestLi [MemberData(nameof(RequestLineInvalidData))] public void ParseRequestLineThrowsOnInvalidRequestLine(string requestLine) { + // These should fail with or without quirk mode + + var parser = CreateParser(CreateEnabledTrace()); + var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(requestLine)); + var requestHandler = new RequestHandler(); + +#pragma warning disable CS0618 // Type or member is obsolete + var exception = Assert.Throws(() => +#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(RequestLineInvalidDataLineFeedTerminator))] + public void ParseRequestSucceedsOnInvalidRequestLineQuirkMode(string requestLine) + { + // Skip test when quirk mode is not enabled + if (!_allowLineFeedTerminator) + { + return; + } + + var parser = CreateParser(CreateEnabledTrace()); + var buffer = new ReadOnlySequence(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 ParseRequestLineThrowsOnInvalidRequestLineQuirkMode(string requestLine) + { + // Skip test when quirk mode is enabled + if (_allowLineFeedTerminator) + { + return; + } + var parser = CreateParser(CreateEnabledTrace()); var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(requestLine)); var requestHandler = new RequestHandler(); @@ -241,6 +293,76 @@ public void ParseHeadersCanParseEmptyHeaderValues( VerifyRawHeaders(rawHeaders, expectedHeaderNames, expectedHeaderValues); } + [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 ParseHeadersCanParseSingleLineFeedInQuirkMode( + string rawHeaders, + string expectedHeaderName1, + string expectedHeaderValue1, + string expectedHeaderName2, + string expectedHeaderValue2) + { + // Skip test when quirk mode is enabled + if (_allowLineFeedTerminator) + { + return; + } + + 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(() => VerifyRawHeaders(rawHeaders, expectedHeaderNames, expectedHeaderValues)); +#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 ParseHeadersCantParseSingleLineFeedWithoutQuirkMode( + string rawHeaders, + string expectedHeaderName1, + string expectedHeaderValue1, + string expectedHeaderName2, + string expectedHeaderValue2) + { + // Skip test when quirk mode is not enabled + if (!_allowLineFeedTerminator) + { + return; + } + + 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")] @@ -316,6 +438,32 @@ public void ParseHeadersThrowsOnInvalidRequestHeaders(string rawHeaders, string var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(rawHeaders)); var requestHandler = new RequestHandler(); +#pragma warning disable CS0618 // Type or member is obsolete + var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete + { + var reader = new SequenceReader(buffer); + parser.ParseHeaders(requestHandler, ref reader); + }); + + Assert.Equal(expectedExceptionMessage, exception.Message); + Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); + } + + [Theory] + [MemberData(nameof(RequestHeaderInvalidDataLineFeedTerminator))] + public void ParseHeadersThrowsOnInvalidRequestHeadersQuirkMode(string rawHeaders, string expectedExceptionMessage) + { + if (_allowLineFeedTerminator) + { + return; + } + + var parser = CreateParser(CreateEnabledTrace()); + + var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(rawHeaders)); + var requestHandler = new RequestHandler(); + #pragma warning disable CS0618 // Type or member is obsolete var exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete @@ -331,6 +479,11 @@ public void ParseHeadersThrowsOnInvalidRequestHeaders(string rawHeaders, string [Fact] public void ExceptionDetailNotIncludedWhenLogLevelInformationNotEnabled() { + if (_allowLineFeedTerminator) + { + return; + } + var parser = CreateParser(_nullTrace); // Invalid request line @@ -356,19 +509,22 @@ public void ExceptionDetailNotIncludedWhenLogLevelInformationNotEnabled() Assert.Equal(CoreStrings.FormatBadRequest_UnrecognizedHTTPVersion(string.Empty), exception.Message); Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, exception.StatusCode); - // Invalid request header - buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes("Header: value\n\r\n")); + if (!_allowLineFeedTerminator) + { + // Invalid request header + buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes("Header: value\n\r\n")); #pragma warning disable CS0618 // Type or member is obsolete - exception = Assert.Throws(() => + exception = Assert.Throws(() => #pragma warning restore CS0618 // Type or member is obsolete - { - var reader = new SequenceReader(buffer); - parser.ParseHeaders(requestHandler, ref reader); - }); + { + var reader = new SequenceReader(buffer); + parser.ParseHeaders(requestHandler, ref reader); + }); - Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(string.Empty), exception.Message); - Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); + Assert.Equal(CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(string.Empty), exception.Message); + Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); + } } [Fact] @@ -426,11 +582,39 @@ public void ParseHeadersThrowsOnInvalidRequestHeadersWithGratuitouslySplitBuffer Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); } - [Fact] - public void ParseHeadersWithGratuitouslySplitBuffers() + [Theory] + [MemberData(nameof(RequestHeaderInvalidDataLineFeedTerminator))] + public void ParseHeadersThrowsOnInvalidRequestHeadersWithGratuitouslySplitBuffersQuirkMode(string rawHeaders, string expectedExceptionMessage) + { + if (_allowLineFeedTerminator) + { + return; + } + + var parser = CreateParser(CreateEnabledTrace()); + + var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(rawHeaders); + var requestHandler = new RequestHandler(); + +#pragma warning disable CS0618 // Type or member is obsolete + var exception = Assert.Throws(() => +#pragma warning restore CS0618 // Type or member is obsolete + { + var reader = new SequenceReader(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); - var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent("Host:\r\nConnection: keep-alive\r\n\r\n"); + var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(headers); var requestHandler = new RequestHandler(); var reader = new SequenceReader(buffer); @@ -439,11 +623,21 @@ public void ParseHeadersWithGratuitouslySplitBuffers() Assert.True(result); } - [Fact] - public void ParseHeadersWithGratuitouslySplitBuffers2() + [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")] + public void ParseHeadersWithGratuitouslySplitBuffersQuirkMode(string headers) { + // Skip test when quirk mode is not enabled + if (!_allowLineFeedTerminator) + { + return; + } + var parser = CreateParser(_nullTrace); - var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent("A:B\r\nB: C\r\n\r\n"); + var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(headers); var requestHandler = new RequestHandler(); var reader = new SequenceReader(buffer); @@ -451,7 +645,6 @@ public void ParseHeadersWithGratuitouslySplitBuffers2() Assert.True(result); } - private bool ParseRequestLine(IHttpParser parser, RequestHandler requestHandler, ReadOnlySequence readableBuffer, out SequencePosition consumed, out SequencePosition examined) { var reader = new SequenceReader(readableBuffer); @@ -507,10 +700,12 @@ private void VerifyRawHeaders(string rawHeaders, IEnumerable expectedHea Assert.True(buffer.Slice(reader.Position).IsEmpty); } - private IHttpParser CreateParser(KestrelTrace log) => new HttpParser(log.IsEnabled(LogLevel.Information)); + private IHttpParser CreateParser(KestrelTrace log) => new HttpParser(log.IsEnabled(LogLevel.Information), _allowLineFeedTerminator); public static IEnumerable RequestLineValidData => HttpParsingData.RequestLineValidData; + public static IEnumerable RequestLineInvalidDataLineFeedTerminator => HttpParsingData.RequestLineInvalidDataLineFeedTerminator.Select(requestLine => new[] { requestLine }); + public static IEnumerable RequestLineIncompleteData => HttpParsingData.RequestLineIncompleteData.Select(requestLine => new[] { requestLine }); public static IEnumerable RequestLineInvalidData => HttpParsingData.RequestLineInvalidData.Select(requestLine => new[] { requestLine }); @@ -521,6 +716,8 @@ private void VerifyRawHeaders(string rawHeaders, IEnumerable expectedHea public static IEnumerable RequestHeaderInvalidData => HttpParsingData.RequestHeaderInvalidData; + public static IEnumerable RequestHeaderInvalidDataLineFeedTerminator => HttpParsingData.RequestHeaderInvalidDataLineFeedTerminator; + private class RequestHandler : IHttpRequestLineHandler, IHttpHeadersHandler { public string Method { get; set; } @@ -606,3 +803,11 @@ public override ReadOnlySequence CreateWithContent(byte[] data) } } } + +// Ensure that all common tests are still passing when the AcceptLineFeedAsLineTerminator quirk mode is enabled. +public class HttpParserQuirkModeTests : HttpParserTests +{ + public HttpParserQuirkModeTests() : base(true) + { + } +} diff --git a/src/Servers/Kestrel/shared/test/HttpParsingData.cs b/src/Servers/Kestrel/shared/test/HttpParsingData.cs index 8baf18513ced..c57cec7fc1d2 100644 --- a/src/Servers/Kestrel/shared/test/HttpParsingData.cs +++ b/src/Servers/Kestrel/shared/test/HttpParsingData.cs @@ -160,8 +160,6 @@ public static IEnumerable RequestLineInvalidData "GET HTTP/1.0\r\n", "GET HTTP/1.1\r\n", "GET / \n", - "GET / HTTP/1.0\n", - "GET / HTTP/1.1\n", "GET / HTTP/1.0\rA\n", "GET / HTTP/1.1\ra\n", "GET / H\r\n", @@ -191,8 +189,6 @@ public static IEnumerable RequestLineInvalidData "CUSTOM HTTP/1.0\r\n", "CUSTOM HTTP/1.1\r\n", "CUSTOM / \n", - "CUSTOM / HTTP/1.0\n", - "CUSTOM / HTTP/1.1\n", "CUSTOM / HTTP/1.0\rA\n", "CUSTOM / HTTP/1.1\ra\n", "CUSTOM / H\r\n", @@ -205,7 +201,15 @@ public static IEnumerable RequestLineInvalidData "CUSTOM / HTTP/1.1a\n", "CUSTOM / HTTP/1.1a\r\n", "CUSTOM / HTTP/1.1ab\r\n", + "CUSTOM / H\n", + "CUSTOM / HT\n", + "CUSTOM / HTT\n", + "CUSTOM / HTTP\n", + "CUSTOM / HTTP/\n", + "CUSTOM / HTTP/1\n", + "CUSTOM / HTTP/1.\n", "CUSTOM / hello\r\n", + "CUSTOM / hello\n", "CUSTOM ? HTTP/1.1\r\n", "CUSTOM /a?b=cHTTP/1.1\r\n", "CUSTOM /a%20bHTTP/1.1\r\n", @@ -217,6 +221,21 @@ public static IEnumerable RequestLineInvalidData } } + // This list is valid in quirk mode + public static IEnumerable RequestLineInvalidDataLineFeedTerminator + { + get + { + return new[] + { + "GET / HTTP/1.0\n", + "GET / HTTP/1.1\n", + "CUSTOM / HTTP/1.0\n", + "CUSTOM / HTTP/1.1\n", + }; + } + } + // Bad HTTP Methods (invalid according to RFC) public static IEnumerable MethodWithNonTokenCharData { @@ -351,96 +370,98 @@ public static IEnumerable TargetWithNullCharData public static IEnumerable QueryStringWithNullCharData => new[] { - "/?\0=a", - "/?a=\0", - }; + "/?\0=a", + "/?a=\0", + }; public static TheoryData UnrecognizedHttpVersionData => new TheoryData - { - "http/1.0", - "http/1.1", - "HTTP/1.2", - "HTTP/3.0", - "8charact", - }; + { + "http/1.0", + "http/1.1", + "HTTP/1.2", + "HTTP/3.0", + "8charact", + }; + + public static IEnumerable RequestHeaderInvalidDataLineFeedTerminator => new[] + { + // Missing CR + new[] { "Header: value\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header: value\x0A") }, + new[] { "Header-1: value1\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: value1\x0A") }, + new[] { "Header-1: value1\r\nHeader-2: value2\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2: value2\x0A") }, + }; public static IEnumerable RequestHeaderInvalidData => new[] { - // Missing CR - new[] { "Header: value\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header: value\x0A") }, - new[] { "Header-1: value1\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: value1\x0A") }, - new[] { "Header-1: value1\r\nHeader-2: value2\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2: value2\x0A") }, - - // Line folding - new[] { "Header: line1\r\n line2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" line2\x0D\x0A") }, - new[] { "Header: line1\r\n\tline2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09line2\x0D\x0A") }, - new[] { "Header: line1\r\n line2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" line2\x0D\x0A") }, - new[] { "Header: line1\r\n \tline2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" \x09line2\x0D\x0A") }, - new[] { "Header: line1\r\n\t line2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09 line2\x0D\x0A") }, - new[] { "Header: line1\r\n\t\tline2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09\x09line2\x0D\x0A") }, - new[] { "Header: line1\r\n \t\t line2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" \x09\x09 line2\x0D\x0A") }, - new[] { "Header: line1\r\n \t \t line2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" \x09 \x09 line2\x0D\x0A") }, - new[] { "Header-1: multi\r\n line\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" line\x0D\x0A") }, - new[] { "Header-1: value1\r\nHeader-2: multi\r\n line\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" line\x0D\x0A") }, - new[] { "Header-1: value1\r\n Header-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" Header-2: value2\x0D\x0A") }, - new[] { "Header-1: value1\r\n\tHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09Header-2: value2\x0D\x0A") }, - - // CR in value - new[] { "Header-1: value1\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: value1\x0D\x0D") }, - new[] { "Header-1: val\rue1\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: val\x0Du") }, - new[] { "Header-1: value1\rHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: value1\x0DH") }, - new[] { "Header-1: value1\r\nHeader-2: value2\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2: value2\x0D\x0D") }, - new[] { "Header-1: value1\r\nHeader-2: v\ralue2\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2: v\x0Da") }, - new[] { "Header-1: Value__\rVector16________Vector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value__\x0DV") }, - new[] { "Header-1: Value___Vector16\r________Vector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16\x0D_") }, - new[] { "Header-1: Value___Vector16_______\rVector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16_______\x0DV") }, - new[] { "Header-1: Value___Vector16________Vector32\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32\x0D\x0D") }, - new[] { "Header-1: Value___Vector16________Vector32_\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32_\x0D\x0D") }, - new[] { "Header-1: Value___Vector16________Vector32Value___Vector16_______\rVector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32Value___Vector16_______\x0DV") }, - new[] { "Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\x0D\x0D") }, - new[] { "Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\x0D\x0D") }, - - // Missing colon - new[] { "Header-1 value1\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1 value1\x0D\x0A") }, - new[] { "Header-1 value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1 value1\x0D\x0A") }, - new[] { "Header-1: value1\r\nHeader-2 value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2 value2\x0D\x0A") }, - new[] { "\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x0A") }, - - // Starting with whitespace - new[] { " Header: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" Header: value\x0D\x0A") }, - new[] { "\tHeader: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09Header: value\x0D\x0A") }, - new[] { " Header-1: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" Header-1: value1\x0D\x0A") }, - new[] { "\tHeader-1: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09Header-1: value1\x0D\x0A") }, - - // Whitespace in header name - new[] { "Header : value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header : value\x0D\x0A") }, - new[] { "Header\t: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header\x09: value\x0D\x0A") }, - new[] { "Header\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header\x0D:") }, - new[] { "Header_\rVector16: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header_\x0DV") }, - new[] { "Header__Vector16\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16\x0D:") }, - new[] { "Header__Vector16_\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16_\x0D:") }, - new[] { "Header_\rVector16________Vector32: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header_\x0DV") }, - new[] { "Header__Vector16________Vector32\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32\x0D:") }, - new[] { "Header__Vector16________Vector32_\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32_\x0D:") }, - new[] { "Header__Vector16________Vector32Header_\rVector16________Vector32: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32Header_\x0DV") }, - new[] { "Header__Vector16________Vector32Header__Vector16________Vector32\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32Header__Vector16________Vector32\x0D:") }, - new[] { "Header__Vector16________Vector32Header__Vector16________Vector32_\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32Header__Vector16________Vector32_\x0D:") }, - new[] { "Header 1: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 1: value1\x0D\x0A") }, - new[] { "Header 1 : value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 1 : value1\x0D\x0A") }, - new[] { "Header 1\t: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 1\x09: value1\x0D\x0A") }, - new[] { "Header 1\r: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 1\x0D:") }, - new[] { "Header-1: value1\r\nHeader 2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 2: value2\x0D\x0A") }, - new[] { "Header-1: value1\r\nHeader-2 : value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2 : value2\x0D\x0A") }, - new[] { "Header-1: value1\r\nHeader-2\t: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2\x09: value2\x0D\x0A") }, - - // Headers not ending in CRLF line - new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r\r", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF }, - new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r ", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF }, - new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r \n", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF }, - - // Empty header name - new[] { ": value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@": value\x0D\x0A") }, - }; + // Line folding + new[] { "Header: line1\r\n line2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" line2\x0D\x0A") }, + new[] { "Header: line1\r\n\tline2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09line2\x0D\x0A") }, + new[] { "Header: line1\r\n line2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" line2\x0D\x0A") }, + new[] { "Header: line1\r\n \tline2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" \x09line2\x0D\x0A") }, + new[] { "Header: line1\r\n\t line2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09 line2\x0D\x0A") }, + new[] { "Header: line1\r\n\t\tline2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09\x09line2\x0D\x0A") }, + new[] { "Header: line1\r\n \t\t line2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" \x09\x09 line2\x0D\x0A") }, + new[] { "Header: line1\r\n \t \t line2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" \x09 \x09 line2\x0D\x0A") }, + new[] { "Header-1: multi\r\n line\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" line\x0D\x0A") }, + new[] { "Header-1: value1\r\nHeader-2: multi\r\n line\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" line\x0D\x0A") }, + new[] { "Header-1: value1\r\n Header-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" Header-2: value2\x0D\x0A") }, + new[] { "Header-1: value1\r\n\tHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09Header-2: value2\x0D\x0A") }, + + // CR in value + new[] { "Header-1: value1\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: value1\x0D\x0D") }, + new[] { "Header-1: val\rue1\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: val\x0Du") }, + new[] { "Header-1: value1\rHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: value1\x0DH") }, + new[] { "Header-1: value1\r\nHeader-2: value2\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2: value2\x0D\x0D") }, + new[] { "Header-1: value1\r\nHeader-2: v\ralue2\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2: v\x0Da") }, + new[] { "Header-1: Value__\rVector16________Vector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value__\x0DV") }, + new[] { "Header-1: Value___Vector16\r________Vector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16\x0D_") }, + new[] { "Header-1: Value___Vector16_______\rVector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16_______\x0DV") }, + new[] { "Header-1: Value___Vector16________Vector32\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32\x0D\x0D") }, + new[] { "Header-1: Value___Vector16________Vector32_\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32_\x0D\x0D") }, + new[] { "Header-1: Value___Vector16________Vector32Value___Vector16_______\rVector32\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32Value___Vector16_______\x0DV") }, + new[] { "Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32Value___Vector16________Vector32\x0D\x0D") }, + new[] { "Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\r\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: Value___Vector16________Vector32Value___Vector16________Vector32_\x0D\x0D") }, + + // Missing colon + new[] { "Header-1 value1\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1 value1\x0D\x0A") }, + new[] { "Header-1 value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1 value1\x0D\x0A") }, + new[] { "Header-1: value1\r\nHeader-2 value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2 value2\x0D\x0A") }, + + // Starting with whitespace + new[] { " Header: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" Header: value\x0D\x0A") }, + new[] { "\tHeader: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09Header: value\x0D\x0A") }, + new[] { " Header-1: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" Header-1: value1\x0D\x0A") }, + new[] { "\tHeader-1: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09Header-1: value1\x0D\x0A") }, + + // Whitespace in header name + new[] { "Header : value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header : value\x0D\x0A") }, + new[] { "Header\t: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header\x09: value\x0D\x0A") }, + new[] { "Header\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header\x0D:") }, + new[] { "Header_\rVector16: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header_\x0DV") }, + new[] { "Header__Vector16\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16\x0D:") }, + new[] { "Header__Vector16_\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16_\x0D:") }, + new[] { "Header_\rVector16________Vector32: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header_\x0DV") }, + new[] { "Header__Vector16________Vector32\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32\x0D:") }, + new[] { "Header__Vector16________Vector32_\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32_\x0D:") }, + new[] { "Header__Vector16________Vector32Header_\rVector16________Vector32: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32Header_\x0DV") }, + new[] { "Header__Vector16________Vector32Header__Vector16________Vector32\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32Header__Vector16________Vector32\x0D:") }, + new[] { "Header__Vector16________Vector32Header__Vector16________Vector32_\r: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header__Vector16________Vector32Header__Vector16________Vector32_\x0D:") }, + new[] { "Header 1: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 1: value1\x0D\x0A") }, + new[] { "Header 1 : value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 1 : value1\x0D\x0A") }, + new[] { "Header 1\t: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 1\x09: value1\x0D\x0A") }, + new[] { "Header 1\r: value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 1\x0D:") }, + new[] { "Header-1: value1\r\nHeader 2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header 2: value2\x0D\x0A") }, + new[] { "Header-1: value1\r\nHeader-2 : value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2 : value2\x0D\x0A") }, + new[] { "Header-1: value1\r\nHeader-2\t: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2\x09: value2\x0D\x0A") }, + + // Headers not ending in CRLF line + new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r\r", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF }, + new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r ", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF }, + new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r \n", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF }, + + // Empty header name + new[] { ": value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@": value\x0D\x0A") }, + }; public static TheoryData HostHeaderData => new TheoryData diff --git a/src/Servers/Kestrel/shared/test/TestServiceContext.cs b/src/Servers/Kestrel/shared/test/TestServiceContext.cs index e4fb4e4fd585..ae6f0c21b846 100644 --- a/src/Servers/Kestrel/shared/test/TestServiceContext.cs +++ b/src/Servers/Kestrel/shared/test/TestServiceContext.cs @@ -17,17 +17,17 @@ internal class TestServiceContext : ServiceContext { public TestServiceContext() { - Initialize(NullLoggerFactory.Instance, CreateLoggingTrace(NullLoggerFactory.Instance)); + Initialize(NullLoggerFactory.Instance, CreateLoggingTrace(NullLoggerFactory.Instance), false); } - public TestServiceContext(ILoggerFactory loggerFactory) + public TestServiceContext(ILoggerFactory loggerFactory, bool allowLineFeedTerminator = false) { - Initialize(loggerFactory, CreateLoggingTrace(loggerFactory)); + Initialize(loggerFactory, CreateLoggingTrace(loggerFactory), allowLineFeedTerminator); } - public TestServiceContext(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace) + public TestServiceContext(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, bool allowLineFeedTerminator = false) { - Initialize(loggerFactory, kestrelTrace); + Initialize(loggerFactory, kestrelTrace, allowLineFeedTerminator); } private static KestrelTrace CreateLoggingTrace(ILoggerFactory loggerFactory) @@ -49,7 +49,7 @@ public void InitializeHeartbeat() SystemClock = heartbeatManager; } - private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace) + private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, bool allowLineFeedTerminator) { LoggerFactory = loggerFactory; Log = kestrelTrace; @@ -58,7 +58,7 @@ private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace) SystemClock = MockSystemClock; DateHeaderValueManager = new DateHeaderValueManager(); ConnectionManager = new ConnectionManager(Log, ResourceCounter.Unlimited); - HttpParser = new HttpParser(Log.IsEnabled(LogLevel.Information)); + HttpParser = new HttpParser(Log.IsEnabled(LogLevel.Information), allowLineFeedTerminator); ServerOptions = new KestrelServerOptions { AddServerHeader = false diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs index 4bc3117769af..0ad7c5623dda 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs @@ -18,6 +18,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; public class BadHttpRequestTests : LoggedTest { + private readonly bool _allowLineFeedTerminator; + + public BadHttpRequestTests() : this(false) + { + } + + protected BadHttpRequestTests(bool allowLineFeedTerminator) + { + _allowLineFeedTerminator = allowLineFeedTerminator; + } + [Theory] [MemberData(nameof(InvalidRequestLineData))] public Task TestInvalidRequestLines(string request, string expectedExceptionMessage) @@ -48,6 +59,27 @@ public Task TestInvalidHeaders(string rawHeaders, string expectedExceptionMessag expectedExceptionMessage); } + [Theory] + [MemberData(nameof(InvalidRequestHeaderDataLineFeedTerminator))] + public async Task TestInvalidHeadersQuirkMode(string rawHeaders, string expectedExceptionMessage) + { + var task = TestBadRequest( + $"GET / HTTP/1.1\r\n{rawHeaders}", + "400 Bad Request", + expectedExceptionMessage); + + // These should not fail in quirk mode + if (_allowLineFeedTerminator) + { + // Unit test should fail since it expects a bad request + await Assert.ThrowsAnyAsync(async () => await task); + } + else + { + // Should fail successfully + await task; + } + } public static Dictionary BadHeaderData => new Dictionary { { "Hea\0der: value".EscapeNonPrintable(), ("Hea\0der: value", "Invalid characters in header name.") }, @@ -266,7 +298,7 @@ private async Task TestBadRequest(string request, string expectedResponseStatusC } }); - await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory) { DiagnosticSource = diagListener })) + await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory, _allowLineFeedTerminator) { DiagnosticSource = diagListener })) { using (var connection = server.CreateConnection()) { @@ -328,6 +360,14 @@ public static TheoryData InvalidRequestLineData public static TheoryData UnrecognizedHttpVersionData => HttpParsingData.UnrecognizedHttpVersionData; public static IEnumerable InvalidRequestHeaderData => HttpParsingData.RequestHeaderInvalidData; - + public static IEnumerable InvalidRequestHeaderDataLineFeedTerminator => HttpParsingData.RequestHeaderInvalidDataLineFeedTerminator; public static TheoryData InvalidHostHeaderData => HttpParsingData.HostHeaderInvalidData; } + +// Ensure that all common tests are still passing when the AllowLineFeedAsLineTerminator quirk mode is enabled. +public class BadHttpRequestTestsQuirksMode : BadHttpRequestTests +{ + public BadHttpRequestTestsQuirksMode() : base(true) + { + } +}