diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj
index 884967d73d78..8c3fd43461fb 100644
--- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj
+++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj
@@ -69,6 +69,7 @@
+
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs
index 66282381cb8c..2fafdb58a57b 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs
@@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
public class HttpParser : IHttpParser where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler
{
private readonly bool _showErrorDetails;
+ private readonly bool _disableHttp1LineFeedTerminators;
///
/// This API supports framework infrastructure and is not intended to be used
@@ -34,9 +35,14 @@ 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, AppContext.TryGetSwitch(KestrelServerOptions.DisableHttp1LineFeedTerminatorsSwitchKey, out var disabled) && disabled)
+ {
+ }
+
+ internal HttpParser(bool showErrorDetails, bool disableHttp1LineFeedTerminators)
{
_showErrorDetails = showErrorDetails;
+ _disableHttp1LineFeedTerminators = disableHttp1LineFeedTerminators;
}
// byte types don't have a data type annotation so we pre-cast them; to avoid in-place casts
@@ -135,9 +141,15 @@ private void ParseRequestLine(TRequestHandler handler, ReadOnlySpan reques
// 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 ((uint)offset + 9 != (uint)requestLine.Length || requestLine[offset + 8] != ByteCR)
{
- RejectRequestLine(requestLine);
+ // LF should have been dropped prior to method call
+ // If !_disableHttp1LineFeedTerminators and offset + 8 is .Length,
+ // then requestLine is valid since it means LF was the next char
+ if (_disableHttp1LineFeedTerminators || (uint)offset + 8 != (uint)requestLine.Length)
+ {
+ RejectRequestLine(requestLine);
+ }
}
// Version
@@ -164,135 +176,142 @@ public bool ParseHeaders(TRequestHandler handler, ref SequenceReader reade
{
while (!reader.End)
{
+ // Check if the reader's span contains an LF to skip the reader if possible
var span = reader.UnreadSpan;
- while (span.Length > 0)
+
+ // Fast path, CR/LF at the beginning
+ if (span.Length >= 2 && span[0] == ByteCR && span[1] == ByteLF)
{
- byte ch1;
- var ch2 = (byte)0;
- var readAhead = 0;
+ reader.Advance(2);
+ handler.OnHeadersComplete(endStream: false);
+ return true;
+ }
- // Fast path, we're still looking at the same span
- if (span.Length >= 2)
- {
- ch1 = span[0];
- ch2 = span[1];
- }
- else if (reader.TryRead(out ch1)) // Possibly split across spans
- {
- // Note if we read ahead by 1 or 2 bytes
- readAhead = (reader.TryRead(out ch2)) ? 2 : 1;
- }
+ var foundCrlf = false;
- if (ch1 == ByteCR)
+ var lfOrCrIndex = span.IndexOfAny(ByteCR, ByteLF);
+ if (lfOrCrIndex >= 0)
+ {
+ if (span[lfOrCrIndex] == ByteCR)
{
- // Check for final CRLF.
- if (ch2 == ByteLF)
- {
- // If we got 2 bytes from the span directly so skip ahead 2 so that
- // the reader's state matches what we expect
- if (readAhead == 0)
- {
- reader.Advance(2);
- }
+ // We got a CR. Is this a CR/LF sequence?
+ var crIndex = lfOrCrIndex;
+ reader.Advance(crIndex + 1);
- // Double CRLF found, so end of headers.
- handler.OnHeadersComplete(endStream: false);
- return true;
+ bool hasDataAfterCr;
+
+ if ((uint)span.Length > (uint)(crIndex + 1) && span[crIndex + 1] == ByteLF)
+ {
+ // CR/LF in the same span (common case)
+ span = span.Slice(0, crIndex);
+ foundCrlf = true;
}
- else if (readAhead == 1)
+ else if ((hasDataAfterCr = reader.TryPeek(out byte lfMaybe)) && lfMaybe == ByteLF)
{
- // Didn't read 2 bytes, reset the reader so we don't consume anything
- reader.Rewind(1);
- return false;
+ // CR/LF but split between spans
+ span = span.Slice(0, span.Length - 1);
+ foundCrlf = true;
}
-
- Debug.Assert(readAhead == 0 || readAhead == 2);
- // Headers don't end in CRLF line.
-
- KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestHeadersNoCRLF);
- }
-
- var length = 0;
- // We only need to look for the end if we didn't read ahead; otherwise there isn't enough in
- // in the span to contain a header.
- if (readAhead == 0)
- {
- length = span.IndexOfAny(ByteCR, ByteLF);
- // If not found length with be -1; casting to uint will turn it to uint.MaxValue
- // which will be larger than any possible span.Length. This also serves to eliminate
- // the bounds check for the next lookup of span[length]
- if ((uint)length < (uint)span.Length)
+ else
{
- // Early memory read to hide latency
- var expectedCR = span[length];
- // Correctly has a CR, move to next
- length++;
-
- if (expectedCR != ByteCR)
+ // What's after the CR?
+ if (!hasDataAfterCr)
{
- // Sequence needs to be CRLF not LF first.
- RejectRequestHeader(span[..length]);
+ // No more chars after CR? Don't consume an incomplete header
+ reader.Rewind(crIndex + 1);
+ return false;
}
-
- if ((uint)length < (uint)span.Length)
+ else if (crIndex == 0)
{
- // 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)]))
- {
- // 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.
- RejectRequestHeader(span[..length]);
- }
-
- // Read the header successfully, skip the reader forward past the headerSpan.
- span = span.Slice(length);
- reader.Advance(length);
+ // CR followed by something other than LF
+ KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestHeadersNoCRLF);
}
else
{
- // No enough data, set length to 0.
- length = 0;
+ // Include the thing after the CR in the rejection exception.
+ var stopIndex = crIndex + 2;
+ RejectRequestHeader(span[..stopIndex]);
}
}
- }
- // End found in current span
- if (length > 0)
- {
- continue;
- }
+ if (foundCrlf)
+ {
+ // Advance past the LF too
+ reader.Advance(1);
- // We moved the reader to look ahead 2 bytes so rewind the reader
- if (readAhead > 0)
- {
- reader.Rewind(readAhead);
+ // Empty line?
+ if (crIndex == 0)
+ {
+ handler.OnHeadersComplete(endStream: false);
+ return true;
+ }
+ }
}
+ else
+ {
+ // We got an LF with no CR before it.
+ var lfIndex = lfOrCrIndex;
+ if (_disableHttp1LineFeedTerminators)
+ {
+ RejectRequestHeader(AppendEndOfLine(span[..lfIndex], lineFeedOnly: true));
+ }
- length = ParseMultiSpanHeader(handler, ref reader);
+ // Consume the header including the LF
+ reader.Advance(lfIndex + 1);
+
+ span = span.Slice(0, lfIndex);
+ if (span.Length == 0)
+ {
+ handler.OnHeadersComplete(endStream: false);
+ return true;
+ }
+ }
+ }
+ else
+ {
+ // No CR or LF. Is this a multi-span header?
+ int length = ParseMultiSpanHeader(handler, ref reader);
if (length < 0)
{
- // Not there
+ // Not multi-line, just bad.
return false;
}
+ // This was a multi-line header. Advance the reader.
reader.Advance(length);
- // As we crossed spans set the current span to default
- // so we move to the next span on the next iteration
- span = default;
+
+ continue;
+ }
+
+ // We got to a point where we believe we have a header.
+ if (!TryTakeSingleHeader(handler, span))
+ {
+ // 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(AppendEndOfLine(span, lineFeedOnly: !foundCrlf));
}
}
return false;
}
+ private static byte[] AppendEndOfLine(ReadOnlySpan span, bool lineFeedOnly)
+ {
+ var array = new byte[span.Length + (lineFeedOnly ? 1 : 2)];
+
+ span.CopyTo(array);
+ array[^1] = ByteLF;
+
+ if (!lineFeedOnly)
+ {
+ array[^2] = ByteCR;
+ }
+
+ return array;
+ }
+
+ // Parse a header that might cross multiple spans, and return the length of the header
+ // or -1 if there was a failure during parsing.
private int ParseMultiSpanHeader(TRequestHandler handler, ref SequenceReader reader)
{
var currentSlice = reader.UnreadSequence;
@@ -305,45 +324,84 @@ private int ParseMultiSpanHeader(TRequestHandler handler, ref SequenceReader headerSpan;
+ ReadOnlySequence header;
+
+ var firstLineEndCharPos = lineEndPosition.Value;
+ currentSlice.TryGet(ref firstLineEndCharPos, out var s);
+ var firstEolChar = s.Span[0];
+
+ // Is the first EOL char the last of the current slice?
if (currentSlice.Slice(reader.Position, lineEndPosition.Value).Length == currentSlice.Length - 1)
{
- // No enough data, so CRLF can't currently be there.
- // However, we need to check the found char is CR and not LF
-
- // Advance 1 to include CR/LF in lineEnd
- lineEnd = currentSlice.GetPosition(1, lineEndPosition.Value);
- var header = currentSlice.Slice(reader.Position, lineEnd);
- headerSpan = header.IsSingleSegment ? header.FirstSpan : header.ToArray();
- if (headerSpan[^1] != ByteCR)
+ // Get the EOL char
+ if (firstEolChar == ByteCR)
+ {
+ // CR without LF, can't read the header
+ return -1;
+ }
+ else
{
- RejectRequestHeader(headerSpan);
+ if (_disableHttp1LineFeedTerminators)
+ {
+ // LF only but disabled
+
+ // Advance 1 to include LF in result
+ lineEnd = currentSlice.GetPosition(1, lineEndPosition.Value);
+ RejectRequestHeader(currentSlice.Slice(reader.Position, lineEnd).ToSpan());
+ }
}
+ }
+
+ // At this point the first EOL char is not the last byte in the current slice
+
+ // Offset 1 to include the first EOL char.
+ firstLineEndCharPos = currentSlice.GetPosition(1, lineEndPosition.Value);
+
+ if (firstEolChar == ByteCR)
+ {
+ // First EOL char is CR, include the char after CR
+ lineEnd = currentSlice.GetPosition(2, lineEndPosition.Value);
+ header = currentSlice.Slice(reader.Position, lineEnd);
+ }
+ else if (_disableHttp1LineFeedTerminators)
+ {
+ // The terminator is an LF and we don't allow it.
+ RejectRequestHeader(currentSlice.Slice(reader.Position, firstLineEndCharPos).ToSpan());
return -1;
}
+ else
+ {
+ // First EOL char is LF. only include this one
+ lineEnd = currentSlice.GetPosition(1, lineEndPosition.Value);
+ header = currentSlice.Slice(reader.Position, lineEnd);
+ }
- // Advance 2 to include CR{LF?} in lineEnd
- lineEnd = currentSlice.GetPosition(2, lineEndPosition.Value);
- headerSpan = currentSlice.Slice(reader.Position, lineEnd).ToSpan();
+ var headerSpan = header.ToSpan();
- if (headerSpan.Length < 5)
+ // 'a:b\n' or 'a:b\r\n'
+ var minHeaderSpan = _disableHttp1LineFeedTerminators ? 5 : 4;
+ if (headerSpan.Length < minHeaderSpan)
{
- // Less than min possible headerSpan is 5 bytes a:b\r\n
RejectRequestHeader(headerSpan);
}
- if (headerSpan[^2] != ByteCR)
+ var terminatorSize = -1;
+
+ if (headerSpan[^1] == ByteLF)
{
- // Sequence needs to be CRLF not LF first.
- RejectRequestHeader(headerSpan[..^1]);
+ if (headerSpan[^2] == ByteCR)
+ {
+ terminatorSize = 2;
+ }
+ else if (!_disableHttp1LineFeedTerminators)
+ {
+ terminatorSize = 1;
+ }
}
- if (headerSpan[^1] != ByteLF ||
- // Exclude the CRLF from the headerLine and parse the header name:value pair
- !TryTakeSingleHeader(handler, headerSpan[..^2]))
+ // Last chance to bail if the terminator size is not valid or the header doesn't parse.
+ if (terminatorSize == -1 || !TryTakeSingleHeader(handler, headerSpan.Slice(0, headerSpan.Length - terminatorSize)))
{
- // 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(headerSpan);
}
@@ -438,7 +496,7 @@ private static bool TryTakeSingleHeader(TRequestHandler handler, ReadOnlySpan(trace.IsEnabled(LogLevel.Information)),
+ HttpParser = new HttpParser(trace.IsEnabled(LogLevel.Information), serverOptions.DisableHttp1LineFeedTerminators),
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 317edfdfd14a..3dbbdee155d6 100644
--- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
+++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
@@ -25,6 +25,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core;
///
public class KestrelServerOptions
{
+ internal const string DisableHttp1LineFeedTerminatorsSwitchKey = "Microsoft.AspNetCore.Server.Kestrel.DisableHttp1LineFeedTerminators";
+
// internal to fast-path header decoding when RequestHeaderEncodingSelector is unchanged.
internal static readonly Func DefaultHeaderEncodingSelector = _ => null;
@@ -175,6 +177,24 @@ internal bool EnableWebTransportAndH3Datagrams
set => _enableWebTransportAndH3Datagrams = value;
}
+ ///
+ /// Internal AppContext switch to toggle whether a request line can end with LF only instead of CR/LF.
+ ///
+ private bool? _disableHttp1LineFeedTerminators;
+ internal bool DisableHttp1LineFeedTerminators
+ {
+ get
+ {
+ if (!_disableHttp1LineFeedTerminators.HasValue)
+ {
+ _disableHttp1LineFeedTerminators = AppContext.TryGetSwitch(DisableHttp1LineFeedTerminatorsSwitchKey, out var disabled) && disabled;
+ }
+
+ return _disableHttp1LineFeedTerminators.Value;
+ }
+ set => _disableHttp1LineFeedTerminators = value;
+ }
+
///
/// Specifies a configuration Action to run for each newly created endpoint. Calling this again will replace
/// the prior action.
diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj
index cf77ba81c063..29990edece34 100644
--- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj
+++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj
@@ -1,4 +1,4 @@
-
+
Core components of ASP.NET Core Kestrel cross-platform web server.
@@ -29,6 +29,7 @@
+
diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
index 2ed4c17d476b..7b21c081fa04 100644
--- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
+++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
@@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
diff --git a/src/Servers/Kestrel/Core/test/HttpParserTests.cs b/src/Servers/Kestrel/Core/test/HttpParserTests.cs
index e6898927d644..ffbd38b10a0a 100644
--- a/src/Servers/Kestrel/Core/test/HttpParserTests.cs
+++ b/src/Servers/Kestrel/Core/test/HttpParserTests.cs
@@ -3,9 +3,6 @@
using System;
using System.Buffers;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.Http;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
@@ -13,8 +10,6 @@
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
-using Moq;
-using Xunit;
using HttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests;
@@ -39,7 +34,7 @@ public void ParsesRequestLine(
#pragma warning restore xUnit1026
string expectedVersion)
{
- var parser = CreateParser(_nullTrace);
+ var parser = CreateParser(_nullTrace, false);
var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
@@ -58,7 +53,7 @@ public void ParsesRequestLine(
[MemberData(nameof(RequestLineIncompleteData))]
public void ParseRequestLineReturnsFalseWhenGivenIncompleteRequestLines(string requestLine)
{
- var parser = CreateParser(_nullTrace);
+ var parser = CreateParser(_nullTrace, false);
var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
@@ -69,7 +64,7 @@ public void ParseRequestLineReturnsFalseWhenGivenIncompleteRequestLines(string r
[MemberData(nameof(RequestLineIncompleteData))]
public void ParseRequestLineDoesNotConsumeIncompleteRequestLine(string requestLine)
{
- var parser = CreateParser(_nullTrace);
+ var parser = CreateParser(_nullTrace, false);
var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
@@ -87,6 +82,34 @@ public void ParseRequestLineThrowsOnInvalidRequestLine(string requestLine)
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 ParseRequestSucceedsOnInvalidRequestLineLineFeedTerminator(string requestLine)
+ {
+ var parser = CreateParser(CreateEnabledTrace(), disableHttp1LineFeedTerminators: false);
+ 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 ParseRequestLineThrowsOnInvalidRequestLineLineFeedTerminator(string requestLine)
+ {
+ 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
@@ -102,7 +125,7 @@ public void ParseRequestLineThrowsOnNonTokenCharsInCustomMethod(string method)
{
var requestLine = $"{method} / HTTP/1.1\r\n";
- var parser = CreateParser(CreateEnabledTrace());
+ var parser = CreateParser(CreateEnabledTrace(), false);
var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
@@ -121,7 +144,7 @@ public void ParseRequestLineThrowsOnUnrecognizedHttpVersion(string httpVersion)
{
var requestLine = $"GET / {httpVersion}\r\n";
- var parser = CreateParser(CreateEnabledTrace());
+ var parser = CreateParser(CreateEnabledTrace(), false);
var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(requestLine));
var requestHandler = new RequestHandler();
@@ -134,6 +157,24 @@ public void ParseRequestLineThrowsOnUnrecognizedHttpVersion(string httpVersion)
Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, exception.StatusCode);
}
+ [Fact]
+ public void StartOfPathNotFound()
+ {
+ var requestLine = $"GET \n";
+
+ var parser = CreateParser(CreateEnabledTrace(), false);
+ 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("GET "), exception.Message);
+ Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
+ }
+
[Theory]
[InlineData("\r")]
[InlineData("H")]
@@ -173,7 +214,7 @@ public void ParseRequestLineThrowsOnUnrecognizedHttpVersion(string httpVersion)
[InlineData("Header-1: value1\r\nHeader-2: value2\r\n\r")]
public void ParseHeadersReturnsFalseWhenGivenIncompleteHeaders(string rawHeaders)
{
- var parser = CreateParser(_nullTrace);
+ var parser = CreateParser(_nullTrace, false);
var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(rawHeaders));
var requestHandler = new RequestHandler();
@@ -199,7 +240,7 @@ public void ParseHeadersReturnsFalseWhenGivenIncompleteHeaders(string rawHeaders
[InlineData("Header: value\r")]
public void ParseHeadersDoesNotConsumeIncompleteHeader(string rawHeaders)
{
- var parser = CreateParser(_nullTrace);
+ var parser = CreateParser(_nullTrace, false);
var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(rawHeaders));
var requestHandler = new RequestHandler();
@@ -224,6 +265,8 @@ public void ParseHeadersCanReadHeaderValueWithoutLeadingWhitespace()
[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,
@@ -238,7 +281,116 @@ public void ParseHeadersCanParseEmptyHeaderValues(
? new[] { expectedHeaderValue1 }
: new[] { expectedHeaderValue1, expectedHeaderValue2 };
- VerifyRawHeaders(rawHeaders, expectedHeaderNames, expectedHeaderValues);
+ 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(() => 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(() => 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]
@@ -289,7 +441,7 @@ public void ParseHeadersPreservesWhitespaceWithinHeaderValue(string headerValue)
[Fact]
public void ParseHeadersConsumesBytesCorrectlyAtEnd()
{
- var parser = CreateParser(_nullTrace);
+ var parser = CreateParser(_nullTrace, false);
const string headerLine = "Header: value\r\n\r";
var buffer1 = new ReadOnlySequence(Encoding.ASCII.GetBytes(headerLine));
@@ -312,7 +464,27 @@ public void ParseHeadersConsumesBytesCorrectlyAtEnd()
[MemberData(nameof(RequestHeaderInvalidData))]
public void ParseHeadersThrowsOnInvalidRequestHeaders(string rawHeaders, string expectedExceptionMessage)
{
- var parser = CreateParser(CreateEnabledTrace());
+ var parser = CreateParser(CreateEnabledTrace(), false);
+ 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 ParseHeadersThrowsOnInvalidRequestHeadersLineFeedTerminator(string rawHeaders, string expectedExceptionMessage)
+ {
+ var parser = CreateParser(CreateEnabledTrace(), true);
var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(rawHeaders));
var requestHandler = new RequestHandler();
@@ -374,7 +546,7 @@ public void ExceptionDetailNotIncludedWhenLogLevelInformationNotEnabled()
[Fact]
public void ParseRequestLineSplitBufferWithoutNewLineDoesNotUpdateConsumed()
{
- var parser = CreateParser(_nullTrace);
+ var parser = CreateParser(_nullTrace, false);
var buffer = ReadOnlySequenceFactory.CreateSegments(
Encoding.ASCII.GetBytes("GET "),
Encoding.ASCII.GetBytes("/"));
@@ -390,7 +562,7 @@ public void ParseRequestLineSplitBufferWithoutNewLineDoesNotUpdateConsumed()
[Fact]
public void ParseRequestLineTlsOverHttp()
{
- var parser = CreateParser(_nullTrace);
+ 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();
@@ -410,7 +582,7 @@ public void ParseRequestLineTlsOverHttp()
[MemberData(nameof(RequestHeaderInvalidData))]
public void ParseHeadersThrowsOnInvalidRequestHeadersWithGratuitouslySplitBuffers(string rawHeaders, string expectedExceptionMessage)
{
- var parser = CreateParser(CreateEnabledTrace());
+ var parser = CreateParser(CreateEnabledTrace(), false);
var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(rawHeaders);
var requestHandler = new RequestHandler();
@@ -426,11 +598,33 @@ public void ParseHeadersThrowsOnInvalidRequestHeadersWithGratuitouslySplitBuffer
Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
}
- [Fact]
- public void ParseHeadersWithGratuitouslySplitBuffers()
+ [Theory]
+ [MemberData(nameof(RequestHeaderInvalidDataLineFeedTerminator))]
+ public void ParseHeadersThrowsOnInvalidRequestHeadersWithGratuitouslySplitBuffersLineFeedTerminator(string rawHeaders, string expectedExceptionMessage)
{
- var parser = CreateParser(_nullTrace);
- var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent("Host:\r\nConnection: keep-alive\r\n\r\n");
+ 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(() =>
+#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, false);
+ var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(headers);
var requestHandler = new RequestHandler();
var reader = new SequenceReader(buffer);
@@ -439,11 +633,44 @@ public void ParseHeadersWithGratuitouslySplitBuffers()
Assert.True(result);
}
- [Fact]
- public void ParseHeadersWithGratuitouslySplitBuffers2()
+ [Theory]
+ [InlineData("Host: \r\nConnection: keep-alive\r")]
+ public void ParseHeaderLineIncompleteDataWithGratuitouslySplitBuffers(string headers)
{
- var parser = CreateParser(_nullTrace);
- var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent("A:B\r\nB: C\r\n\r\n");
+ var parser = CreateParser(_nullTrace, false);
+ var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(headers);
+
+ var requestHandler = new RequestHandler();
+ var reader = new SequenceReader(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(Encoding.ASCII.GetBytes(headers));
+
+ var requestHandler = new RequestHandler();
+ var reader = new SequenceReader(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(buffer);
@@ -474,7 +701,7 @@ private void VerifyHeader(
string rawHeaderValue,
string expectedHeaderValue)
{
- var parser = CreateParser(_nullTrace);
+ var parser = CreateParser(_nullTrace, false);
var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes($"{headerName}:{rawHeaderValue}\r\n"));
var requestHandler = new RequestHandler();
@@ -488,11 +715,11 @@ private void VerifyHeader(
Assert.True(buffer.Slice(reader.Position).IsEmpty);
}
- private void VerifyRawHeaders(string rawHeaders, IEnumerable expectedHeaderNames, IEnumerable expectedHeaderValues)
+ private void VerifyRawHeaders(string rawHeaders, IEnumerable expectedHeaderNames, IEnumerable expectedHeaderValues, bool disableHttp1LineFeedTerminators = true)
{
Assert.True(expectedHeaderNames.Count() == expectedHeaderValues.Count(), $"{nameof(expectedHeaderNames)} and {nameof(expectedHeaderValues)} sizes must match");
- var parser = CreateParser(_nullTrace);
+ var parser = CreateParser(_nullTrace, disableHttp1LineFeedTerminators);
var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(rawHeaders));
var requestHandler = new RequestHandler();
@@ -507,12 +734,14 @@ 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, bool disableHttp1LineFeedTerminators = true) => new HttpParser(log.IsEnabled(LogLevel.Information), disableHttp1LineFeedTerminators);
public static IEnumerable