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