| | | 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 | | |
| | | 4 | | using System.Collections; |
| | | 5 | | using System.Collections.Generic; |
| | | 6 | | using System.Diagnostics; |
| | | 7 | | using System.Text; |
| | | 8 | | |
| | | 9 | | // Relevant cookie specs: |
| | | 10 | | // |
| | | 11 | | // PERSISTENT CLIENT STATE HTTP COOKIES (1996) |
| | | 12 | | // From <http:// web.archive.org/web/20020803110822/http://wp.netscape.com/newsref/std/cookie_spec.html> |
| | | 13 | | // |
| | | 14 | | // RFC2109 HTTP State Management Mechanism (February 1997) |
| | | 15 | | // From <http:// tools.ietf.org/html/rfc2109> |
| | | 16 | | // |
| | | 17 | | // RFC2965 HTTP State Management Mechanism (October 2000) |
| | | 18 | | // From <http:// tools.ietf.org/html/rfc2965> |
| | | 19 | | // |
| | | 20 | | // RFC6265 HTTP State Management Mechanism (April 2011) |
| | | 21 | | // From <http:// tools.ietf.org/html/rfc6265> |
| | | 22 | | // |
| | | 23 | | // The Version attribute of the cookie header is defined and used only in RFC2109 and RFC2965 cookie |
| | | 24 | | // specs and specifies Version=1. The Version attribute is not used in the Netscape cookie spec |
| | | 25 | | // (considered as Version=0). Nor is it used in the most recent cookie spec, RFC6265, introduced in 2011. |
| | | 26 | | // RFC6265 deprecates all previous cookie specs including the Version attribute. |
| | | 27 | | // |
| | | 28 | | // Cookies without an explicit Domain attribute will only match a potential uri that matches the original |
| | | 29 | | // uri from where the cookie came from. |
| | | 30 | | // For explicit Domain attribute in the cookie, see the rules defined in Cookie.HostMatchesDomain(). |
| | | 31 | | |
| | | 32 | | namespace System.Net |
| | | 33 | | { |
| | | 34 | | internal readonly struct HeaderVariantInfo |
| | | 35 | | { |
| | | 36 | | private readonly string _name; |
| | | 37 | | private readonly CookieVariant _variant; |
| | | 38 | | |
| | | 39 | | internal HeaderVariantInfo(string name, CookieVariant variant) |
| | | 40 | | { |
| | | 41 | | _name = name; |
| | | 42 | | _variant = variant; |
| | | 43 | | } |
| | | 44 | | |
| | | 45 | | internal string Name |
| | | 46 | | { |
| | | 47 | | get |
| | | 48 | | { |
| | | 49 | | return _name; |
| | | 50 | | } |
| | | 51 | | } |
| | | 52 | | |
| | | 53 | | internal CookieVariant Variant |
| | | 54 | | { |
| | | 55 | | get |
| | | 56 | | { |
| | | 57 | | return _variant; |
| | | 58 | | } |
| | | 59 | | } |
| | | 60 | | } |
| | | 61 | | |
| | | 62 | | // CookieContainer |
| | | 63 | | // |
| | | 64 | | // Manage cookies for a user (implicit). Based on RFC 2965. |
| | | 65 | | [Serializable] |
| | | 66 | | [System.Runtime.CompilerServices.TypeForwardedFrom("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c5 |
| | | 67 | | public class CookieContainer |
| | | 68 | | { |
| | | 69 | | public const int DefaultCookieLimit = 300; |
| | | 70 | | public const int DefaultPerDomainCookieLimit = 20; |
| | | 71 | | public const int DefaultCookieLengthLimit = 4096; |
| | | 72 | | |
| | | 73 | | private static readonly HeaderVariantInfo[] s_headerInfo = { |
| | | 74 | | new HeaderVariantInfo(HttpKnownHeaderNames.SetCookie, CookieVariant.Rfc2109), |
| | | 75 | | new HeaderVariantInfo(HttpKnownHeaderNames.SetCookie2, CookieVariant.Rfc2965) |
| | | 76 | | }; |
| | | 77 | | |
| | | 78 | | private readonly Hashtable m_domainTable = new Hashtable(); // Do not rename (binary serialization) |
| | | 79 | | private int m_maxCookieSize = DefaultCookieLengthLimit; // Do not rename (binary serialization) |
| | | 80 | | private int m_maxCookies = DefaultCookieLimit; // Do not rename (binary serialization) |
| | | 81 | | private int m_maxCookiesPerDomain = DefaultPerDomainCookieLimit; // Do not rename (binary serialization) |
| | | 82 | | private int m_count; // Do not rename (binary serialization) |
| | | 83 | | #pragma warning disable CA1823 // Avoid unused private fields |
| | | 84 | | private readonly string m_fqdnMyDomain = string.Empty; |
| | | 85 | | #pragma warning restore CA1823 // Avoid unused private fields |
| | | 86 | | |
| | | 87 | | public CookieContainer() |
| | | 88 | | { |
| | | 89 | | } |
| | | 90 | | |
| | | 91 | | public CookieContainer(int capacity) |
| | | 92 | | { |
| | | 93 | | ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity); |
| | | 94 | | m_maxCookies = capacity; |
| | | 95 | | } |
| | | 96 | | |
| | | 97 | | public CookieContainer(int capacity, int perDomainCapacity, int maxCookieSize) : this(capacity) |
| | | 98 | | { |
| | | 99 | | if (perDomainCapacity != int.MaxValue && (perDomainCapacity <= 0 || perDomainCapacity > capacity)) |
| | | 100 | | { |
| | | 101 | | throw new ArgumentOutOfRangeException(nameof(perDomainCapacity), SR.Format(SR.net_cookie_capacity_range, |
| | | 102 | | } |
| | | 103 | | m_maxCookiesPerDomain = perDomainCapacity; |
| | | 104 | | ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxCookieSize); |
| | | 105 | | m_maxCookieSize = maxCookieSize; |
| | | 106 | | } |
| | | 107 | | |
| | | 108 | | // NOTE: after shrinking the capacity, Count can become greater than Capacity. |
| | | 109 | | public int Capacity |
| | | 110 | | { |
| | | 111 | | get |
| | | 112 | | { |
| | | 113 | | return m_maxCookies; |
| | | 114 | | } |
| | | 115 | | set |
| | | 116 | | { |
| | | 117 | | if (value <= 0 || (value < m_maxCookiesPerDomain && m_maxCookiesPerDomain != int.MaxValue)) |
| | | 118 | | { |
| | | 119 | | throw new ArgumentOutOfRangeException(nameof(value), SR.Format(SR.net_cookie_capacity_range, "Capaci |
| | | 120 | | } |
| | | 121 | | if (value < m_maxCookies) |
| | | 122 | | { |
| | | 123 | | m_maxCookies = value; |
| | | 124 | | AgeCookies(null); |
| | | 125 | | } |
| | | 126 | | m_maxCookies = value; |
| | | 127 | | } |
| | | 128 | | } |
| | | 129 | | |
| | | 130 | | /// <devdoc> |
| | | 131 | | /// <para>Returns the total number of cookies in the container.</para> |
| | | 132 | | /// </devdoc> |
| | | 133 | | public int Count |
| | | 134 | | { |
| | | 135 | | get |
| | | 136 | | { |
| | | 137 | | return m_count; |
| | | 138 | | } |
| | | 139 | | } |
| | | 140 | | |
| | | 141 | | public int MaxCookieSize |
| | | 142 | | { |
| | | 143 | | get |
| | | 144 | | { |
| | | 145 | | return m_maxCookieSize; |
| | | 146 | | } |
| | | 147 | | set |
| | | 148 | | { |
| | | 149 | | ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); |
| | | 150 | | m_maxCookieSize = value; |
| | | 151 | | } |
| | | 152 | | } |
| | | 153 | | |
| | | 154 | | /// <devdoc> |
| | | 155 | | /// <para>After shrinking domain capacity, each domain will less hold than new domain capacity.</para> |
| | | 156 | | /// </devdoc> |
| | | 157 | | public int PerDomainCapacity |
| | | 158 | | { |
| | | 159 | | get |
| | | 160 | | { |
| | | 161 | | return m_maxCookiesPerDomain; |
| | | 162 | | } |
| | | 163 | | set |
| | | 164 | | { |
| | | 165 | | ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); |
| | | 166 | | if (value != int.MaxValue) |
| | | 167 | | { |
| | | 168 | | ArgumentOutOfRangeException.ThrowIfGreaterThan(value, m_maxCookies); |
| | | 169 | | } |
| | | 170 | | |
| | | 171 | | if (value < m_maxCookiesPerDomain) |
| | | 172 | | { |
| | | 173 | | m_maxCookiesPerDomain = value; |
| | | 174 | | AgeCookies(null); |
| | | 175 | | } |
| | | 176 | | m_maxCookiesPerDomain = value; |
| | | 177 | | } |
| | | 178 | | } |
| | | 179 | | |
| | | 180 | | // This method will construct a faked URI: the Domain property is required for param. |
| | | 181 | | public void Add(Cookie cookie) |
| | | 182 | | { |
| | | 183 | | ArgumentNullException.ThrowIfNull(cookie); |
| | | 184 | | |
| | | 185 | | if (cookie.Domain.Length == 0) |
| | | 186 | | { |
| | | 187 | | throw new ArgumentException( |
| | | 188 | | SR.Format(SR.net_emptystringcall, nameof(cookie) + "." + nameof(cookie.Domain)), |
| | | 189 | | nameof(cookie)); |
| | | 190 | | } |
| | | 191 | | |
| | | 192 | | Uri? uri; |
| | | 193 | | var uriSb = new StringBuilder(); |
| | | 194 | | |
| | | 195 | | // We cannot add an invalid cookie into the container. |
| | | 196 | | // Trying to prepare Uri for the cookie verification. |
| | | 197 | | uriSb.Append(cookie.Secure ? UriScheme.Https : UriScheme.Http).Append(UriScheme.SchemeDelimiter); |
| | | 198 | | |
| | | 199 | | // If the original cookie has an explicitly set domain, copy it over to the new cookie. |
| | | 200 | | if (!cookie.DomainImplicit) |
| | | 201 | | { |
| | | 202 | | if (cookie.Domain[0] == '.') |
| | | 203 | | { |
| | | 204 | | uriSb.Append('0'); // URI cctor should consume this faked host. |
| | | 205 | | } |
| | | 206 | | } |
| | | 207 | | uriSb.Append(cookie.Domain); |
| | | 208 | | |
| | | 209 | | |
| | | 210 | | // Either keep Port as implicit or set it according to original cookie. |
| | | 211 | | if (cookie.PortList != null) |
| | | 212 | | { |
| | | 213 | | uriSb.Append(':').Append(cookie.PortList[0]); |
| | | 214 | | } |
| | | 215 | | |
| | | 216 | | // Path must be present, set to root by default. |
| | | 217 | | uriSb.Append(cookie.Path); |
| | | 218 | | |
| | | 219 | | if (!Uri.TryCreate(uriSb.ToString(), UriKind.Absolute, out uri)) |
| | | 220 | | throw new CookieException(SR.Format(SR.net_cookie_attribute, "Domain", cookie.Domain)); |
| | | 221 | | |
| | | 222 | | // We don't know cookie verification status, so re-create the cookie and verify it. |
| | | 223 | | Cookie new_cookie = cookie.Clone(); |
| | | 224 | | new_cookie.VerifyAndSetDefaults(new_cookie.Variant, uri); |
| | | 225 | | |
| | | 226 | | InternalAdd(new_cookie); |
| | | 227 | | } |
| | | 228 | | |
| | | 229 | | // This method is called *only* when cookie verification is done, so unlike with public |
| | | 230 | | // Add(Cookie cookie) the cookie is in a reasonable condition. |
| | | 231 | | internal void InternalAdd(Cookie cookie) |
| | | 232 | | { |
| | | 233 | | PathList? pathList; |
| | | 234 | | |
| | | 235 | | if (cookie.Value.Length > m_maxCookieSize) |
| | | 236 | | { |
| | | 237 | | throw new CookieException(SR.Format(SR.net_cookie_size, cookie, m_maxCookieSize)); |
| | | 238 | | } |
| | | 239 | | |
| | | 240 | | try |
| | | 241 | | { |
| | | 242 | | lock (m_domainTable.SyncRoot) |
| | | 243 | | { |
| | | 244 | | pathList = (PathList?)m_domainTable[cookie.DomainKey]; |
| | | 245 | | if (pathList == null) |
| | | 246 | | { |
| | | 247 | | m_domainTable[cookie.DomainKey] = (pathList = new PathList()); |
| | | 248 | | } |
| | | 249 | | } |
| | | 250 | | int domain_count = pathList.GetCookiesCount(); |
| | | 251 | | |
| | | 252 | | CookieCollection? cookies; |
| | | 253 | | lock (pathList.SyncRoot) |
| | | 254 | | { |
| | | 255 | | cookies = (CookieCollection?)pathList[cookie.Path]!; |
| | | 256 | | |
| | | 257 | | if (cookies == null) |
| | | 258 | | { |
| | | 259 | | cookies = new CookieCollection(); |
| | | 260 | | pathList[cookie.Path] = cookies; |
| | | 261 | | } |
| | | 262 | | } |
| | | 263 | | |
| | | 264 | | if (cookie.Expired) |
| | | 265 | | { |
| | | 266 | | // Explicit removal command (Max-Age == 0) |
| | | 267 | | lock (cookies) |
| | | 268 | | { |
| | | 269 | | int idx = cookies.IndexOf(cookie); |
| | | 270 | | if (idx != -1) |
| | | 271 | | { |
| | | 272 | | cookies.RemoveAt(idx); |
| | | 273 | | --m_count; |
| | | 274 | | } |
| | | 275 | | } |
| | | 276 | | } |
| | | 277 | | else |
| | | 278 | | { |
| | | 279 | | // This is about real cookie adding, check Capacity first |
| | | 280 | | if (domain_count >= m_maxCookiesPerDomain && !AgeCookies(cookie.DomainKey)) |
| | | 281 | | { |
| | | 282 | | return; // Cannot age: reject new cookie |
| | | 283 | | } |
| | | 284 | | else if (m_count >= m_maxCookies && !AgeCookies(null)) |
| | | 285 | | { |
| | | 286 | | return; // Cannot age: reject new cookie |
| | | 287 | | } |
| | | 288 | | |
| | | 289 | | // About to change the collection. |
| | | 290 | | lock (cookies) |
| | | 291 | | { |
| | | 292 | | m_count += cookies.InternalAdd(cookie, true); |
| | | 293 | | } |
| | | 294 | | } |
| | | 295 | | |
| | | 296 | | // We don't want to cleanup m_domaintable/m_list too often. Add check to avoid overhead. |
| | | 297 | | if (m_domainTable.Count > m_count || pathList.Count > m_maxCookiesPerDomain) |
| | | 298 | | { |
| | | 299 | | DomainTableCleanup(); |
| | | 300 | | } |
| | | 301 | | } |
| | | 302 | | catch (OutOfMemoryException) |
| | | 303 | | { |
| | | 304 | | throw; |
| | | 305 | | } |
| | | 306 | | catch (Exception e) |
| | | 307 | | { |
| | | 308 | | throw new CookieException(SR.net_container_add_cookie, e); |
| | | 309 | | } |
| | | 310 | | } |
| | | 311 | | |
| | | 312 | | // This function, when called, must delete at least one cookie. |
| | | 313 | | // If there are expired cookies in given scope they are cleaned up. |
| | | 314 | | // If nothing is found the least used Collection will be found and removed |
| | | 315 | | // from the container. |
| | | 316 | | // |
| | | 317 | | // Also note that expired cookies are also removed during request preparation |
| | | 318 | | // (this.GetCookies method). |
| | | 319 | | // |
| | | 320 | | // Param. 'domain' == null means to age in the whole container. |
| | | 321 | | private bool AgeCookies(string? domain) |
| | | 322 | | { |
| | | 323 | | Debug.Assert(m_maxCookies != 0); |
| | | 324 | | Debug.Assert(m_maxCookiesPerDomain != 0); |
| | | 325 | | |
| | | 326 | | int removed = 0; |
| | | 327 | | DateTime oldUsed = DateTime.MaxValue; |
| | | 328 | | DateTime tempUsed; |
| | | 329 | | |
| | | 330 | | CookieCollection? lruCc = null; |
| | | 331 | | string? lruDomain; |
| | | 332 | | string tempDomain; |
| | | 333 | | |
| | | 334 | | PathList pathList; |
| | | 335 | | int domain_count; |
| | | 336 | | int itemp = 0; |
| | | 337 | | float remainingFraction = 1.0F; |
| | | 338 | | |
| | | 339 | | // The container was shrunk, might need additional cleanup for each domain |
| | | 340 | | if (m_count > m_maxCookies) |
| | | 341 | | { |
| | | 342 | | // Means the fraction of the container to be left. |
| | | 343 | | // Each domain will be cut accordingly. |
| | | 344 | | remainingFraction = (float)m_maxCookies / (float)m_count; |
| | | 345 | | } |
| | | 346 | | lock (m_domainTable.SyncRoot) |
| | | 347 | | { |
| | | 348 | | foreach (object item in m_domainTable) |
| | | 349 | | { |
| | | 350 | | DictionaryEntry entry = (DictionaryEntry)item; |
| | | 351 | | if (domain == null) |
| | | 352 | | { |
| | | 353 | | tempDomain = (string)entry.Key; |
| | | 354 | | pathList = (PathList)entry.Value!; // Aliasing to trick foreach |
| | | 355 | | } |
| | | 356 | | else |
| | | 357 | | { |
| | | 358 | | tempDomain = domain; |
| | | 359 | | pathList = (PathList)m_domainTable[domain]!; |
| | | 360 | | } |
| | | 361 | | |
| | | 362 | | domain_count = 0; // Cookies in the domain |
| | | 363 | | lock (pathList.SyncRoot) |
| | | 364 | | { |
| | | 365 | | foreach (CookieCollection? cc in pathList.Values) |
| | | 366 | | { |
| | | 367 | | Debug.Assert(cc != null); |
| | | 368 | | itemp = ExpireCollection(cc); |
| | | 369 | | removed += itemp; |
| | | 370 | | m_count -= itemp; // Update this container's count |
| | | 371 | | domain_count += cc.Count; |
| | | 372 | | |
| | | 373 | | // We also find the least used cookie collection in ENTIRE container. |
| | | 374 | | // We count the collection as LRU only if it holds 1+ elements. |
| | | 375 | | if (cc.Count > 0 && (tempUsed = cc.TimeStamp(CookieCollection.Stamp.Check)) < oldUsed) |
| | | 376 | | { |
| | | 377 | | lruDomain = tempDomain; |
| | | 378 | | lruCc = cc; |
| | | 379 | | oldUsed = tempUsed; |
| | | 380 | | } |
| | | 381 | | } |
| | | 382 | | } |
| | | 383 | | |
| | | 384 | | // Check if we have reduced to the limit of the domain by expiration only. |
| | | 385 | | int min_count = Math.Min((int)(domain_count * remainingFraction), Math.Min(m_maxCookiesPerDomain, m_ |
| | | 386 | | if (domain_count > min_count) |
| | | 387 | | { |
| | | 388 | | // This case requires sorting all domain collections by timestamp. |
| | | 389 | | CookieCollection[] cookies; |
| | | 390 | | DateTime[] stamps; |
| | | 391 | | lock (pathList.SyncRoot) |
| | | 392 | | { |
| | | 393 | | cookies = new CookieCollection[pathList.Count]; |
| | | 394 | | stamps = new DateTime[pathList.Count]; |
| | | 395 | | foreach (CookieCollection? cc in pathList.Values) |
| | | 396 | | { |
| | | 397 | | stamps[itemp] = cc!.TimeStamp(CookieCollection.Stamp.Check); |
| | | 398 | | cookies[itemp] = cc; |
| | | 399 | | ++itemp; |
| | | 400 | | } |
| | | 401 | | } |
| | | 402 | | Array.Sort(stamps, cookies); |
| | | 403 | | |
| | | 404 | | itemp = 0; |
| | | 405 | | for (int i = 0; i < cookies.Length; ++i) |
| | | 406 | | { |
| | | 407 | | CookieCollection cc = cookies[i]; |
| | | 408 | | |
| | | 409 | | lock (cc) |
| | | 410 | | { |
| | | 411 | | while (domain_count > min_count && cc.Count > 0) |
| | | 412 | | { |
| | | 413 | | cc.RemoveAt(0); |
| | | 414 | | --domain_count; |
| | | 415 | | --m_count; |
| | | 416 | | ++removed; |
| | | 417 | | } |
| | | 418 | | } |
| | | 419 | | if (domain_count <= min_count) |
| | | 420 | | { |
| | | 421 | | break; |
| | | 422 | | } |
| | | 423 | | } |
| | | 424 | | |
| | | 425 | | if (domain_count > min_count && domain != null) |
| | | 426 | | { |
| | | 427 | | // Cannot complete aging of explicit domain (no cookie adding allowed). |
| | | 428 | | return false; |
| | | 429 | | } |
| | | 430 | | } |
| | | 431 | | } |
| | | 432 | | } |
| | | 433 | | |
| | | 434 | | // We have completed aging of the specified domain. |
| | | 435 | | if (domain != null) |
| | | 436 | | { |
| | | 437 | | return true; |
| | | 438 | | } |
| | | 439 | | |
| | | 440 | | // The rest is for entire container aging. |
| | | 441 | | // We must get at least one free slot. |
| | | 442 | | |
| | | 443 | | // Don't need to apply LRU if we already cleaned something. |
| | | 444 | | if (removed != 0) |
| | | 445 | | { |
| | | 446 | | return true; |
| | | 447 | | } |
| | | 448 | | |
| | | 449 | | if (oldUsed == DateTime.MaxValue) |
| | | 450 | | { |
| | | 451 | | // Something strange. Either capacity is 0 or all collections are locked with cc.Used. |
| | | 452 | | return false; |
| | | 453 | | } |
| | | 454 | | |
| | | 455 | | // Remove oldest cookies from the least used collection. |
| | | 456 | | lock (lruCc!) |
| | | 457 | | { |
| | | 458 | | while (m_count >= m_maxCookies && lruCc.Count > 0) |
| | | 459 | | { |
| | | 460 | | lruCc.RemoveAt(0); |
| | | 461 | | --m_count; |
| | | 462 | | } |
| | | 463 | | } |
| | | 464 | | return true; |
| | | 465 | | } |
| | | 466 | | |
| | | 467 | | private void DomainTableCleanup() |
| | | 468 | | { |
| | | 469 | | var removePathList = new List<object>(); |
| | | 470 | | var removeDomainList = new List<string>(); |
| | | 471 | | |
| | | 472 | | string currentDomain; |
| | | 473 | | PathList pathList; |
| | | 474 | | |
| | | 475 | | lock (m_domainTable.SyncRoot) |
| | | 476 | | { |
| | | 477 | | // Manual use of IDictionaryEnumerator instead of foreach to avoid DictionaryEntry box allocations. |
| | | 478 | | IDictionaryEnumerator enumerator = m_domainTable.GetEnumerator(); |
| | | 479 | | while (enumerator.MoveNext()) |
| | | 480 | | { |
| | | 481 | | currentDomain = (string)enumerator.Key; |
| | | 482 | | pathList = (PathList)enumerator.Value!; |
| | | 483 | | |
| | | 484 | | lock (pathList.SyncRoot) |
| | | 485 | | { |
| | | 486 | | IDictionaryEnumerator e = pathList.GetEnumerator(); |
| | | 487 | | while (e.MoveNext()) |
| | | 488 | | { |
| | | 489 | | CookieCollection cc = (CookieCollection)e.Value!; |
| | | 490 | | if (cc.Count == 0) |
| | | 491 | | { |
| | | 492 | | removePathList.Add(e.Key); |
| | | 493 | | } |
| | | 494 | | } |
| | | 495 | | |
| | | 496 | | foreach (var key in removePathList) |
| | | 497 | | { |
| | | 498 | | pathList.Remove(key); |
| | | 499 | | } |
| | | 500 | | |
| | | 501 | | removePathList.Clear(); |
| | | 502 | | if (pathList.Count == 0) removeDomainList.Add(currentDomain); |
| | | 503 | | } |
| | | 504 | | } |
| | | 505 | | |
| | | 506 | | foreach (var key in removeDomainList) |
| | | 507 | | { |
| | | 508 | | m_domainTable.Remove(key); |
| | | 509 | | } |
| | | 510 | | } |
| | | 511 | | } |
| | | 512 | | |
| | | 513 | | // Return number of cookies removed from the collection. |
| | | 514 | | private static int ExpireCollection(CookieCollection cc) |
| | | 515 | | { |
| | | 516 | | lock (cc) |
| | | 517 | | { |
| | | 518 | | int oldCount = cc.Count; |
| | | 519 | | int idx = oldCount - 1; |
| | | 520 | | |
| | | 521 | | // Cannot use enumerator as we are going to alter collection. |
| | | 522 | | while (idx >= 0) |
| | | 523 | | { |
| | | 524 | | Cookie cookie = cc[idx]; |
| | | 525 | | if (cookie.Expired) |
| | | 526 | | { |
| | | 527 | | cc.RemoveAt(idx); |
| | | 528 | | } |
| | | 529 | | --idx; |
| | | 530 | | } |
| | | 531 | | return oldCount - cc.Count; |
| | | 532 | | } |
| | | 533 | | } |
| | | 534 | | |
| | | 535 | | public void Add(CookieCollection cookies) |
| | | 536 | | { |
| | | 537 | | ArgumentNullException.ThrowIfNull(cookies); |
| | | 538 | | |
| | | 539 | | foreach (Cookie c in (ICollection<Cookie>)cookies) |
| | | 540 | | { |
| | | 541 | | Add(c); |
| | | 542 | | } |
| | | 543 | | } |
| | | 544 | | |
| | | 545 | | public void Add(Uri uri, Cookie cookie) |
| | | 546 | | { |
| | | 547 | | ArgumentNullException.ThrowIfNull(uri); |
| | | 548 | | ArgumentNullException.ThrowIfNull(cookie); |
| | | 549 | | |
| | | 550 | | Cookie new_cookie = cookie.Clone(); |
| | | 551 | | new_cookie.VerifyAndSetDefaults(new_cookie.Variant, uri); |
| | | 552 | | |
| | | 553 | | InternalAdd(new_cookie); |
| | | 554 | | } |
| | | 555 | | |
| | | 556 | | public void Add(Uri uri, CookieCollection cookies) |
| | | 557 | | { |
| | | 558 | | ArgumentNullException.ThrowIfNull(uri); |
| | | 559 | | ArgumentNullException.ThrowIfNull(cookies); |
| | | 560 | | |
| | | 561 | | foreach (Cookie c in cookies) |
| | | 562 | | { |
| | | 563 | | Cookie new_cookie = c.Clone(); |
| | | 564 | | new_cookie.VerifyAndSetDefaults(new_cookie.Variant, uri); |
| | | 565 | | InternalAdd(new_cookie); |
| | | 566 | | } |
| | | 567 | | } |
| | | 568 | | |
| | | 569 | | internal CookieCollection CookieCutter(Uri uri, string? headerName, string setCookieHeader) |
| | | 570 | | { |
| | | 571 | | if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"uri:{uri} headerName:{headerName} setCookieH |
| | | 572 | | |
| | | 573 | | CookieCollection cookies = new CookieCollection(); |
| | | 574 | | CookieVariant variant = CookieVariant.Unknown; |
| | | 575 | | if (headerName == null) |
| | | 576 | | { |
| | | 577 | | variant = CookieVariant.Default; |
| | | 578 | | } |
| | | 579 | | else |
| | | 580 | | { |
| | | 581 | | for (int i = 0; i < s_headerInfo.Length; ++i) |
| | | 582 | | { |
| | | 583 | | if ((string.Equals(headerName, s_headerInfo[i].Name, StringComparison.OrdinalIgnoreCase))) |
| | | 584 | | { |
| | | 585 | | variant = s_headerInfo[i].Variant; |
| | | 586 | | } |
| | | 587 | | } |
| | | 588 | | } |
| | | 589 | | |
| | | 590 | | try |
| | | 591 | | { |
| | | 592 | | CookieParser parser = new CookieParser(setCookieHeader); |
| | | 593 | | do |
| | | 594 | | { |
| | | 595 | | Cookie? cookie = parser.Get(); |
| | | 596 | | if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"CookieParser returned cookie:{cookie |
| | | 597 | | |
| | | 598 | | if (cookie == null) |
| | | 599 | | { |
| | | 600 | | if (parser.EndofHeader()) |
| | | 601 | | { |
| | | 602 | | break; |
| | | 603 | | } |
| | | 604 | | continue; |
| | | 605 | | } |
| | | 606 | | |
| | | 607 | | // Parser marks invalid cookies this way |
| | | 608 | | if (string.IsNullOrEmpty(cookie.Name)) |
| | | 609 | | { |
| | | 610 | | throw new CookieException(SR.net_cookie_format); |
| | | 611 | | } |
| | | 612 | | |
| | | 613 | | // This will set the default values from the response URI |
| | | 614 | | // AND will check for cookie validity |
| | | 615 | | cookie.VerifyAndSetDefaults(variant, uri); |
| | | 616 | | // If many same cookies arrive we collapse them into just one, hence setting |
| | | 617 | | // parameter isStrict = true below |
| | | 618 | | cookies.InternalAdd(cookie, true); |
| | | 619 | | } while (true); |
| | | 620 | | } |
| | | 621 | | catch (OutOfMemoryException) |
| | | 622 | | { |
| | | 623 | | throw; |
| | | 624 | | } |
| | | 625 | | catch (Exception e) |
| | | 626 | | { |
| | | 627 | | throw new CookieException(SR.Format(SR.net_cookie_parse_header, uri.AbsoluteUri), e); |
| | | 628 | | } |
| | | 629 | | |
| | | 630 | | int cookiesCount = cookies.Count; |
| | | 631 | | for (int i = 0; i < cookiesCount; i++) |
| | | 632 | | { |
| | | 633 | | InternalAdd((Cookie)cookies[i]); |
| | | 634 | | } |
| | | 635 | | |
| | | 636 | | return cookies; |
| | | 637 | | } |
| | | 638 | | |
| | | 639 | | public CookieCollection GetCookies(Uri uri) |
| | | 640 | | { |
| | | 641 | | ArgumentNullException.ThrowIfNull(uri); |
| | | 642 | | |
| | | 643 | | return InternalGetCookies(uri) ?? new CookieCollection(); |
| | | 644 | | } |
| | | 645 | | |
| | | 646 | | /// <summary>Gets a <see cref="CookieCollection"/> that contains all of the <see cref="Cookie"/> instances in th |
| | | 647 | | /// <returns>A <see cref="CookieCollection"/> that contains all of the <see cref="Cookie"/> instances in the con |
| | | 648 | | public CookieCollection GetAllCookies() |
| | | 649 | | { |
| | | 650 | | var result = new CookieCollection(); |
| | | 651 | | |
| | | 652 | | lock (m_domainTable.SyncRoot) |
| | | 653 | | { |
| | | 654 | | IDictionaryEnumerator lists = m_domainTable.GetEnumerator(); |
| | | 655 | | while (lists.MoveNext()) |
| | | 656 | | { |
| | | 657 | | PathList list = (PathList)lists.Value!; |
| | | 658 | | lock (list.SyncRoot) |
| | | 659 | | { |
| | | 660 | | IDictionaryEnumerator collections = list.List.GetEnumerator(); |
| | | 661 | | while (collections.MoveNext()) |
| | | 662 | | { |
| | | 663 | | result.Add((CookieCollection)collections.Value!); |
| | | 664 | | } |
| | | 665 | | } |
| | | 666 | | } |
| | | 667 | | } |
| | | 668 | | |
| | | 669 | | return result; |
| | | 670 | | } |
| | | 671 | | |
| | | 672 | | internal CookieCollection? InternalGetCookies(Uri uri) |
| | | 673 | | { |
| | | 674 | | if (m_count == 0) |
| | | 675 | | { |
| | | 676 | | return null; |
| | | 677 | | } |
| | | 678 | | |
| | | 679 | | bool isSecure = (uri.Scheme == UriScheme.Https || uri.Scheme == UriScheme.Wss); |
| | | 680 | | int port = uri.Port; |
| | | 681 | | CookieCollection? cookies = null; |
| | | 682 | | |
| | | 683 | | List<string> matchingDomainKeys = [uri.Host]; |
| | | 684 | | ReadOnlySpan<char> host = uri.Host; |
| | | 685 | | int lastDot = host.LastIndexOf('.'); |
| | | 686 | | while (lastDot > 0) |
| | | 687 | | { |
| | | 688 | | int dot = host[..lastDot].LastIndexOf('.'); |
| | | 689 | | if (dot > 0) |
| | | 690 | | { |
| | | 691 | | string match = host[(dot + 1)..].ToString(); |
| | | 692 | | matchingDomainKeys.Add(match); |
| | | 693 | | } |
| | | 694 | | |
| | | 695 | | lastDot = dot; |
| | | 696 | | } |
| | | 697 | | |
| | | 698 | | BuildCookieCollectionFromDomainMatches(uri, isSecure, port, ref cookies, matchingDomainKeys); |
| | | 699 | | return cookies; |
| | | 700 | | } |
| | | 701 | | |
| | | 702 | | private void BuildCookieCollectionFromDomainMatches(Uri uri, bool isSecure, int port, ref CookieCollection? cook |
| | | 703 | | { |
| | | 704 | | for (int i = 0; i < matchingDomainKeys.Count; i++) |
| | | 705 | | { |
| | | 706 | | PathList pathList; |
| | | 707 | | lock (m_domainTable.SyncRoot) |
| | | 708 | | { |
| | | 709 | | pathList = (PathList)m_domainTable[matchingDomainKeys[i]]!; |
| | | 710 | | if (pathList == null) |
| | | 711 | | { |
| | | 712 | | continue; |
| | | 713 | | } |
| | | 714 | | } |
| | | 715 | | |
| | | 716 | | lock (pathList.SyncRoot) |
| | | 717 | | { |
| | | 718 | | SortedList list = pathList.List; |
| | | 719 | | int listCount = list.Count; |
| | | 720 | | for (int e = 0; e < listCount; e++) |
| | | 721 | | { |
| | | 722 | | string path = (string)list.GetKey(e); |
| | | 723 | | if (PathMatch(uri.AbsolutePath, path)) |
| | | 724 | | { |
| | | 725 | | CookieCollection cc = (CookieCollection)list.GetByIndex(e)!; |
| | | 726 | | cc.TimeStamp(CookieCollection.Stamp.Set); |
| | | 727 | | MergeUpdateCollections(ref cookies, uri.Host, cc, port, isSecure); |
| | | 728 | | } |
| | | 729 | | } |
| | | 730 | | } |
| | | 731 | | |
| | | 732 | | // Remove unused domain. |
| | | 733 | | if (pathList.Count == 0) |
| | | 734 | | { |
| | | 735 | | lock (m_domainTable.SyncRoot) |
| | | 736 | | { |
| | | 737 | | m_domainTable.Remove(matchingDomainKeys[i]); |
| | | 738 | | } |
| | | 739 | | } |
| | | 740 | | } |
| | | 741 | | } |
| | | 742 | | |
| | | 743 | | // Implement path-matching according to https://tools.ietf.org/html/rfc6265#section-5.1.4: |
| | | 744 | | // | A request-path path-matches a given cookie-path if at least one of the following conditions holds: |
| | | 745 | | // | - The cookie-path and the request-path are identical. |
| | | 746 | | // | - The cookie-path is a prefix of the request-path, and the last character of the cookie-path is %x2F ("/"). |
| | | 747 | | // | - The cookie-path is a prefix of the request-path, and the first character of the request-path that is not |
| | | 748 | | // The latter conditions are needed to make sure that |
| | | 749 | | // PathMatch("/fooBar, "/foo") == false |
| | | 750 | | // but: |
| | | 751 | | // PathMatch("/foo/bar", "/foo") == true, PathMatch("/foo/bar", "/foo/") == true |
| | | 752 | | private static bool PathMatch(string requestPath, string cookiePath) |
| | | 753 | | { |
| | | 754 | | cookiePath = CookieParser.CheckQuoted(cookiePath); |
| | | 755 | | |
| | | 756 | | if (!requestPath.StartsWith(cookiePath, StringComparison.Ordinal)) |
| | | 757 | | return false; |
| | | 758 | | return requestPath.Length == cookiePath.Length || |
| | | 759 | | cookiePath.EndsWith('/') || |
| | | 760 | | requestPath[cookiePath.Length] == '/'; |
| | | 761 | | } |
| | | 762 | | |
| | | 763 | | private void MergeUpdateCollections(ref CookieCollection? destination, string host, CookieCollection source, int |
| | | 764 | | { |
| | | 765 | | lock (source) |
| | | 766 | | { |
| | | 767 | | // Cannot use foreach as we are going to update 'source' |
| | | 768 | | for (int idx = 0; idx < source.Count; ++idx) |
| | | 769 | | { |
| | | 770 | | bool to_add = false; |
| | | 771 | | |
| | | 772 | | Cookie cookie = source[idx]; |
| | | 773 | | |
| | | 774 | | if (cookie.Expired) |
| | | 775 | | { |
| | | 776 | | // If expired, remove from container and don't add to the destination |
| | | 777 | | source.RemoveAt(idx); |
| | | 778 | | --m_count; |
| | | 779 | | --idx; |
| | | 780 | | } |
| | | 781 | | else |
| | | 782 | | { |
| | | 783 | | if (cookie.PortList != null) |
| | | 784 | | { |
| | | 785 | | foreach (int p in cookie.PortList) |
| | | 786 | | { |
| | | 787 | | if (p == port) |
| | | 788 | | { |
| | | 789 | | to_add = true; |
| | | 790 | | break; |
| | | 791 | | } |
| | | 792 | | } |
| | | 793 | | } |
| | | 794 | | else |
| | | 795 | | { |
| | | 796 | | // It was implicit Port, always OK to add. |
| | | 797 | | to_add = true; |
| | | 798 | | } |
| | | 799 | | |
| | | 800 | | // Refuse to add a secure cookie into an 'unsecure' destination |
| | | 801 | | if (cookie.Secure && !isSecure) |
| | | 802 | | { |
| | | 803 | | to_add = false; |
| | | 804 | | } |
| | | 805 | | |
| | | 806 | | // For implicit domains exact match is needed |
| | | 807 | | if (cookie.DomainImplicit && !string.Equals(host, cookie.Domain, StringComparison.OrdinalIgnoreC |
| | | 808 | | { |
| | | 809 | | to_add = false; |
| | | 810 | | } |
| | | 811 | | |
| | | 812 | | if (to_add) |
| | | 813 | | { |
| | | 814 | | // In 'source' are already ordered. |
| | | 815 | | // If two same cookies come from different 'source' then they |
| | | 816 | | // will follow (not replace) each other. |
| | | 817 | | destination ??= new CookieCollection(); |
| | | 818 | | destination.InternalAdd(cookie, false); |
| | | 819 | | } |
| | | 820 | | } |
| | | 821 | | } |
| | | 822 | | } |
| | | 823 | | } |
| | | 824 | | |
| | | 825 | | public string GetCookieHeader(Uri uri) |
| | | 826 | | { |
| | | 827 | | ArgumentNullException.ThrowIfNull(uri); |
| | | 828 | | |
| | | 829 | | return GetCookieHeader(uri, out _); |
| | | 830 | | } |
| | | 831 | | |
| | | 832 | | internal string GetCookieHeader(Uri uri, out string optCookie2) |
| | | 833 | | { |
| | | 834 | | CookieCollection? cookies = InternalGetCookies(uri); |
| | | 835 | | if (cookies == null) |
| | | 836 | | { |
| | | 837 | | optCookie2 = string.Empty; |
| | | 838 | | return string.Empty; |
| | | 839 | | } |
| | | 840 | | |
| | | 841 | | string delimiter = string.Empty; |
| | | 842 | | |
| | | 843 | | StringBuilder builder = StringBuilderCache.Acquire(); |
| | | 844 | | for (int i = 0; i < cookies.Count; i++) |
| | | 845 | | { |
| | | 846 | | builder.Append(delimiter); |
| | | 847 | | cookies[i].ToString(builder); |
| | | 848 | | |
| | | 849 | | delimiter = "; "; |
| | | 850 | | } |
| | | 851 | | |
| | | 852 | | optCookie2 = cookies.IsOtherVersionSeen ? |
| | | 853 | | (Cookie.SpecialAttributeLiteral + |
| | | 854 | | CookieFields.VersionAttributeName + |
| | | 855 | | Cookie.EqualsLiteral + |
| | | 856 | | Cookie.MaxSupportedVersionString) : string.Empty; |
| | | 857 | | |
| | | 858 | | return StringBuilderCache.GetStringAndRelease(builder); |
| | | 859 | | } |
| | | 860 | | |
| | | 861 | | public void SetCookies(Uri uri, string cookieHeader) |
| | | 862 | | { |
| | | 863 | | ArgumentNullException.ThrowIfNull(uri); |
| | | 864 | | ArgumentNullException.ThrowIfNull(cookieHeader); |
| | | 865 | | |
| | | 866 | | CookieCutter(uri, null, cookieHeader); // Will throw on error |
| | | 867 | | } |
| | | 868 | | } |
| | | 869 | | |
| | | 870 | | // PathList needs to be public in order to maintain binary serialization compatibility as the System shim |
| | | 871 | | // needs to have access to type-forward it. |
| | | 872 | | [Serializable] |
| | | 873 | | [System.Runtime.CompilerServices.TypeForwardedFrom("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c5 |
| | | 874 | | public sealed class PathList |
| | | 875 | | { |
| | | 876 | | // Usage of PathList depends on it being shallowly immutable; |
| | | 877 | | // adding any mutable fields to it would result in breaks. |
| | 0 | 878 | | private readonly SortedList m_list = SortedList.Synchronized(new SortedList(PathListComparer.StaticInstance)); / |
| | | 879 | | |
| | 0 | 880 | | internal int Count => m_list.Count; |
| | | 881 | | |
| | | 882 | | internal int GetCookiesCount() |
| | 0 | 883 | | { |
| | 0 | 884 | | int count = 0; |
| | 0 | 885 | | lock (SyncRoot) |
| | 0 | 886 | | { |
| | 0 | 887 | | IList list = m_list.GetValueList(); |
| | 0 | 888 | | int listCount = list.Count; |
| | 0 | 889 | | for (int i = 0; i < listCount; i++) |
| | 0 | 890 | | { |
| | 0 | 891 | | count += ((CookieCollection)list[i]!).Count; |
| | 0 | 892 | | } |
| | 0 | 893 | | } |
| | 0 | 894 | | return count; |
| | 0 | 895 | | } |
| | | 896 | | |
| | | 897 | | internal ICollection Values |
| | | 898 | | { |
| | | 899 | | get |
| | 0 | 900 | | { |
| | 0 | 901 | | return m_list.Values; |
| | 0 | 902 | | } |
| | | 903 | | } |
| | | 904 | | |
| | | 905 | | internal object? this[string s] |
| | | 906 | | { |
| | | 907 | | get |
| | 0 | 908 | | { |
| | 0 | 909 | | lock (SyncRoot) |
| | 0 | 910 | | { |
| | 0 | 911 | | return m_list[s]; |
| | | 912 | | } |
| | 0 | 913 | | } |
| | | 914 | | set |
| | 0 | 915 | | { |
| | 0 | 916 | | lock (SyncRoot) |
| | 0 | 917 | | { |
| | 0 | 918 | | Debug.Assert(value != null); |
| | 0 | 919 | | m_list[s] = value; |
| | 0 | 920 | | } |
| | 0 | 921 | | } |
| | | 922 | | } |
| | | 923 | | |
| | | 924 | | internal IDictionaryEnumerator GetEnumerator() |
| | 0 | 925 | | { |
| | 0 | 926 | | lock (SyncRoot) |
| | 0 | 927 | | { |
| | 0 | 928 | | return m_list.GetEnumerator(); |
| | | 929 | | } |
| | 0 | 930 | | } |
| | | 931 | | |
| | | 932 | | internal void Remove(object key) |
| | 0 | 933 | | { |
| | 0 | 934 | | lock (SyncRoot) |
| | 0 | 935 | | { |
| | 0 | 936 | | m_list.Remove(key); |
| | 0 | 937 | | } |
| | 0 | 938 | | } |
| | | 939 | | |
| | 0 | 940 | | internal SortedList List => m_list; |
| | | 941 | | |
| | 0 | 942 | | internal object SyncRoot => m_list.SyncRoot; |
| | | 943 | | |
| | | 944 | | [Serializable] |
| | | 945 | | [System.Runtime.CompilerServices.TypeForwardedFrom("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77 |
| | | 946 | | private sealed class PathListComparer : IComparer |
| | | 947 | | { |
| | 0 | 948 | | internal static readonly PathListComparer StaticInstance = new PathListComparer(); |
| | | 949 | | |
| | | 950 | | int IComparer.Compare(object? ol, object? or) |
| | 0 | 951 | | { |
| | 0 | 952 | | string pathLeft = CookieParser.CheckQuoted((string)ol!); |
| | 0 | 953 | | string pathRight = CookieParser.CheckQuoted((string)or!); |
| | 0 | 954 | | int ll = pathLeft.Length; |
| | 0 | 955 | | int lr = pathRight.Length; |
| | 0 | 956 | | int length = Math.Min(ll, lr); |
| | | 957 | | |
| | 0 | 958 | | for (int i = 0; i < length; ++i) |
| | 0 | 959 | | { |
| | 0 | 960 | | if (pathLeft[i] != pathRight[i]) |
| | 0 | 961 | | { |
| | 0 | 962 | | return pathLeft[i] - pathRight[i]; |
| | | 963 | | } |
| | 0 | 964 | | } |
| | 0 | 965 | | return lr - ll; |
| | 0 | 966 | | } |
| | | 967 | | } |
| | | 968 | | } |
| | | 969 | | } |
| | | 970 | | |