From 21ba20a5f1c0f75578da598269a1726285a79005 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 6 Jun 2016 23:15:51 -0400 Subject: [PATCH] Allow whitespace after HTTP response header name Be a bit more lenient in what we allow in the formatting of response headers on Unix. The RFC states that "Each header field consists of a case-insensitive field name followed by a colon (":"), optional leading whitespace, the field value, and optional trailing whitespace." However, whitespace between the header name and the colon is accepted by WinHttpHandler, by the .NET Framework, and by all browsers on which I tried it, and servers can easily include such whitespace; if they do and we're not tolerant of it, requests to such servers will fail. --- .../Net/Http/Unix/CurlResponseHeaderReader.cs | 35 ++++++++++++------- .../FunctionalTests/HttpClientHandlerTest.cs | 2 +- .../Headers/CurlResponseHeaderReaderTest.cs | 35 +++++++++++++------ 3 files changed, 48 insertions(+), 24 deletions(-) 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));