1 //------------------------------------------------------------------------------
2 // <copyright file="HttpEncoder.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 //------------------------------------------------------------------------------
6 
7 /*
8  * Base class providing extensibility hooks for custom encoding / decoding
9  *
10  * Copyright (c) 2009 Microsoft Corporation
11  */
12 
13 namespace System.Web.Util {
14     using System;
15     using System.Diagnostics.CodeAnalysis;
16     using System.Globalization;
17     using System.IO;
18     using System.Net;
19     using System.Text;
20     using System.Web;
21     using System.Web.Configuration;
22 
23     public class HttpEncoder {
24 
25         private static HttpEncoder _customEncoder;
26         private readonly bool _isDefaultEncoder;
27 
28         private static readonly Lazy<HttpEncoder> _customEncoderResolver =
29             new Lazy<HttpEncoder>(GetCustomEncoderFromConfig);
30 
31         private static readonly HttpEncoder _defaultEncoder = new HttpEncoder();
32 
33         private static readonly string[] _headerEncodingTable = new string[] {
34             "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
35             "%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f",
36             "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
37             "%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f"
38         };
39 
HttpEncoder()40         public HttpEncoder() {
41             _isDefaultEncoder = (GetType() == typeof(HttpEncoder));
42         }
43 
44         public static HttpEncoder Current {
45             get {
46                 // always use the fallback encoder when rendering an error page so that we can at least display *something*
47                 // to the user before closing the connection
48 
49                 HttpContext httpContext = HttpContext.Current;
50                 if (httpContext != null && httpContext.DisableCustomHttpEncoder) {
51                     return _defaultEncoder;
52                 }
53                 else {
54                     if (_customEncoder == null) {
55                         _customEncoder = _customEncoderResolver.Value;
56                     }
57                     return _customEncoder;
58                 }
59             }
60             set {
61                 if (value == null) {
62                     throw new ArgumentNullException("value");
63                 }
64                 _customEncoder = value;
65             }
66         }
67 
68         public static HttpEncoder Default {
69             get {
70                 return _defaultEncoder;
71             }
72         }
73 
74         internal virtual bool JavaScriptEncodeAmpersand {
75             get {
76                 return !AppSettings.JavaScriptDoNotEncodeAmpersand;
77             }
78         }
79 
AppendCharAsUnicodeJavaScript(StringBuilder builder, char c)80         private static void AppendCharAsUnicodeJavaScript(StringBuilder builder, char c) {
81             builder.Append("\\u");
82             builder.Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
83         }
84 
CharRequiresJavaScriptEncoding(char c)85         private bool CharRequiresJavaScriptEncoding(char c) {
86             return c < 0x20 // control chars always have to be encoded
87                 || c == '\"' // chars which must be encoded per JSON spec
88                 || c == '\\'
89                 || c == '\'' // HTML-sensitive chars encoded for safety
90                 || c == '<'
91                 || c == '>'
92                 || (c == '&' && JavaScriptEncodeAmpersand) // Bug Dev11 #133237. Encode '&' to provide additional security for people who incorrectly call the encoding methods (unless turned off by backcompat switch)
93                 || c == '\u0085' // newline chars (see Unicode 6.2, Table 5-1 [http://www.unicode.org/versions/Unicode6.2.0/ch05.pdf]) have to be encoded (DevDiv #663531)
94                 || c == '\u2028'
95                 || c == '\u2029';
96         }
97 
CollapsePercentUFromStringInternal(string s, Encoding e)98         internal static string CollapsePercentUFromStringInternal(string s, Encoding e) {
99             int count = s.Length;
100             UrlDecoder helper = new UrlDecoder(count, e);
101 
102             // go thorugh the string's chars collapsing just %uXXXX and
103             // appending each char as char
104             int loc = s.IndexOf("%u", StringComparison.Ordinal);
105             if (loc == -1) {
106                 return s;
107             }
108 
109             for (int pos = 0; pos < count; pos++) {
110                 char ch = s[pos];
111 
112                 if (ch == '%' && pos < count - 5) {
113                     if (s[pos + 1] == 'u') {
114                         int h1 = HttpEncoderUtility.HexToInt(s[pos + 2]);
115                         int h2 = HttpEncoderUtility.HexToInt(s[pos + 3]);
116                         int h3 = HttpEncoderUtility.HexToInt(s[pos + 4]);
117                         int h4 = HttpEncoderUtility.HexToInt(s[pos + 5]);
118 
119                         if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0) { //valid 4 hex chars
120                             ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4);
121                             pos += 5;
122 
123                             // add as char
124                             helper.AddChar(ch);
125                             continue;
126                         }
127                     }
128                 }
129                 if ((ch & 0xFF80) == 0)
130                     helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode
131                 else
132                     helper.AddChar(ch);
133             }
134             return Utf16StringValidator.ValidateString(helper.GetString());
135         }
136 
GetCustomEncoderFromConfig()137         private static HttpEncoder GetCustomEncoderFromConfig() {
138             // App since this is static per AppDomain
139             RuntimeConfig config = RuntimeConfig.GetAppConfig();
140             HttpRuntimeSection runtimeSection = config.HttpRuntime;
141             string encoderTypeName = runtimeSection.EncoderType;
142 
143             // validate the type
144             Type encoderType = ConfigUtil.GetType(encoderTypeName, "encoderType", runtimeSection);
145             ConfigUtil.CheckBaseType(typeof(HttpEncoder) /* expectedBaseType */, encoderType, "encoderType", runtimeSection);
146 
147             // instantiate
148             HttpEncoder encoder = (HttpEncoder)HttpRuntime.CreatePublicInstance(encoderType);
149             return encoder;
150         }
151 
152         // Encode the header if it contains a CRLF pair
153         // VSWhidbey 257154
HeaderEncodeInternal(string value)154         private static string HeaderEncodeInternal(string value) {
155             string sanitizedHeader = value;
156             if (HeaderValueNeedsEncoding(value)) {
157                 // DevDiv Bugs 146028
158                 // Denial Of Service scenarios involving
159                 // control characters are possible.
160                 // We are encoding the following characters:
161                 // - All CTL characters except HT (horizontal tab)
162                 // - DEL character (\x7f)
163                 StringBuilder sb = new StringBuilder();
164                 foreach (char c in value) {
165                     if (c < 32 && c != 9) {
166                         sb.Append(_headerEncodingTable[c]);
167                     }
168                     else if (c == 127) {
169                         sb.Append("%7f");
170                     }
171                     else {
172                         sb.Append(c);
173                     }
174                 }
175                 sanitizedHeader = sb.ToString();
176             }
177 
178             return sanitizedHeader;
179         }
180 
181         [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters",
182                  Justification = "Input parameter strings are immutable, so this is an appropriate way to return multiple strings.")]
HeaderNameValueEncode(string headerName, string headerValue, out string encodedHeaderName, out string encodedHeaderValue)183         protected internal virtual void HeaderNameValueEncode(string headerName, string headerValue, out string encodedHeaderName, out string encodedHeaderValue) {
184             encodedHeaderName = (String.IsNullOrEmpty(headerName)) ? headerName : HeaderEncodeInternal(headerName);
185             encodedHeaderValue = (String.IsNullOrEmpty(headerValue)) ? headerValue : HeaderEncodeInternal(headerValue);
186         }
187 
188         // Returns true if the string contains a control character (other than horizontal tab) or the DEL character.
HeaderValueNeedsEncoding(string value)189         private static bool HeaderValueNeedsEncoding(string value) {
190             foreach (char c in value) {
191                 if ((c < 32 && c != 9) || (c == 127)) {
192                     return true;
193                 }
194             }
195             return false;
196         }
197 
HtmlAttributeEncode(string value)198         internal string HtmlAttributeEncode(string value) {
199             if (String.IsNullOrEmpty(value)) {
200                 return value;
201             }
202 
203             if(_isDefaultEncoder) {
204                 // Don't create string writer if we don't have nothing to encode
205                 int pos = IndexOfHtmlAttributeEncodingChars(value, 0);
206                 if (pos == -1) {
207                     return value;
208                 }
209             }
210 
211             StringWriter writer = new StringWriter(CultureInfo.InvariantCulture);
212             HtmlAttributeEncode(value, writer);
213             return writer.ToString();
214         }
215 
HtmlAttributeEncode(string value, TextWriter output)216         protected internal virtual void HtmlAttributeEncode(string value, TextWriter output) {
217             if (value == null) {
218                 return;
219             }
220             if (output == null) {
221                 throw new ArgumentNullException("output");
222             }
223 
224             // we have a special faster path for HttpWriter
225             HttpWriter httpWriter = output as HttpWriter;
226             if (httpWriter != null) {
227                 HtmlAttributeEncodeInternal(value, httpWriter);
228             }
229             else {
230                 HtmlAttributeEncodeInternal(value, output);
231             }
232         }
233 
HtmlAttributeEncodeInternal(string value, HttpWriter writer)234         private static void HtmlAttributeEncodeInternal(string value, HttpWriter writer) {
235             int pos = IndexOfHtmlAttributeEncodingChars(value, 0);
236             if (pos == -1) {
237                 writer.Write(value);
238                 return;
239             }
240 
241             int cch = value.Length;
242             int startPos = 0;
243             for (; ; ) {
244                 if (pos > startPos) {
245                     writer.WriteString(value, startPos, pos - startPos);
246                 }
247 
248                 char ch = value[pos];
249                 switch (ch) {
250                     case '"':
251                         writer.Write("&quot;");
252                         break;
253                     case '\'':
254                         writer.Write("&#39;");
255                         break;
256                     case '&':
257                         writer.Write("&amp;");
258                         break;
259                     case '<':
260                         // Whidbey 32404: The character '<' is not valid in an XML attribute value.
261                         // (See the W3C XML rec).
262                         writer.Write("&lt;");
263                         break;
264                 }
265 
266                 startPos = pos + 1;
267                 if (startPos >= cch)
268                     break;
269 
270                 pos = IndexOfHtmlAttributeEncodingChars(value, startPos);
271                 if (pos == -1) {
272                     writer.WriteString(value, startPos, cch - startPos);
273                     break;
274                 }
275             }
276         }
277 
HtmlAttributeEncodeInternal(String s, TextWriter output)278         private unsafe static void HtmlAttributeEncodeInternal(String s, TextWriter output) {
279             int index = IndexOfHtmlAttributeEncodingChars(s, 0);
280             if (index == -1) {
281                 output.Write(s);
282             }
283             else {
284                 int cch = s.Length - index;
285                 fixed (char* str = s) {
286                     char* pch = str;
287                     while (index-- > 0) {
288                         output.Write(*pch++);
289                     }
290 
291                     while (cch-- > 0) {
292                         char ch = *pch++;
293                         if (ch <= '<') {
294                             switch (ch) {
295                                 case '<':
296                                     output.Write("&lt;");
297                                     break;
298                                 case '"':
299                                     output.Write("&quot;");
300                                     break;
301                                 case '\'':
302                                     output.Write("&#39;");
303                                     break;
304                                 case '&':
305                                     output.Write("&amp;");
306                                     break;
307                                 default:
308                                     output.Write(ch);
309                                     break;
310                             }
311                         }
312                         else {
313                             output.Write(ch);
314                         }
315                     }
316                 }
317             }
318         }
319 
HtmlDecode(string value)320         internal string HtmlDecode(string value) {
321             if (String.IsNullOrEmpty(value))
322             {
323                 return value;
324             }
325 
326             if(_isDefaultEncoder) {
327                 return WebUtility.HtmlDecode(value);
328             }
329 
330             StringWriter writer = new StringWriter(CultureInfo.InvariantCulture);
331             HtmlDecode(value, writer);
332             return writer.ToString();
333         }
334 
HtmlDecode(string value, TextWriter output)335         protected internal virtual void HtmlDecode(string value, TextWriter output) {
336             WebUtility.HtmlDecode(value, output);
337         }
338 
HtmlEncode(string value)339         internal string HtmlEncode(string value) {
340             if (String.IsNullOrEmpty(value))
341             {
342                 return value;
343             }
344 
345             if(_isDefaultEncoder) {
346                 return WebUtility.HtmlEncode(value);
347             }
348 
349             StringWriter writer = new StringWriter(CultureInfo.InvariantCulture);
350             HtmlEncode(value, writer);
351             return writer.ToString();
352         }
353 
HtmlEncode(string value, TextWriter output)354         protected internal virtual void HtmlEncode(string value, TextWriter output) {
355             WebUtility.HtmlEncode(value, output);
356         }
357 
IndexOfHtmlAttributeEncodingChars(string s, int startPos)358         private static unsafe int IndexOfHtmlAttributeEncodingChars(string s, int startPos) {
359             Debug.Assert(0 <= startPos && startPos <= s.Length, "0 <= startPos && startPos <= s.Length");
360             int cch = s.Length - startPos;
361             fixed (char* str = s) {
362                 for (char* pch = &str[startPos]; cch > 0; pch++, cch--) {
363                     char ch = *pch;
364                     if (ch <= '<') {
365                         switch (ch) {
366                             case '<':
367                             case '"':
368                             case '\'':
369                             case '&':
370                                 return s.Length - cch;
371                         }
372                     }
373                 }
374             }
375 
376             return -1;
377         }
378 
InitializeOnFirstRequest()379         internal static void InitializeOnFirstRequest() {
380             // Instantiate the encoder if it hasn't already been created. Note that this isn't storing the returned encoder
381             // anywhere; it's just priming the Lazy<T> so that future calls to the Value property getter will return quickly
382             // without going back to config.
383 
384             HttpEncoder encoder = _customEncoderResolver.Value;
385         }
386 
IsNonAsciiByte(byte b)387         private static bool IsNonAsciiByte(byte b) {
388             return (b >= 0x7F || b < 0x20);
389         }
390 
JavaScriptStringEncode(string value)391         protected internal virtual string JavaScriptStringEncode(string value) {
392             if (String.IsNullOrEmpty(value)) {
393                 return String.Empty;
394             }
395 
396             StringBuilder b = null;
397             int startIndex = 0;
398             int count = 0;
399             for (int i = 0; i < value.Length; i++) {
400                 char c = value[i];
401 
402                 // Append the unhandled characters (that do not require special treament)
403                 // to the string builder when special characters are detected.
404                 if (CharRequiresJavaScriptEncoding(c)) {
405                     if (b == null) {
406                         b = new StringBuilder(value.Length + 5);
407                     }
408 
409                     if (count > 0) {
410                         b.Append(value, startIndex, count);
411                     }
412 
413                     startIndex = i + 1;
414                     count = 0;
415                 }
416 
417                 switch (c) {
418                     case '\r':
419                         b.Append("\\r");
420                         break;
421                     case '\t':
422                         b.Append("\\t");
423                         break;
424                     case '\"':
425                         b.Append("\\\"");
426                         break;
427                     case '\\':
428                         b.Append("\\\\");
429                         break;
430                     case '\n':
431                         b.Append("\\n");
432                         break;
433                     case '\b':
434                         b.Append("\\b");
435                         break;
436                     case '\f':
437                         b.Append("\\f");
438                         break;
439                     default:
440                         if (CharRequiresJavaScriptEncoding(c)) {
441                             AppendCharAsUnicodeJavaScript(b, c);
442                         }
443                         else {
444                             count++;
445                         }
446                         break;
447                 }
448             }
449 
450             if (b == null) {
451                 return value;
452             }
453 
454             if (count > 0) {
455                 b.Append(value, startIndex, count);
456             }
457 
458             return b.ToString();
459         }
460 
UrlDecode(byte[] bytes, int offset, int count)461         internal byte[] UrlDecode(byte[] bytes, int offset, int count) {
462             if (!ValidateUrlEncodingParameters(bytes, offset, count)) {
463                 return null;
464             }
465 
466             int decodedBytesCount = 0;
467             byte[] decodedBytes = new byte[count];
468 
469             for (int i = 0; i < count; i++) {
470                 int pos = offset + i;
471                 byte b = bytes[pos];
472 
473                 if (b == '+') {
474                     b = (byte)' ';
475                 }
476                 else if (b == '%' && i < count - 2) {
477                     int h1 = HttpEncoderUtility.HexToInt((char)bytes[pos + 1]);
478                     int h2 = HttpEncoderUtility.HexToInt((char)bytes[pos + 2]);
479 
480                     if (h1 >= 0 && h2 >= 0) {     // valid 2 hex chars
481                         b = (byte)((h1 << 4) | h2);
482                         i += 2;
483                     }
484                 }
485 
486                 decodedBytes[decodedBytesCount++] = b;
487             }
488 
489             if (decodedBytesCount < decodedBytes.Length) {
490                 byte[] newDecodedBytes = new byte[decodedBytesCount];
491                 Array.Copy(decodedBytes, newDecodedBytes, decodedBytesCount);
492                 decodedBytes = newDecodedBytes;
493             }
494 
495             return decodedBytes;
496         }
497 
UrlDecode(byte[] bytes, int offset, int count, Encoding encoding)498         internal string UrlDecode(byte[] bytes, int offset, int count, Encoding encoding) {
499             if (!ValidateUrlEncodingParameters(bytes, offset, count)) {
500                 return null;
501             }
502 
503             UrlDecoder helper = new UrlDecoder(count, encoding);
504 
505             // go through the bytes collapsing %XX and %uXXXX and appending
506             // each byte as byte, with exception of %uXXXX constructs that
507             // are appended as chars
508 
509             for (int i = 0; i < count; i++) {
510                 int pos = offset + i;
511                 byte b = bytes[pos];
512 
513                 // The code assumes that + and % cannot be in multibyte sequence
514 
515                 if (b == '+') {
516                     b = (byte)' ';
517                 }
518                 else if (b == '%' && i < count - 2) {
519                     if (bytes[pos + 1] == 'u' && i < count - 5) {
520                         int h1 = HttpEncoderUtility.HexToInt((char)bytes[pos + 2]);
521                         int h2 = HttpEncoderUtility.HexToInt((char)bytes[pos + 3]);
522                         int h3 = HttpEncoderUtility.HexToInt((char)bytes[pos + 4]);
523                         int h4 = HttpEncoderUtility.HexToInt((char)bytes[pos + 5]);
524 
525                         if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0) {   // valid 4 hex chars
526                             char ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4);
527                             i += 5;
528 
529                             // don't add as byte
530                             helper.AddChar(ch);
531                             continue;
532                         }
533                     }
534                     else {
535                         int h1 = HttpEncoderUtility.HexToInt((char)bytes[pos + 1]);
536                         int h2 = HttpEncoderUtility.HexToInt((char)bytes[pos + 2]);
537 
538                         if (h1 >= 0 && h2 >= 0) {     // valid 2 hex chars
539                             b = (byte)((h1 << 4) | h2);
540                             i += 2;
541                         }
542                     }
543                 }
544 
545                 helper.AddByte(b);
546             }
547 
548             return Utf16StringValidator.ValidateString(helper.GetString());
549         }
550 
UrlDecode(string value, Encoding encoding)551         internal string UrlDecode(string value, Encoding encoding) {
552             if (value == null) {
553                 return null;
554             }
555 
556             int count = value.Length;
557             UrlDecoder helper = new UrlDecoder(count, encoding);
558 
559             // go through the string's chars collapsing %XX and %uXXXX and
560             // appending each char as char, with exception of %XX constructs
561             // that are appended as bytes
562 
563             for (int pos = 0; pos < count; pos++) {
564                 char ch = value[pos];
565 
566                 if (ch == '+') {
567                     ch = ' ';
568                 }
569                 else if (ch == '%' && pos < count - 2) {
570                     if (value[pos + 1] == 'u' && pos < count - 5) {
571                         int h1 = HttpEncoderUtility.HexToInt(value[pos + 2]);
572                         int h2 = HttpEncoderUtility.HexToInt(value[pos + 3]);
573                         int h3 = HttpEncoderUtility.HexToInt(value[pos + 4]);
574                         int h4 = HttpEncoderUtility.HexToInt(value[pos + 5]);
575 
576                         if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0) {   // valid 4 hex chars
577                             ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4);
578                             pos += 5;
579 
580                             // only add as char
581                             helper.AddChar(ch);
582                             continue;
583                         }
584                     }
585                     else {
586                         int h1 = HttpEncoderUtility.HexToInt(value[pos + 1]);
587                         int h2 = HttpEncoderUtility.HexToInt(value[pos + 2]);
588 
589                         if (h1 >= 0 && h2 >= 0) {     // valid 2 hex chars
590                             byte b = (byte)((h1 << 4) | h2);
591                             pos += 2;
592 
593                             // don't add as char
594                             helper.AddByte(b);
595                             continue;
596                         }
597                     }
598                 }
599 
600                 if ((ch & 0xFF80) == 0)
601                     helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode
602                 else
603                     helper.AddChar(ch);
604             }
605 
606             return Utf16StringValidator.ValidateString(helper.GetString());
607         }
608 
UrlEncode(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue)609         internal byte[] UrlEncode(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue) {
610             byte[] encoded = UrlEncode(bytes, offset, count);
611 
612             return (alwaysCreateNewReturnValue && (encoded != null) && (encoded == bytes))
613                 ? (byte[])encoded.Clone()
614                 : encoded;
615         }
616 
UrlEncode(byte[] bytes, int offset, int count)617         protected internal virtual byte[] UrlEncode(byte[] bytes, int offset, int count) {
618             if (!ValidateUrlEncodingParameters(bytes, offset, count)) {
619                 return null;
620             }
621 
622             int cSpaces = 0;
623             int cUnsafe = 0;
624 
625             // count them first
626             for (int i = 0; i < count; i++) {
627                 char ch = (char)bytes[offset + i];
628 
629                 if (ch == ' ')
630                     cSpaces++;
631                 else if (!HttpEncoderUtility.IsUrlSafeChar(ch))
632                     cUnsafe++;
633             }
634 
635             // nothing to expand?
636             if (cSpaces == 0 && cUnsafe == 0) {
637                 // DevDiv 912606: respect "offset" and "count"
638                 if (0 == offset && bytes.Length == count) {
639                     return bytes;
640                 }
641                 else {
642                     var subarray = new byte[count];
643                     Buffer.BlockCopy(bytes, offset, subarray, 0, count);
644                     return subarray;
645                 }
646             }
647 
648             // expand not 'safe' characters into %XX, spaces to +s
649             byte[] expandedBytes = new byte[count + cUnsafe * 2];
650             int pos = 0;
651 
652             for (int i = 0; i < count; i++) {
653                 byte b = bytes[offset + i];
654                 char ch = (char)b;
655 
656                 if (HttpEncoderUtility.IsUrlSafeChar(ch)) {
657                     expandedBytes[pos++] = b;
658                 }
659                 else if (ch == ' ') {
660                     expandedBytes[pos++] = (byte)'+';
661                 }
662                 else {
663                     expandedBytes[pos++] = (byte)'%';
664                     expandedBytes[pos++] = (byte)HttpEncoderUtility.IntToHex((b >> 4) & 0xf);
665                     expandedBytes[pos++] = (byte)HttpEncoderUtility.IntToHex(b & 0x0f);
666                 }
667             }
668 
669             return expandedBytes;
670         }
671 
672         //  Helper to encode the non-ASCII url characters only
UrlEncodeNonAscii(string str, Encoding e)673         internal String UrlEncodeNonAscii(string str, Encoding e) {
674             if (String.IsNullOrEmpty(str))
675                 return str;
676             if (e == null)
677                 e = Encoding.UTF8;
678             byte[] bytes = e.GetBytes(str);
679             byte[] encodedBytes = UrlEncodeNonAscii(bytes, 0, bytes.Length, false /* alwaysCreateNewReturnValue */);
680             return Encoding.ASCII.GetString(encodedBytes);
681         }
682 
UrlEncodeNonAscii(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue)683         internal byte[] UrlEncodeNonAscii(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue) {
684             if (!ValidateUrlEncodingParameters(bytes, offset, count)) {
685                 return null;
686             }
687 
688             int cNonAscii = 0;
689 
690             // count them first
691             for (int i = 0; i < count; i++) {
692                 if (IsNonAsciiByte(bytes[offset + i]))
693                     cNonAscii++;
694             }
695 
696             // nothing to expand?
697             if (!alwaysCreateNewReturnValue && cNonAscii == 0)
698                 return bytes;
699 
700             // expand not 'safe' characters into %XX, spaces to +s
701             byte[] expandedBytes = new byte[count + cNonAscii * 2];
702             int pos = 0;
703 
704             for (int i = 0; i < count; i++) {
705                 byte b = bytes[offset + i];
706 
707                 if (IsNonAsciiByte(b)) {
708                     expandedBytes[pos++] = (byte)'%';
709                     expandedBytes[pos++] = (byte)HttpEncoderUtility.IntToHex((b >> 4) & 0xf);
710                     expandedBytes[pos++] = (byte)HttpEncoderUtility.IntToHex(b & 0x0f);
711                 }
712                 else {
713                     expandedBytes[pos++] = b;
714                 }
715             }
716 
717             return expandedBytes;
718         }
719 
720         [Obsolete("This method produces non-standards-compliant output and has interoperability issues. The preferred alternative is UrlEncode(*).")]
UrlEncodeUnicode(string value, bool ignoreAscii)721         internal string UrlEncodeUnicode(string value, bool ignoreAscii) {
722             if (value == null) {
723                 return null;
724             }
725 
726             int l = value.Length;
727             StringBuilder sb = new StringBuilder(l);
728 
729             for (int i = 0; i < l; i++) {
730                 char ch = value[i];
731 
732                 if ((ch & 0xff80) == 0) {  // 7 bit?
733                     if (ignoreAscii || HttpEncoderUtility.IsUrlSafeChar(ch)) {
734                         sb.Append(ch);
735                     }
736                     else if (ch == ' ') {
737                         sb.Append('+');
738                     }
739                     else {
740                         sb.Append('%');
741                         sb.Append(HttpEncoderUtility.IntToHex((ch >> 4) & 0xf));
742                         sb.Append(HttpEncoderUtility.IntToHex((ch) & 0xf));
743                     }
744                 }
745                 else { // arbitrary Unicode?
746                     sb.Append("%u");
747                     sb.Append(HttpEncoderUtility.IntToHex((ch >> 12) & 0xf));
748                     sb.Append(HttpEncoderUtility.IntToHex((ch >> 8) & 0xf));
749                     sb.Append(HttpEncoderUtility.IntToHex((ch >> 4) & 0xf));
750                     sb.Append(HttpEncoderUtility.IntToHex((ch) & 0xf));
751                 }
752             }
753 
754             return sb.ToString();
755         }
756 
757         [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings",
758             Justification = "Does not represent an entire URL, just a portion.")]
UrlPathEncode(string value)759         protected internal virtual string UrlPathEncode(string value) {
760             // DevDiv 995259: HttpUtility.UrlPathEncode should not encode IDN part of the url
761             if (BinaryCompatibility.Current.TargetsAtLeastFramework46) {
762                 if (String.IsNullOrEmpty(value)) {
763                     return value;
764                 }
765 
766                 string schemeAndAuthority;
767                 string path;
768                 string queryAndFragment;
769                 bool isValidUrl = UriUtil.TrySplitUriForPathEncode(value, out schemeAndAuthority, out path, out queryAndFragment, checkScheme: false);
770 
771                 if (!isValidUrl) {
772                     // If the value is not a valid url, we treat it as a relative url.
773                     // We don't need to extract query string from the url since UrlPathEncode()
774                     // does not encode query string.
775                     schemeAndAuthority = null;
776                     path = value;
777                     queryAndFragment = null;
778                 }
779 
780                 return schemeAndAuthority + UrlPathEncodeImpl(path) + queryAndFragment;
781             }
782             else {
783                 return UrlPathEncodeImpl(value);
784             }
785         }
786 
787         // This is the original UrlPathEncode(string)
788         [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings",
789             Justification = "Does not represent an entire URL, just a portion.")]
UrlPathEncodeImpl(string value)790         private string UrlPathEncodeImpl(string value) {
791             if (String.IsNullOrEmpty(value)) {
792                 return value;
793             }
794 
795             // recurse in case there is a query string
796             int i = value.IndexOf('?');
797             if (i >= 0)
798                 return UrlPathEncodeImpl(value.Substring(0, i)) + value.Substring(i);
799 
800             // encode DBCS characters and spaces only
801             return HttpEncoderUtility.UrlEncodeSpaces(UrlEncodeNonAscii(value, Encoding.UTF8));
802         }
803 
UrlTokenDecode(string input)804         internal byte[] UrlTokenDecode(string input) {
805             if (input == null)
806                 throw new ArgumentNullException("input");
807 
808             int len = input.Length;
809             if (len < 1)
810                 return new byte[0];
811 
812             ///////////////////////////////////////////////////////////////////
813             // Step 1: Calculate the number of padding chars to append to this string.
814             //         The number of padding chars to append is stored in the last char of the string.
815             int numPadChars = (int)input[len - 1] - (int)'0';
816             if (numPadChars < 0 || numPadChars > 10)
817                 return null;
818 
819 
820             ///////////////////////////////////////////////////////////////////
821             // Step 2: Create array to store the chars (not including the last char)
822             //          and the padding chars
823             char[] base64Chars = new char[len - 1 + numPadChars];
824 
825 
826             ////////////////////////////////////////////////////////
827             // Step 3: Copy in the chars. Transform the "-" to "+", and "*" to "/"
828             for (int iter = 0; iter < len - 1; iter++) {
829                 char c = input[iter];
830 
831                 switch (c) {
832                     case '-':
833                         base64Chars[iter] = '+';
834                         break;
835 
836                     case '_':
837                         base64Chars[iter] = '/';
838                         break;
839 
840                     default:
841                         base64Chars[iter] = c;
842                         break;
843                 }
844             }
845 
846             ////////////////////////////////////////////////////////
847             // Step 4: Add padding chars
848             for (int iter = len - 1; iter < base64Chars.Length; iter++) {
849                 base64Chars[iter] = '=';
850             }
851 
852             // Do the actual conversion
853             return Convert.FromBase64CharArray(base64Chars, 0, base64Chars.Length);
854         }
855 
UrlTokenEncode(byte[] input)856         internal string UrlTokenEncode(byte[] input) {
857             if (input == null)
858                 throw new ArgumentNullException("input");
859             if (input.Length < 1)
860                 return String.Empty;
861 
862             string base64Str = null;
863             int endPos = 0;
864             char[] base64Chars = null;
865 
866             ////////////////////////////////////////////////////////
867             // Step 1: Do a Base64 encoding
868             base64Str = Convert.ToBase64String(input);
869             if (base64Str == null)
870                 return null;
871 
872             ////////////////////////////////////////////////////////
873             // Step 2: Find how many padding chars are present in the end
874             for (endPos = base64Str.Length; endPos > 0; endPos--) {
875                 if (base64Str[endPos - 1] != '=') // Found a non-padding char!
876                 {
877                     break; // Stop here
878                 }
879             }
880 
881             ////////////////////////////////////////////////////////
882             // Step 3: Create char array to store all non-padding chars,
883             //      plus a char to indicate how many padding chars are needed
884             base64Chars = new char[endPos + 1];
885             base64Chars[endPos] = (char)((int)'0' + base64Str.Length - endPos); // Store a char at the end, to indicate how many padding chars are needed
886 
887             ////////////////////////////////////////////////////////
888             // Step 3: Copy in the other chars. Transform the "+" to "-", and "/" to "_"
889             for (int iter = 0; iter < endPos; iter++) {
890                 char c = base64Str[iter];
891 
892                 switch (c) {
893                     case '+':
894                         base64Chars[iter] = '-';
895                         break;
896 
897                     case '/':
898                         base64Chars[iter] = '_';
899                         break;
900 
901                     case '=':
902                         Debug.Assert(false);
903                         base64Chars[iter] = c;
904                         break;
905 
906                     default:
907                         base64Chars[iter] = c;
908                         break;
909                 }
910             }
911             return new string(base64Chars);
912         }
913 
ValidateUrlEncodingParameters(byte[] bytes, int offset, int count)914         internal static bool ValidateUrlEncodingParameters(byte[] bytes, int offset, int count) {
915             if (bytes == null && count == 0)
916                 return false;
917             if (bytes == null) {
918                 throw new ArgumentNullException("bytes");
919             }
920             if (offset < 0 || offset > bytes.Length) {
921                 throw new ArgumentOutOfRangeException("offset");
922             }
923             if (count < 0 || offset + count > bytes.Length) {
924                 throw new ArgumentOutOfRangeException("count");
925             }
926 
927             return true;
928         }
929 
930         // Internal class to facilitate URL decoding -- keeps char buffer and byte buffer, allows appending of either chars or bytes
931         private class UrlDecoder {
932             private int _bufferSize;
933 
934             // Accumulate characters in a special array
935             private int _numChars;
936             private char[] _charBuffer;
937 
938             // Accumulate bytes for decoding into characters in a special array
939             private int _numBytes;
940             private byte[] _byteBuffer;
941 
942             // Encoding to convert chars to bytes
943             private Encoding _encoding;
944 
FlushBytes()945             private void FlushBytes() {
946                 if (_numBytes > 0) {
947                     _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars);
948                     _numBytes = 0;
949                 }
950             }
951 
UrlDecoder(int bufferSize, Encoding encoding)952             internal UrlDecoder(int bufferSize, Encoding encoding) {
953                 _bufferSize = bufferSize;
954                 _encoding = encoding;
955 
956                 _charBuffer = new char[bufferSize];
957                 // byte buffer created on demand
958             }
959 
AddChar(char ch)960             internal void AddChar(char ch) {
961                 if (_numBytes > 0)
962                     FlushBytes();
963 
964                 _charBuffer[_numChars++] = ch;
965             }
966 
AddByte(byte b)967             internal void AddByte(byte b) {
968                 // if there are no pending bytes treat 7 bit bytes as characters
969                 // this optimization is temp disable as it doesn't work for some encodings
970                 /*
971                                 if (_numBytes == 0 && ((b & 0x80) == 0)) {
972                                     AddChar((char)b);
973                                 }
974                                 else
975                 */
976                 {
977                     if (_byteBuffer == null)
978                         _byteBuffer = new byte[_bufferSize];
979 
980                     _byteBuffer[_numBytes++] = b;
981                 }
982             }
983 
GetString()984             internal String GetString() {
985                 if (_numBytes > 0)
986                     FlushBytes();
987 
988                 if (_numChars > 0)
989                     return new String(_charBuffer, 0, _numChars);
990                 else
991                     return String.Empty;
992             }
993         }
994 
995     }
996 }
997