diff --git a/src/System.Net.Http/src/System/Net/Http/Unix/CurlResponseHeaderReader.cs b/src/System.Net.Http/src/System/Net/Http/Unix/CurlResponseHeaderReader.cs index 46b5b09e27b5..9ed8ec7b8b45 100644 --- a/src/System.Net.Http/src/System/Net/Http/Unix/CurlResponseHeaderReader.cs +++ b/src/System.Net.Http/src/System/Net/Http/Unix/CurlResponseHeaderReader.cs @@ -78,9 +78,17 @@ public bool ReadHeader(out string headerName, out string headerValue) if (index > 0) { + // For compatability, skip past any whitespace before the colon, even though + // the RFC suggests there shouldn't be any. + int headerNameLength = index; + while (index < _span.Length && IsWhiteSpaceLatin1(_span[index])) + { + index++; + } + CheckResponseMsgFormat(index < _span.Length); CheckResponseMsgFormat(_span[index] == ':'); - HeaderBufferSpan headerNameSpan = _span.Substring(0, index); + HeaderBufferSpan headerNameSpan = _span.Substring(0, headerNameLength); if (!HttpKnownHeaderNames.TryGetHeaderName(_span.Buffer, _span.Length, out headerName)) { headerName = headerNameSpan.ToString(); @@ -111,6 +119,19 @@ private static bool ValidHeaderNameChar(byte c) return c > ' ' && invalidChars.IndexOf((char)c) < 0; } + internal static bool IsWhiteSpaceLatin1(byte c) + { + // SPACE + // U+0009 = HORIZONTAL TAB + // U+000a = LINE FEED + // U+000b = VERTICAL TAB + // U+000c = FORM FEED + // U+000d = CARRIAGE RETURN + // U+0085 = NEXT LINE + // U+00a0 = NO-BREAK SPACE + return c == ' ' || (c >= '\x0009' && c <= '\x000d') || c == '\x00a0' || c == '\x0085'; + } + private unsafe struct HeaderBufferSpan { private readonly byte* _pointer; @@ -268,18 +289,6 @@ public override string ToString() { return Length == 0 ? string.Empty : HttpRuleParser.DefaultHttpEncoding.GetString(_pointer, Length); } - - private static bool IsWhiteSpaceLatin1(byte c) - { - // U+0009 = HORIZONTAL TAB - // U+000a = LINE FEED - // U+000b = VERTICAL TAB - // U+000c = FORM FEED - // U+000d = CARRIAGE RETURN - // U+0085 = NEXT LINE - // U+00a0 = NO-BREAK SPACE - return c == ' ' || (c >= '\x0009' && c <= '\x000d') || c == '\x00a0' || c == '\x0085'; - } } } } diff --git a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs index fd33e143b21e..d4d0c6825757 100644 --- a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs +++ b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs @@ -739,7 +739,7 @@ await LoopbackServer.ReadRequestAndSendResponseAsync(server, $"HTTP/1.1 200 OK\r\n" + $"Date: {DateTimeOffset.UtcNow:R}\r\n" + $"Set-Cookie: {cookie1.Key}={cookie1.Value}; Path=/\r\n" + - $"Set-Cookie: {cookie2.Key}={cookie2.Value}; Path=/\r\n" + + $"Set-Cookie : {cookie2.Key}={cookie2.Value}; Path=/\r\n" + // space before colon to verify header is trimmed and recognized $"Set-Cookie: {cookie3.Key}={cookie3.Value}; Path=/\r\n" + "\r\n"); diff --git a/src/System.Net.Http/tests/UnitTests/Headers/CurlResponseHeaderReaderTest.cs b/src/System.Net.Http/tests/UnitTests/Headers/CurlResponseHeaderReaderTest.cs index 28b824dd7dc5..42eb666ba3f2 100644 --- a/src/System.Net.Http/tests/UnitTests/Headers/CurlResponseHeaderReaderTest.cs +++ b/src/System.Net.Http/tests/UnitTests/Headers/CurlResponseHeaderReaderTest.cs @@ -109,19 +109,34 @@ public unsafe void ReadStatusLine_ValidStatusCodeLine_ResponseMessageVersionSet( #endregion #region Headers - [Theory] - [InlineData("TestHeader", "Test header value", false)] - [InlineData("TestHeader", "Test header value", true)] - [InlineData("TestHeader", "", false)] - [InlineData("TestHeader", "", true)] - [InlineData("Server", "IIS", false)] - [InlineData("Server", "IIS", true)] - public unsafe void ReadHeader_ValidHeaderLine_HeaderReturned(string expectedHeaderName, string expectedHeaderValue, bool spaceAfterColon) + public static IEnumerable ReadHeader_ValidHeaderLine_HeaderReturned_MemberData() { - string headerLine = $"{expectedHeaderName}:{(spaceAfterColon ? " " : "")}{expectedHeaderValue}"; + var namesAndValues = new KeyValuePair[] + { + new KeyValuePair("TestHeader", "Test header value"), + new KeyValuePair("TestHeader", ""), + new KeyValuePair("Server", "IIS"), + new KeyValuePair("Server", "I:I:S"), + }; + var whitespaces = new string[] { "", " ", " ", " \t" }; + + foreach (KeyValuePair nameAndValue in namesAndValues) + { + foreach (string beforeColon in whitespaces) // only "" is valid according to the RFC, but we parse more leniently + { + foreach (string afterColon in whitespaces) + { + yield return new object[] { $"{nameAndValue.Key}{beforeColon}:{afterColon}{nameAndValue.Value}", nameAndValue.Key, nameAndValue.Value }; + } + } + } + } + [Theory] + [MemberData(nameof(ReadHeader_ValidHeaderLine_HeaderReturned_MemberData))] + public unsafe void ReadHeader_ValidHeaderLine_HeaderReturned(string headerLine, string expectedHeaderName, string expectedHeaderValue) + { byte[] buffer = headerLine.Select(c => checked((byte)c)).ToArray(); - fixed (byte* pBuffer = buffer) { var reader = new CurlResponseHeaderReader(new IntPtr(pBuffer), checked((ulong)buffer.Length));