1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 using System.Collections.Generic;
6 using System.Diagnostics;
7 using System.Diagnostics.Contracts;
8 using System.Globalization;
9 using System.IO;
10 using System.Net.Mail;
11 using System.Text;
12 
13 namespace System.Net.Http.Headers
14 {
15     internal static class HeaderUtilities
16     {
17         private const string qualityName = "q";
18 
19         internal const string ConnectionClose = "close";
20         internal static readonly TransferCodingHeaderValue TransferEncodingChunked =
21             new TransferCodingHeaderValue("chunked");
22         internal static readonly NameValueWithParametersHeaderValue ExpectContinue =
23             new NameValueWithParametersHeaderValue("100-continue");
24 
25         internal const string BytesUnit = "bytes";
26 
27         // Validator
28         internal static readonly Action<HttpHeaderValueCollection<string>, string> TokenValidator = ValidateToken;
29 
30         private static readonly char[] s_hexUpperChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
31 
SetQuality(ObjectCollection<NameValueHeaderValue> parameters, double? value)32         internal static void SetQuality(ObjectCollection<NameValueHeaderValue> parameters, double? value)
33         {
34             Debug.Assert(parameters != null);
35 
36             NameValueHeaderValue qualityParameter = NameValueHeaderValue.Find(parameters, qualityName);
37             if (value.HasValue)
38             {
39                 // Note that even if we check the value here, we can't prevent a user from adding an invalid quality
40                 // value using Parameters.Add(). Even if we would prevent the user from adding an invalid value
41                 // using Parameters.Add() he could always add invalid values using HttpHeaders.AddWithoutValidation().
42                 // So this check is really for convenience to show users that they're trying to add an invalid
43                 // value.
44                 if ((value < 0) || (value > 1))
45                 {
46                     throw new ArgumentOutOfRangeException(nameof(value));
47                 }
48 
49                 string qualityString = ((double)value).ToString("0.0##", NumberFormatInfo.InvariantInfo);
50                 if (qualityParameter != null)
51                 {
52                     qualityParameter.Value = qualityString;
53                 }
54                 else
55                 {
56                     parameters.Add(new NameValueHeaderValue(qualityName, qualityString));
57                 }
58             }
59             else
60             {
61                 // Remove quality parameter
62                 if (qualityParameter != null)
63                 {
64                     parameters.Remove(qualityParameter);
65                 }
66             }
67         }
68 
69         // Encode a string using RFC 5987 encoding.
70         // encoding'lang'PercentEncodedSpecials
Encode5987(string input)71         internal static string Encode5987(string input)
72         {
73             string output;
74             IsInputEncoded5987(input, out output);
75 
76             return output;
77         }
78 
IsInputEncoded5987(string input, out string output)79         internal static bool IsInputEncoded5987(string input, out string output)
80         {
81             // Encode a string using RFC 5987 encoding.
82             // encoding'lang'PercentEncodedSpecials
83             bool wasEncoded = false;
84             StringBuilder builder = StringBuilderCache.Acquire();
85             builder.Append("utf-8\'\'");
86             foreach (char c in input)
87             {
88                 // attr-char = ALPHA / DIGIT / "!" / "#" / "$" / "&" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
89                 //      ; token except ( "*" / "'" / "%" )
90                 if (c > 0x7F) // Encodes as multiple utf-8 bytes
91                 {
92                     byte[] bytes = Encoding.UTF8.GetBytes(c.ToString());
93                     foreach (byte b in bytes)
94                     {
95                         AddHexEscaped((char)b, builder);
96                         wasEncoded = true;
97                     }
98                 }
99                 else if (!HttpRuleParser.IsTokenChar(c) || c == '*' || c == '\'' || c == '%')
100                 {
101                     // ASCII - Only one encoded byte.
102                     AddHexEscaped(c, builder);
103                     wasEncoded = true;
104                 }
105                 else
106                 {
107                     builder.Append(c);
108                 }
109 
110             }
111 
112             output = StringBuilderCache.GetStringAndRelease(builder);
113             return wasEncoded;
114         }
115 
116         /// <summary>Transforms an ASCII character into its hexadecimal representation, adding the characters to a StringBuilder.</summary>
AddHexEscaped(char c, StringBuilder destination)117         private static void AddHexEscaped(char c, StringBuilder destination)
118         {
119             Debug.Assert(destination != null);
120             Debug.Assert(c <= 0xFF);
121 
122             destination.Append('%');
123             destination.Append(s_hexUpperChars[(c & 0xf0) >> 4]);
124             destination.Append(s_hexUpperChars[c & 0xf]);
125         }
126 
GetQuality(ObjectCollection<NameValueHeaderValue> parameters)127         internal static double? GetQuality(ObjectCollection<NameValueHeaderValue> parameters)
128         {
129             Debug.Assert(parameters != null);
130 
131             NameValueHeaderValue qualityParameter = NameValueHeaderValue.Find(parameters, qualityName);
132             if (qualityParameter != null)
133             {
134                 // Note that the RFC requires decimal '.' regardless of the culture. I.e. using ',' as decimal
135                 // separator is considered invalid (even if the current culture would allow it).
136                 double qualityValue = 0;
137                 if (double.TryParse(qualityParameter.Value, NumberStyles.AllowDecimalPoint,
138                     NumberFormatInfo.InvariantInfo, out qualityValue))
139                 {
140                     return qualityValue;
141                 }
142                 // If the stored value is an invalid quality value, just return null and log a warning.
143                 if (NetEventSource.IsEnabled) NetEventSource.Error(null, SR.Format(SR.net_http_log_headers_invalid_quality, qualityParameter.Value));
144             }
145             return null;
146         }
147 
CheckValidToken(string value, string parameterName)148         internal static void CheckValidToken(string value, string parameterName)
149         {
150             if (string.IsNullOrEmpty(value))
151             {
152                 throw new ArgumentException(SR.net_http_argument_empty_string, parameterName);
153             }
154 
155             if (HttpRuleParser.GetTokenLength(value, 0) != value.Length)
156             {
157                 throw new FormatException(string.Format(System.Globalization.CultureInfo.InvariantCulture, SR.net_http_headers_invalid_value, value));
158             }
159         }
160 
CheckValidComment(string value, string parameterName)161         internal static void CheckValidComment(string value, string parameterName)
162         {
163             if (string.IsNullOrEmpty(value))
164             {
165                 throw new ArgumentException(SR.net_http_argument_empty_string, parameterName);
166             }
167 
168             int length = 0;
169             if ((HttpRuleParser.GetCommentLength(value, 0, out length) != HttpParseResult.Parsed) ||
170                 (length != value.Length)) // no trailing spaces allowed
171             {
172                 throw new FormatException(string.Format(System.Globalization.CultureInfo.InvariantCulture, SR.net_http_headers_invalid_value, value));
173             }
174         }
175 
CheckValidQuotedString(string value, string parameterName)176         internal static void CheckValidQuotedString(string value, string parameterName)
177         {
178             if (string.IsNullOrEmpty(value))
179             {
180                 throw new ArgumentException(SR.net_http_argument_empty_string, parameterName);
181             }
182 
183             int length = 0;
184             if ((HttpRuleParser.GetQuotedStringLength(value, 0, out length) != HttpParseResult.Parsed) ||
185                 (length != value.Length)) // no trailing spaces allowed
186             {
187                 throw new FormatException(string.Format(System.Globalization.CultureInfo.InvariantCulture, SR.net_http_headers_invalid_value, value));
188             }
189         }
190 
191         internal static bool AreEqualCollections<T>(ObjectCollection<T> x, ObjectCollection<T> y) where T : class
192         {
193             return AreEqualCollections(x, y, null);
194         }
195 
196         internal static bool AreEqualCollections<T>(ObjectCollection<T> x, ObjectCollection<T> y, IEqualityComparer<T> comparer) where T : class
197         {
198             if (x == null)
199             {
200                 return (y == null) || (y.Count == 0);
201             }
202 
203             if (y == null)
204             {
205                 return (x.Count == 0);
206             }
207 
208             if (x.Count != y.Count)
209             {
210                 return false;
211             }
212 
213             if (x.Count == 0)
214             {
215                 return true;
216             }
217 
218             // We have two unordered lists. So comparison is an O(n*m) operation which is expensive. Usually
219             // headers have 1-2 parameters (if any), so this comparison shouldn't be too expensive.
220             bool[] alreadyFound = new bool[x.Count];
221             int i = 0;
222             foreach (var xItem in x)
223             {
224                 Debug.Assert(xItem != null);
225 
226                 i = 0;
227                 bool found = false;
228                 foreach (var yItem in y)
229                 {
230                     if (!alreadyFound[i])
231                     {
232                         if (((comparer == null) && xItem.Equals(yItem)) ||
233                             ((comparer != null) && comparer.Equals(xItem, yItem)))
234                         {
235                             alreadyFound[i] = true;
236                             found = true;
237                             break;
238                         }
239                     }
240                     i++;
241                 }
242 
243                 if (!found)
244                 {
245                     return false;
246                 }
247             }
248 
249             // Since we never re-use a "found" value in 'y', we expect 'alreadyFound' to have all fields set to 'true'.
250             // Otherwise the two collections can't be equal and we should not get here.
Contract.ForAll(alreadyFound, value => { return value; })251             Debug.Assert(Contract.ForAll(alreadyFound, value => { return value; }),
252                 "Expected all values in 'alreadyFound' to be true since collections are considered equal.");
253 
254             return true;
255         }
256 
GetNextNonEmptyOrWhitespaceIndex(string input, int startIndex, bool skipEmptyValues, out bool separatorFound)257         internal static int GetNextNonEmptyOrWhitespaceIndex(string input, int startIndex, bool skipEmptyValues,
258             out bool separatorFound)
259         {
260             Debug.Assert(input != null);
261             Debug.Assert(startIndex <= input.Length); // it's OK if index == value.Length.
262 
263             separatorFound = false;
264             int current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex);
265 
266             if ((current == input.Length) || (input[current] != ','))
267             {
268                 return current;
269             }
270 
271             // If we have a separator, skip the separator and all following whitespace. If we support
272             // empty values, continue until the current character is neither a separator nor a whitespace.
273             separatorFound = true;
274             current++; // skip delimiter.
275             current = current + HttpRuleParser.GetWhitespaceLength(input, current);
276 
277             if (skipEmptyValues)
278             {
279                 while ((current < input.Length) && (input[current] == ','))
280                 {
281                     current++; // skip delimiter.
282                     current = current + HttpRuleParser.GetWhitespaceLength(input, current);
283                 }
284             }
285 
286             return current;
287         }
288 
GetDateTimeOffsetValue(HeaderDescriptor descriptor, HttpHeaders store)289         internal static DateTimeOffset? GetDateTimeOffsetValue(HeaderDescriptor descriptor, HttpHeaders store)
290         {
291             Debug.Assert(store != null);
292 
293             object storedValue = store.GetParsedValues(descriptor);
294             if (storedValue != null)
295             {
296                 return (DateTimeOffset)storedValue;
297             }
298             return null;
299         }
300 
GetTimeSpanValue(HeaderDescriptor descriptor, HttpHeaders store)301         internal static TimeSpan? GetTimeSpanValue(HeaderDescriptor descriptor, HttpHeaders store)
302         {
303             Debug.Assert(store != null);
304 
305             object storedValue = store.GetParsedValues(descriptor);
306             if (storedValue != null)
307             {
308                 return (TimeSpan)storedValue;
309             }
310             return null;
311         }
312 
TryParseInt32(string value, out int result)313         internal static bool TryParseInt32(string value, out int result) =>
314             TryParseInt32(value, 0, value.Length, out result);
315 
TryParseInt32(string value, int offset, int length, out int result)316         internal static bool TryParseInt32(string value, int offset, int length, out int result) // TODO #21281: Replace with int.TryParse(Span<char>) once it's available
317         {
318             if (offset < 0 || length < 0 || offset > value.Length - length)
319             {
320                 result = 0;
321                 return false;
322             }
323 
324             int tmpResult = 0;
325             int pos = offset, endPos = offset + length;
326             while (pos < endPos)
327             {
328                 char c = value[pos++];
329                 int digit = c - '0';
330                 if ((uint)digit > 9 || // invalid digit
331                     tmpResult > int.MaxValue / 10 || // will overflow when shifting digits
332                     (tmpResult == int.MaxValue / 10 && digit > 7)) // will overflow when adding in digit
333                 {
334                     result = 0;
335                     return false;
336                 }
337                 tmpResult = (tmpResult * 10) + digit;
338             }
339 
340             result = tmpResult;
341             return true;
342         }
343 
TryParseInt64(string value, int offset, int length, out long result)344         internal static bool TryParseInt64(string value, int offset, int length, out long result) // TODO #21281: Replace with int.TryParse(Span<char>) once it's available
345         {
346             if (offset < 0 || length < 0 || offset > value.Length - length)
347             {
348                 result = 0;
349                 return false;
350             }
351 
352             long tmpResult = 0;
353             int pos = offset, endPos = offset + length;
354             while (pos < endPos)
355             {
356                 char c = value[pos++];
357                 int digit = c - '0';
358                 if ((uint)digit > 9 || // invalid digit
359                     tmpResult > long.MaxValue / 10 || // will overflow when shifting digits
360                     (tmpResult == long.MaxValue / 10 && digit > 7)) // will overflow when adding in digit
361                 {
362                     result = 0;
363                     return false;
364                 }
365                 tmpResult = (tmpResult * 10) + digit;
366             }
367 
368             result = tmpResult;
369             return true;
370         }
371 
DumpHeaders(params HttpHeaders[] headers)372         internal static string DumpHeaders(params HttpHeaders[] headers)
373         {
374             // Return all headers as string similar to:
375             // {
376             //    HeaderName1: Value1
377             //    HeaderName1: Value2
378             //    HeaderName2: Value1
379             //    ...
380             // }
381             StringBuilder sb = new StringBuilder();
382             sb.Append("{\r\n");
383 
384             for (int i = 0; i < headers.Length; i++)
385             {
386                 if (headers[i] != null)
387                 {
388                     foreach (var header in headers[i])
389                     {
390                         foreach (var headerValue in header.Value)
391                         {
392                             sb.Append("  ");
393                             sb.Append(header.Key);
394                             sb.Append(": ");
395                             sb.Append(headerValue);
396                             sb.Append("\r\n");
397                         }
398                     }
399                 }
400             }
401 
402             sb.Append('}');
403 
404             return sb.ToString();
405         }
406 
IsValidEmailAddress(string value)407         internal static bool IsValidEmailAddress(string value)
408         {
409             try
410             {
411 #if uap
412                 new MailAddress(value);
413 #else
414                 MailAddressParser.ParseAddress(value);
415 #endif
416                 return true;
417             }
418             catch (FormatException e)
419             {
420                 if (NetEventSource.IsEnabled) NetEventSource.Error(null, SR.Format(SR.net_http_log_headers_wrong_email_format, value, e.Message));
421             }
422             return false;
423         }
424 
ValidateToken(HttpHeaderValueCollection<string> collection, string value)425         private static void ValidateToken(HttpHeaderValueCollection<string> collection, string value)
426         {
427             CheckValidToken(value, "item");
428         }
429     }
430 }
431