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/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 RequestLineValidData => HttpParsingData.RequestLineValidData; public static IEnumerable RequestLineIncompleteData => HttpParsingData.RequestLineIncompleteData.Select(requestLine => new[] { requestLine }); + public static IEnumerable RequestLineInvalidDataLineFeedTerminator => HttpParsingData.RequestLineInvalidDataLineFeedTerminator.Select(requestLine => new[] { requestLine }); + public static IEnumerable RequestLineInvalidData => HttpParsingData.RequestLineInvalidData.Select(requestLine => new[] { requestLine }); public static IEnumerable MethodWithNonTokenCharData => HttpParsingData.MethodWithNonTokenCharData.Select(method => new[] { method }); @@ -521,6 +750,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; } diff --git a/src/Servers/Kestrel/shared/test/HttpParsingData.cs b/src/Servers/Kestrel/shared/test/HttpParsingData.cs index 8baf18513ced..f259dd9232ee 100644 --- a/src/Servers/Kestrel/shared/test/HttpParsingData.cs +++ b/src/Servers/Kestrel/shared/test/HttpParsingData.cs @@ -205,7 +205,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 +225,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 { @@ -364,13 +387,19 @@ public static IEnumerable TargetWithNullCharData "8charact", }; - public static IEnumerable RequestHeaderInvalidData => new[] + 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") }, + // Empty header name + new[] { ":a\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@":a\x0A") }, + }; + + public static IEnumerable RequestHeaderInvalidData => new[] + { // 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") }, @@ -404,7 +433,7 @@ public static IEnumerable TargetWithNullCharData 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") }, + new[] { "HeaderValue1\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"HeaderValue1\x0D\x0A") }, // Starting with whitespace new[] { " Header: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" Header: value\x0D\x0A") }, @@ -435,11 +464,13 @@ public static IEnumerable TargetWithNullCharData // 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 ", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF }, new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r \n", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF }, + new[] { "Header-1: value1\r\nHeader-2\t: value2 \n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2\x09: value2 \x0A") }, // Empty header name new[] { ": value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@": value\x0D\x0A") }, + new[] { ":a\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@":a\x0D\x0A") }, }; public static TheoryData HostHeaderData diff --git a/src/Servers/Kestrel/shared/test/TestServiceContext.cs b/src/Servers/Kestrel/shared/test/TestServiceContext.cs index e4fb4e4fd585..119a13020b91 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 disableHttp1LineFeedTerminators = true) { - Initialize(loggerFactory, CreateLoggingTrace(loggerFactory)); + Initialize(loggerFactory, CreateLoggingTrace(loggerFactory), disableHttp1LineFeedTerminators); } - public TestServiceContext(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace) + public TestServiceContext(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, bool disableHttp1LineFeedTerminators = true) { - Initialize(loggerFactory, kestrelTrace); + Initialize(loggerFactory, kestrelTrace, disableHttp1LineFeedTerminators); } 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 disableHttp1LineFeedTerminators) { 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), disableHttp1LineFeedTerminators); 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 564ba843842a..5bde02517fff 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs @@ -524,5 +524,7 @@ public static TheoryData InvalidRequestLineData public static IEnumerable InvalidRequestHeaderData => HttpParsingData.RequestHeaderInvalidData; + public static IEnumerable InvalidRequestHeaderDataLineFeedTerminator => HttpParsingData.RequestHeaderInvalidDataLineFeedTerminator; + public static TheoryData InvalidHostHeaderData => HttpParsingData.HostHeaderInvalidData; } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs index 1cb13d90fe68..7c2a98a2f626 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs @@ -2284,6 +2284,49 @@ await connection.Receive( } } + [Fact] + public async Task SingleLineFeedIsSupportedAnywhere() + { + // Exercises all combinations of LF and CRLF as line separators. + // Uses a bit mask for all the possible combinations. + + var lines = new[] + { + $"GET / HTTP/1.1", + "Content-Length: 0", + $"Host: localhost", + "", + }; + + await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory, disableHttp1LineFeedTerminators: false))) + { + var mask = Math.Pow(2, lines.Length) - 1; + + for (var m = 0; m <= mask; m++) + { + using (var client = server.CreateConnection()) + { + var sb = new StringBuilder(); + + for (var pos = 0; pos < lines.Length; pos++) + { + sb.Append(lines[pos]); + var separator = (m & (1 << pos)) != 0 ? "\n" : "\r\n"; + sb.Append(separator); + } + + var text = sb.ToString(); + var writer = new StreamWriter(client.Stream, Encoding.GetEncoding("iso-8859-1")); + await writer.WriteAsync(text).ConfigureAwait(false); + await writer.FlushAsync().ConfigureAwait(false); + await client.Stream.FlushAsync().ConfigureAwait(false); + + await client.Receive("HTTP/1.1 200"); + } + } + } + } + public static TheoryData HostHeaderData => HttpParsingData.HostHeaderData; private class IntAsClass