Fix encoding of Digest authentication headers (dotnet/corefx#36627)
authorFilip Navara <filip.navara@gmail.com>
Sat, 13 Apr 2019 16:07:10 +0000 (18:07 +0200)
committerDavid Shulman <david.shulman@microsoft.com>
Sat, 13 Apr 2019 16:07:10 +0000 (09:07 -0700)
* Fix encoding of Digest authentication headers for servers that don't understand RFC 5987 encoding

* Quote-prefix special characters when encoding Digest headers

* Address PR feedback

Commit migrated from https://github.com/dotnet/corefx/commit/cce7870129dcca472f4e6108e21e87bc9573b8d2

src/libraries/System.Net.Http/src/System/Net/Http/Headers/ContentDispositionHeaderValue.cs
src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.Digest.cs
src/libraries/System.Net.Http/tests/UnitTests/DigestAuthenticationTests.cs

index b2f2aab..d38085a 100644 (file)
@@ -5,7 +5,6 @@
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Globalization;
-using System.IO;
 using System.Text;
 
 namespace System.Net.Http.Headers
@@ -425,7 +424,7 @@ namespace System.Net.Http.Headers
         }
 
         // Returns input for decoding failures, as the content might not be encoded.
-        private string EncodeAndQuoteMime(string input)
+        private static string EncodeAndQuoteMime(string input)
         {
             string result = input;
             bool needsQuotes = false;
@@ -441,7 +440,7 @@ namespace System.Net.Http.Headers
                 throw new ArgumentException(SR.Format(CultureInfo.InvariantCulture,
                     SR.net_http_headers_invalid_value, input));
             }
-            else if (RequiresEncoding(result))
+            else if (HeaderUtilities.ContainsNonAscii(result))
             {
                 needsQuotes = true; // Encoded data must always be quoted, the equals signs are invalid in tokens.
                 result = EncodeMime(result); // =?utf-8?B?asdfasdfaesdf?=
@@ -460,7 +459,7 @@ namespace System.Net.Http.Headers
         }
 
         // Returns true if the value starts and ends with a quote.
-        private bool IsQuoted(ReadOnlySpan<char> value)
+        private static bool IsQuoted(ReadOnlySpan<char> value)
         {
             return
                 value.Length > 1 &&
@@ -468,23 +467,8 @@ namespace System.Net.Http.Headers
                 value[value.Length - 1] == '"';
         }
 
-        // tspecials are required to be in a quoted string.  Only non-ascii needs to be encoded.
-        private bool RequiresEncoding(string input)
-        {
-            Debug.Assert(input != null);
-
-            foreach (char c in input)
-            {
-                if ((int)c > 0x7f)
-                {
-                    return true;
-                }
-            }
-            return false;
-        }
-
         // Encode using MIME encoding.
-        private string EncodeMime(string input)
+        private static string EncodeMime(string input)
         {
             byte[] buffer = Encoding.UTF8.GetBytes(input);
             string encodedName = Convert.ToBase64String(buffer);
@@ -492,7 +476,7 @@ namespace System.Net.Http.Headers
         }
 
         // Attempt to decode MIME encoded strings.
-        private bool TryDecodeMime(string input, out string output)
+        private static bool TryDecodeMime(string input, out string output)
         {
             Debug.Assert(input != null);
 
@@ -535,7 +519,7 @@ namespace System.Net.Http.Headers
 
         // Attempt to decode using RFC 5987 encoding.
         // encoding'language'my%20string
-        private bool TryDecode5987(string input, out string output)
+        private static bool TryDecode5987(string input, out string output)
         {
             output = null;
             
index 73050a2..e18dea7 100644 (file)
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
+using System.Buffers;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Globalization;
@@ -65,58 +66,63 @@ namespace System.Net.Http.Headers
             }
         }
 
-        // Encode a string using RFC 5987 encoding.
-        // encoding'lang'PercentEncodedSpecials
-        internal static string Encode5987(string input)
+        internal static bool ContainsNonAscii(string input)
         {
-            string output;
-            IsInputEncoded5987(input, out output);
+            Debug.Assert(input != null);
 
-            return output;
+            foreach (char c in input)
+            {
+                if ((int)c > 0x7f)
+                {
+                    return true;
+                }
+            }
+            return false;
         }
 
-        internal static bool IsInputEncoded5987(string input, out string output)
+        // Encode a string using RFC 5987 encoding.
+        // encoding'lang'PercentEncodedSpecials
+        internal static string Encode5987(string input)
         {
             // Encode a string using RFC 5987 encoding.
             // encoding'lang'PercentEncodedSpecials
-            bool wasEncoded = false;
             StringBuilder builder = StringBuilderCache.Acquire();
+            byte[] utf8bytes = ArrayPool<byte>.Shared.Rent(Encoding.UTF8.GetMaxByteCount(input.Length));
+            int utf8length = Encoding.UTF8.GetBytes(input, 0, input.Length, utf8bytes, 0);
+
             builder.Append("utf-8\'\'");
-            foreach (char c in input)
+            for (int i = 0; i < utf8length; i++)
             {
+                byte utf8byte = utf8bytes[i];
+
                 // attr-char = ALPHA / DIGIT / "!" / "#" / "$" / "&" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
                 //      ; token except ( "*" / "'" / "%" )
-                if (c > 0x7F) // Encodes as multiple utf-8 bytes
+                if (utf8byte > 0x7F) // Encodes as multiple utf-8 bytes
                 {
-                    byte[] bytes = Encoding.UTF8.GetBytes(c.ToString());
-                    foreach (byte b in bytes)
-                    {
-                        AddHexEscaped((char)b, builder);
-                        wasEncoded = true;
-                    }
+                    AddHexEscaped(utf8byte, builder);
                 }
-                else if (!HttpRuleParser.IsTokenChar(c) || c == '*' || c == '\'' || c == '%')
+                else if (!HttpRuleParser.IsTokenChar((char)utf8byte) || utf8byte == '*' || utf8byte == '\'' || utf8byte == '%')
                 {
                     // ASCII - Only one encoded byte.
-                    AddHexEscaped(c, builder);
-                    wasEncoded = true;
+                    AddHexEscaped(utf8byte, builder);
                 }
                 else
                 {
-                    builder.Append(c);
+                    builder.Append((char)utf8byte);
                 }
 
             }
 
-            output = StringBuilderCache.GetStringAndRelease(builder);
-            return wasEncoded;
+            Array.Clear(utf8bytes, 0, utf8length);
+            ArrayPool<byte>.Shared.Return(utf8bytes);
+
+            return StringBuilderCache.GetStringAndRelease(builder);
         }
 
         /// <summary>Transforms an ASCII character into its hexadecimal representation, adding the characters to a StringBuilder.</summary>
-        private static void AddHexEscaped(char c, StringBuilder destination)
+        private static void AddHexEscaped(byte c, StringBuilder destination)
         {
             Debug.Assert(destination != null);
-            Debug.Assert(c <= 0xFF);
 
             destination.Append('%');
             destination.Append(s_hexUpperChars[(c & 0xf0) >> 4]);
index b156134..b65b240 100644 (file)
@@ -91,9 +91,9 @@ namespace System.Net.Http
             }
             else
             {
-                string usernameStar;
-                if (HeaderUtilities.IsInputEncoded5987(credential.UserName, out usernameStar))
+                if (HeaderUtilities.ContainsNonAscii(credential.UserName))
                 {
+                    string usernameStar = HeaderUtilities.Encode5987(credential.UserName);
                     sb.AppendKeyValue(UsernameStar, usernameStar, includeQuotes: false);
                 }
                 else
@@ -408,6 +408,9 @@ namespace System.Net.Http
 
     internal static class StringBuilderExtensions
     {
+        // Characters that require escaping in quoted string
+        private static readonly char[] SpecialCharacters = new[] { '"', '\\' };
+
         public static void AppendKeyValue(this StringBuilder sb, string key, string value, bool includeQuotes = true, bool includeComma = true)
         {
             sb.Append(key);
@@ -415,12 +418,29 @@ namespace System.Net.Http
             if (includeQuotes)
             {
                 sb.Append('"');
+                int lastSpecialIndex = 0;
+                int specialIndex;
+                while (true)
+                {
+                    specialIndex = value.IndexOfAny(SpecialCharacters, lastSpecialIndex);
+                    if (specialIndex >= 0)
+                    {
+                        sb.Append(value, lastSpecialIndex, specialIndex - lastSpecialIndex);
+                        sb.Append('\\');
+                        sb.Append(value[specialIndex]);
+                        lastSpecialIndex = specialIndex + 1;
+                    }
+                    else
+                    {
+                        sb.Append(value, lastSpecialIndex, value.Length - lastSpecialIndex);
+                        break;
+                    }
+                }
+                sb.Append('"');
             }
-
-            sb.Append(value);
-            if (includeQuotes)
+            else
             {
-                sb.Append('"');
+                sb.Append(value);
             }
 
             if (includeComma)
index f339ba7..45d0371 100644 (file)
@@ -56,5 +56,20 @@ namespace System.Net.Http.Tests
 
             Assert.Equal(expectedResult, parameter != null);
         }
+
+        [Theory]
+        [InlineData("test", "username=\"test\"")]
+        [InlineData("test@example.org", "username=\"test@example.org\"")]
+        [InlineData("test\"example.org", "username=\"test\\\"example.org\"")]
+        [InlineData("t\u00E6st", "username*=utf-8''t%C3%A6st")]
+        [InlineData("\uD834\uDD1E", "username*=utf-8''%F0%9D%84%9E")]
+        public async void DigestResponse_UserName_Encoding(string username, string encodedUserName)
+        {
+            NetworkCredential credential = new NetworkCredential(username, "bar");
+            AuthenticationHelper.DigestResponse digestResponse = new AuthenticationHelper.DigestResponse("realm=\"NetCore\", nonce=\"qMRqWgAAAAAQMjIABgAAAFwEiEwAAAAA\"");
+            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://microsoft.com/");
+            string parameter = await AuthenticationHelper.GetDigestTokenForCredential(credential, request, digestResponse).ConfigureAwait(false);
+            Assert.StartsWith(encodedUserName, parameter);
+        }
     }
 }