File: System\Net\CookieContainer.cs
Web Access
Project: src\src\libraries\System.Net.Primitives\src\System.Net.Primitives.csproj (System.Net.Primitives)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.NetworkInformation;
using System.Text;
 
// Relevant cookie specs:
//
// PERSISTENT CLIENT STATE HTTP COOKIES (1996)
// From <http:// web.archive.org/web/20020803110822/http://wp.netscape.com/newsref/std/cookie_spec.html>
//
// RFC2109 HTTP State Management Mechanism (February 1997)
// From <http:// tools.ietf.org/html/rfc2109>
//
// RFC2965 HTTP State Management Mechanism (October 2000)
// From <http:// tools.ietf.org/html/rfc2965>
//
// RFC6265 HTTP State Management Mechanism (April 2011)
// From <http:// tools.ietf.org/html/rfc6265>
//
// The Version attribute of the cookie header is defined and used only in RFC2109 and RFC2965 cookie
// specs and specifies Version=1. The Version attribute is not used in the  Netscape cookie spec
// (considered as Version=0). Nor is it used in the most recent cookie spec, RFC6265, introduced in 2011.
// RFC6265 deprecates all previous cookie specs including the Version attribute.
//
// Cookies without an explicit Domain attribute will only match a potential uri that matches the original
// uri from where the cookie came from.
//
// For explicit Domain attribute in the cookie, the following rules apply:
//
// Version=0 (Netscape, RFC6265) allows the Domain attribute of the cookie to match any tail substring
// of the host uri.
//
// Version=1 related cookie specs only allows the Domain attribute to match the host uri based on a
// more restricted set of rules.
//
// According to RFC2109/RFC2965, the cookie will be rejected for matching if:
// * The value for the Domain attribute contains no embedded dots or does not start with a dot.
// * The value for the request-host does not domain-match the Domain attribute.
// " The request-host is a FQDN (not IP address) and has the form HD, where D is the value of the Domain
//  attribute, and H is a string that contains one or more dots.
//
// Examples:
// * A cookie from request-host y.x.foo.com for Domain=.foo.com would be rejected, because H is y.x
//  and contains a dot.
//
// * A cookie from request-host x.foo.com for Domain=.foo.com would be accepted.
//
// * A cookie with Domain=.com or Domain=.com., will always be rejected, because there is no embedded dot.
//
// * A cookie with Domain=ajax.com will be rejected because the value for Domain does not begin with a dot.
 
namespace System.Net
{
    internal readonly struct HeaderVariantInfo
    {
        private readonly string _name;
        private readonly CookieVariant _variant;
 
        internal HeaderVariantInfo(string name, CookieVariant variant)
        {
            _name = name;
            _variant = variant;
        }
 
        internal string Name
        {
            get
            {
                return _name;
            }
        }
 
        internal CookieVariant Variant
        {
            get
            {
                return _variant;
            }
        }
    }
 
    // CookieContainer
    //
    // Manage cookies for a user (implicit). Based on RFC 2965.
    [Serializable]
    [System.Runtime.CompilerServices.TypeForwardedFrom("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
    public class CookieContainer
    {
        public const int DefaultCookieLimit = 300;
        public const int DefaultPerDomainCookieLimit = 20;
        public const int DefaultCookieLengthLimit = 4096;
 
        private static readonly string s_fqdnMyDomain = CreateFqdnMyDomain();
        private static readonly HeaderVariantInfo[] s_headerInfo = {
            new HeaderVariantInfo(HttpKnownHeaderNames.SetCookie,  CookieVariant.Rfc2109),
            new HeaderVariantInfo(HttpKnownHeaderNames.SetCookie2, CookieVariant.Rfc2965)
        };
 
        private readonly Hashtable m_domainTable = new Hashtable(); // Do not rename (binary serialization)
        private int m_maxCookieSize = DefaultCookieLengthLimit; // Do not rename (binary serialization)
        private int m_maxCookies = DefaultCookieLimit; // Do not rename (binary serialization)
        private int m_maxCookiesPerDomain = DefaultPerDomainCookieLimit; // Do not rename (binary serialization)
        private int m_count; // Do not rename (binary serialization)
        private readonly string m_fqdnMyDomain = s_fqdnMyDomain; // Do not rename (binary serialization)
 
        public CookieContainer()
        {
        }
 
        public CookieContainer(int capacity)
        {
            ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity);
            m_maxCookies = capacity;
        }
 
        public CookieContainer(int capacity, int perDomainCapacity, int maxCookieSize) : this(capacity)
        {
            if (perDomainCapacity != int.MaxValue && (perDomainCapacity <= 0 || perDomainCapacity > capacity))
            {
                throw new ArgumentOutOfRangeException(nameof(perDomainCapacity), SR.Format(SR.net_cookie_capacity_range, "PerDomainCapacity", 0, capacity));
            }
            m_maxCookiesPerDomain = perDomainCapacity;
            ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxCookieSize);
            m_maxCookieSize = maxCookieSize;
        }
 
        private static string CreateFqdnMyDomain()
        {
            string domain = HostInformation.DomainName;
            return domain != null && domain.Length > 1 ?
                '.' + domain :
                string.Empty;
        }
 
        // NOTE: after shrinking the capacity, Count can become greater than Capacity.
        public int Capacity
        {
            get
            {
                return m_maxCookies;
            }
            set
            {
                if (value <= 0 || (value < m_maxCookiesPerDomain && m_maxCookiesPerDomain != int.MaxValue))
                {
                    throw new ArgumentOutOfRangeException(nameof(value), SR.Format(SR.net_cookie_capacity_range, "Capacity", 0, m_maxCookiesPerDomain));
                }
                if (value < m_maxCookies)
                {
                    m_maxCookies = value;
                    AgeCookies(null);
                }
                m_maxCookies = value;
            }
        }
 
        /// <devdoc>
        ///   <para>Returns the total number of cookies in the container.</para>
        /// </devdoc>
        public int Count
        {
            get
            {
                return m_count;
            }
        }
 
        public int MaxCookieSize
        {
            get
            {
                return m_maxCookieSize;
            }
            set
            {
                ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
                m_maxCookieSize = value;
            }
        }
 
        /// <devdoc>
        ///   <para>After shrinking domain capacity, each domain will less hold than new domain capacity.</para>
        /// </devdoc>
        public int PerDomainCapacity
        {
            get
            {
                return m_maxCookiesPerDomain;
            }
            set
            {
                ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
                if (value != int.MaxValue)
                {
                    ArgumentOutOfRangeException.ThrowIfGreaterThan(value, m_maxCookies);
                }
 
                if (value < m_maxCookiesPerDomain)
                {
                    m_maxCookiesPerDomain = value;
                    AgeCookies(null);
                }
                m_maxCookiesPerDomain = value;
            }
        }
 
        // This method will construct a faked URI: the Domain property is required for param.
        public void Add(Cookie cookie)
        {
            ArgumentNullException.ThrowIfNull(cookie);
 
            if (cookie.Domain.Length == 0)
            {
                throw new ArgumentException(
                    SR.Format(SR.net_emptystringcall, nameof(cookie) + "." + nameof(cookie.Domain)),
                    nameof(cookie));
            }
 
            Uri? uri;
            var uriSb = new StringBuilder();
 
            // We cannot add an invalid cookie into the container.
            // Trying to prepare Uri for the cookie verification.
            uriSb.Append(cookie.Secure ? UriScheme.Https : UriScheme.Http).Append(UriScheme.SchemeDelimiter);
 
            // If the original cookie has an explicitly set domain, copy it over to the new cookie.
            if (!cookie.DomainImplicit)
            {
                if (cookie.Domain[0] == '.')
                {
                    uriSb.Append('0'); // URI cctor should consume this faked host.
                }
            }
            uriSb.Append(cookie.Domain);
 
 
            // Either keep Port as implicit or set it according to original cookie.
            if (cookie.PortList != null)
            {
                uriSb.Append(':').Append(cookie.PortList[0]);
            }
 
            // Path must be present, set to root by default.
            uriSb.Append(cookie.Path);
 
            if (!Uri.TryCreate(uriSb.ToString(), UriKind.Absolute, out uri))
                throw new CookieException(SR.Format(SR.net_cookie_attribute, "Domain", cookie.Domain));
 
            // We don't know cookie verification status, so re-create the cookie and verify it.
            Cookie new_cookie = cookie.Clone();
            new_cookie.VerifySetDefaults(new_cookie.Variant, uri, IsLocalDomain(uri.Host), m_fqdnMyDomain, true, true);
 
            Add(new_cookie, true);
        }
 
        // This method is called *only* when cookie verification is done, so unlike with public
        // Add(Cookie cookie) the cookie is in a reasonable condition.
        internal void Add(Cookie cookie, bool throwOnError)
        {
            PathList? pathList;
 
            if (cookie.Value.Length > m_maxCookieSize)
            {
                if (throwOnError)
                {
                    throw new CookieException(SR.Format(SR.net_cookie_size, cookie, m_maxCookieSize));
                }
                return;
            }
 
            try
            {
                lock (m_domainTable.SyncRoot)
                {
                    pathList = (PathList?)m_domainTable[cookie.DomainKey];
                    if (pathList == null)
                    {
                        m_domainTable[cookie.DomainKey] = (pathList = new PathList());
                    }
                }
                int domain_count = pathList.GetCookiesCount();
 
                CookieCollection? cookies;
                lock (pathList.SyncRoot)
                {
                    cookies = (CookieCollection?)pathList[cookie.Path]!;
 
                    if (cookies == null)
                    {
                        cookies = new CookieCollection();
                        pathList[cookie.Path] = cookies;
                    }
                }
 
                if (cookie.Expired)
                {
                    // Explicit removal command (Max-Age == 0)
                    lock (cookies)
                    {
                        int idx = cookies.IndexOf(cookie);
                        if (idx != -1)
                        {
                            cookies.RemoveAt(idx);
                            --m_count;
                        }
                    }
                }
                else
                {
                    // This is about real cookie adding, check Capacity first
                    if (domain_count >= m_maxCookiesPerDomain && !AgeCookies(cookie.DomainKey))
                    {
                        return; // Cannot age: reject new cookie
                    }
                    else if (m_count >= m_maxCookies && !AgeCookies(null))
                    {
                        return; // Cannot age: reject new cookie
                    }
 
                    // About to change the collection.
                    lock (cookies)
                    {
                        m_count += cookies.InternalAdd(cookie, true);
                    }
                }
 
                // We don't want to cleanup m_domaintable/m_list too often. Add check to avoid overhead.
                if (m_domainTable.Count > m_count || pathList.Count > m_maxCookiesPerDomain)
                {
                    DomainTableCleanup();
                }
            }
            catch (OutOfMemoryException)
            {
                throw;
            }
            catch (Exception e)
            {
                if (throwOnError)
                {
                    throw new CookieException(SR.net_container_add_cookie, e);
                }
            }
        }
 
        // This function, when called, must delete at least one cookie.
        // If there are expired cookies in given scope they are cleaned up.
        // If nothing is found the least used Collection will be found and removed
        // from the container.
        //
        // Also note that expired cookies are also removed during request preparation
        // (this.GetCookies method).
        //
        // Param. 'domain' == null means to age in the whole container.
        private bool AgeCookies(string? domain)
        {
            Debug.Assert(m_maxCookies != 0);
            Debug.Assert(m_maxCookiesPerDomain != 0);
 
            int removed = 0;
            DateTime oldUsed = DateTime.MaxValue;
            DateTime tempUsed;
 
            CookieCollection? lruCc = null;
            string? lruDomain;
            string tempDomain;
 
            PathList pathList;
            int domain_count;
            int itemp = 0;
            float remainingFraction = 1.0F;
 
            // The container was shrunk, might need additional cleanup for each domain
            if (m_count > m_maxCookies)
            {
                // Means the fraction of the container to be left.
                // Each domain will be cut accordingly.
                remainingFraction = (float)m_maxCookies / (float)m_count;
            }
            lock (m_domainTable.SyncRoot)
            {
                foreach (object item in m_domainTable)
                {
                    DictionaryEntry entry = (DictionaryEntry)item;
                    if (domain == null)
                    {
                        tempDomain = (string)entry.Key;
                        pathList = (PathList)entry.Value!; // Aliasing to trick foreach
                    }
                    else
                    {
                        tempDomain = domain;
                        pathList = (PathList)m_domainTable[domain]!;
                    }
 
                    domain_count = 0; // Cookies in the domain
                    lock (pathList.SyncRoot)
                    {
                        foreach (CookieCollection? cc in pathList.Values)
                        {
                            Debug.Assert(cc != null);
                            itemp = ExpireCollection(cc);
                            removed += itemp;
                            m_count -= itemp; // Update this container's count
                            domain_count += cc.Count;
 
                            // We also find the least used cookie collection in ENTIRE container.
                            // We count the collection as LRU only if it holds 1+ elements.
                            if (cc.Count > 0 && (tempUsed = cc.TimeStamp(CookieCollection.Stamp.Check)) < oldUsed)
                            {
                                lruDomain = tempDomain;
                                lruCc = cc;
                                oldUsed = tempUsed;
                            }
                        }
                    }
 
                    // Check if we have reduced to the limit of the domain by expiration only.
                    int min_count = Math.Min((int)(domain_count * remainingFraction), Math.Min(m_maxCookiesPerDomain, m_maxCookies) - 1);
                    if (domain_count > min_count)
                    {
                        // This case requires sorting all domain collections by timestamp.
                        CookieCollection[] cookies;
                        DateTime[] stamps;
                        lock (pathList.SyncRoot)
                        {
                            cookies = new CookieCollection[pathList.Count];
                            stamps = new DateTime[pathList.Count];
                            foreach (CookieCollection? cc in pathList.Values)
                            {
                                stamps[itemp] = cc!.TimeStamp(CookieCollection.Stamp.Check);
                                cookies[itemp] = cc;
                                ++itemp;
                            }
                        }
                        Array.Sort(stamps, cookies);
 
                        itemp = 0;
                        for (int i = 0; i < cookies.Length; ++i)
                        {
                            CookieCollection cc = cookies[i];
 
                            lock (cc)
                            {
                                while (domain_count > min_count && cc.Count > 0)
                                {
                                    cc.RemoveAt(0);
                                    --domain_count;
                                    --m_count;
                                    ++removed;
                                }
                            }
                            if (domain_count <= min_count)
                            {
                                break;
                            }
                        }
 
                        if (domain_count > min_count && domain != null)
                        {
                            // Cannot complete aging of explicit domain (no cookie adding allowed).
                            return false;
                        }
                    }
                }
            }
 
            // We have completed aging of the specified domain.
            if (domain != null)
            {
                return true;
            }
 
            // The rest is for entire container aging.
            // We must get at least one free slot.
 
            // Don't need to apply LRU if we already cleaned something.
            if (removed != 0)
            {
                return true;
            }
 
            if (oldUsed == DateTime.MaxValue)
            {
                // Something strange. Either capacity is 0 or all collections are locked with cc.Used.
                return false;
            }
 
            // Remove oldest cookies from the least used collection.
            lock (lruCc!)
            {
                while (m_count >= m_maxCookies && lruCc.Count > 0)
                {
                    lruCc.RemoveAt(0);
                    --m_count;
                }
            }
            return true;
        }
 
        private void DomainTableCleanup()
        {
            var removePathList = new List<object>();
            var removeDomainList = new List<string>();
 
            string currentDomain;
            PathList pathList;
 
            lock (m_domainTable.SyncRoot)
            {
                // Manual use of IDictionaryEnumerator instead of foreach to avoid DictionaryEntry box allocations.
                IDictionaryEnumerator enumerator = m_domainTable.GetEnumerator();
                while (enumerator.MoveNext())
                {
                    currentDomain = (string)enumerator.Key;
                    pathList = (PathList)enumerator.Value!;
 
                    lock (pathList.SyncRoot)
                    {
                        IDictionaryEnumerator e = pathList.GetEnumerator();
                        while (e.MoveNext())
                        {
                            CookieCollection cc = (CookieCollection)e.Value!;
                            if (cc.Count == 0)
                            {
                                removePathList.Add(e.Key);
                            }
                        }
 
                        foreach (var key in removePathList)
                        {
                            pathList.Remove(key);
                        }
 
                        removePathList.Clear();
                        if (pathList.Count == 0) removeDomainList.Add(currentDomain);
                    }
                }
 
                foreach (var key in removeDomainList)
                {
                    m_domainTable.Remove(key);
                }
            }
        }
 
        // Return number of cookies removed from the collection.
        private static int ExpireCollection(CookieCollection cc)
        {
            lock (cc)
            {
                int oldCount = cc.Count;
                int idx = oldCount - 1;
 
                // Cannot use enumerator as we are going to alter collection.
                while (idx >= 0)
                {
                    Cookie cookie = cc[idx];
                    if (cookie.Expired)
                    {
                        cc.RemoveAt(idx);
                    }
                    --idx;
                }
                return oldCount - cc.Count;
            }
        }
 
        public void Add(CookieCollection cookies)
        {
            ArgumentNullException.ThrowIfNull(cookies);
 
            foreach (Cookie c in (ICollection<Cookie>)cookies)
            {
                Add(c);
            }
        }
 
        // This will try (if needed) get the full domain name of the host given the Uri.
        // NEVER call this function from internal methods with 'fqdnRemote' == null.
        // Since this method counts security issue for DNS and hence will slow
        // the performance.
        internal bool IsLocalDomain(string host)
        {
            int dot = host.IndexOf('.');
            if (dot == -1)
            {
                // No choice but to treat it as a host on the local domain.
                // This also covers 'localhost' and 'loopback'.
                return true;
            }
 
            // Quick test for typical cases: loopback addresses for IPv4 and IPv6.
            if ((host == "127.0.0.1") || (host == "::1") || (host == "0:0:0:0:0:0:0:1"))
            {
                return true;
            }
 
            // Test domain membership.
            if (string.Compare(m_fqdnMyDomain, 0, host, dot, m_fqdnMyDomain.Length, StringComparison.OrdinalIgnoreCase) == 0)
            {
                return true;
            }
 
            // Test for "127.###.###.###" without using regex.
            ReadOnlySpan<char> hostSpan = host;
            Span<Range> ipParts = stackalloc Range[5];
            ipParts = ipParts.Slice(0, hostSpan.Split(ipParts, '.'));
            if (ipParts.Length == 4 && hostSpan[ipParts[0]] is "127")
            {
                int i;
                for (i = 1; i < ipParts.Length; i++)
                {
                    ReadOnlySpan<char> part = hostSpan[ipParts[i]];
                    switch (part.Length)
                    {
                        case 3:
                            if (!char.IsAsciiDigit(part[2]))
                            {
                                break;
                            }
                            goto case 2;
 
                        case 2:
                            if (!char.IsAsciiDigit(part[1]))
                            {
                                break;
                            }
                            goto case 1;
 
                        case 1:
                            if (!char.IsAsciiDigit(part[0]))
                            {
                                break;
                            }
                            continue;
                    }
                    break;
                }
                if (i == 4)
                {
                    return true;
                }
            }
 
            return false;
        }
 
        public void Add(Uri uri, Cookie cookie)
        {
            ArgumentNullException.ThrowIfNull(uri);
            ArgumentNullException.ThrowIfNull(cookie);
 
            Cookie new_cookie = cookie.Clone();
            new_cookie.VerifySetDefaults(new_cookie.Variant, uri, IsLocalDomain(uri.Host), m_fqdnMyDomain, true, true);
 
            Add(new_cookie, true);
        }
 
        public void Add(Uri uri, CookieCollection cookies)
        {
            ArgumentNullException.ThrowIfNull(uri);
            ArgumentNullException.ThrowIfNull(cookies);
 
            bool isLocalDomain = IsLocalDomain(uri.Host);
            foreach (Cookie c in cookies)
            {
                Cookie new_cookie = c.Clone();
                new_cookie.VerifySetDefaults(new_cookie.Variant, uri, isLocalDomain, m_fqdnMyDomain, true, true);
                Add(new_cookie, true);
            }
        }
 
        internal CookieCollection CookieCutter(Uri uri, string? headerName, string setCookieHeader, bool isThrow)
        {
            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"uri:{uri} headerName:{headerName} setCookieHeader:{setCookieHeader} isThrow:{isThrow}");
 
            CookieCollection cookies = new CookieCollection();
            CookieVariant variant = CookieVariant.Unknown;
            if (headerName == null)
            {
                variant = CookieVariant.Default;
            }
            else
            {
                for (int i = 0; i < s_headerInfo.Length; ++i)
                {
                    if ((string.Equals(headerName, s_headerInfo[i].Name, StringComparison.OrdinalIgnoreCase)))
                    {
                        variant = s_headerInfo[i].Variant;
                    }
                }
            }
 
            bool isLocalDomain = IsLocalDomain(uri.Host);
            try
            {
                CookieParser parser = new CookieParser(setCookieHeader);
                do
                {
                    Cookie? cookie = parser.Get();
                    if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"CookieParser returned cookie:{cookie}");
 
                    if (cookie == null)
                    {
                        if (parser.EndofHeader())
                        {
                            break;
                        }
                        continue;
                    }
 
                    // Parser marks invalid cookies this way
                    if (string.IsNullOrEmpty(cookie.Name))
                    {
                        if (isThrow)
                        {
                            throw new CookieException(SR.net_cookie_format);
                        }
                        // Otherwise, ignore (reject) cookie
                        continue;
                    }
 
                    // This will set the default values from the response URI
                    // AND will check for cookie validity
                    if (!cookie.VerifySetDefaults(variant, uri, isLocalDomain, m_fqdnMyDomain, true, isThrow))
                    {
                        continue;
                    }
                    // If many same cookies arrive we collapse them into just one, hence setting
                    // parameter isStrict = true below
                    cookies.InternalAdd(cookie, true);
                } while (true);
            }
            catch (OutOfMemoryException)
            {
                throw;
            }
            catch (Exception e)
            {
                if (isThrow)
                {
                    throw new CookieException(SR.Format(SR.net_cookie_parse_header, uri.AbsoluteUri), e);
                }
            }
 
            int cookiesCount = cookies.Count;
            for (int i = 0; i < cookiesCount; i++)
            {
                Add((Cookie)cookies[i], isThrow);
            }
 
            return cookies;
        }
 
        public CookieCollection GetCookies(Uri uri)
        {
            ArgumentNullException.ThrowIfNull(uri);
 
            return InternalGetCookies(uri) ?? new CookieCollection();
        }
 
        /// <summary>Gets a <see cref="CookieCollection"/> that contains all of the <see cref="Cookie"/> instances in the container.</summary>
        /// <returns>A <see cref="CookieCollection"/> that contains all of the <see cref="Cookie"/> instances in the container.</returns>
        public CookieCollection GetAllCookies()
        {
            var result = new CookieCollection();
 
            lock (m_domainTable.SyncRoot)
            {
                IDictionaryEnumerator lists = m_domainTable.GetEnumerator();
                while (lists.MoveNext())
                {
                    PathList list = (PathList)lists.Value!;
                    lock (list.SyncRoot)
                    {
                        IDictionaryEnumerator collections = list.List.GetEnumerator();
                        while (collections.MoveNext())
                        {
                            result.Add((CookieCollection)collections.Value!);
                        }
                    }
                }
            }
 
            return result;
        }
 
        internal CookieCollection? InternalGetCookies(Uri uri)
        {
            if (m_count == 0)
            {
                return null;
            }
 
            bool isSecure = (uri.Scheme == UriScheme.Https || uri.Scheme == UriScheme.Wss);
            int port = uri.Port;
            CookieCollection? cookies = null;
 
            var domainAttributeMatchAnyCookieVariant = new System.Collections.Generic.List<string>();
            System.Collections.Generic.List<string>? domainAttributeMatchOnlyCookieVariantPlain = null;
 
            string fqdnRemote = uri.Host;
 
            // Add initial candidates to match Domain attribute of possible cookies.
            // For these Domains, cookie can have any CookieVariant enum value.
            domainAttributeMatchAnyCookieVariant.Add(fqdnRemote);
            domainAttributeMatchAnyCookieVariant.Add("." + fqdnRemote);
 
            int dot = fqdnRemote.IndexOf('.');
            if (dot == -1)
            {
                // DNS.resolve may return short names even for other inet domains ;-(
                // We _don't_ know what the exact domain is, so try also grab short hostname cookies.
                // Grab long name from the local domain
                if (!string.IsNullOrEmpty(m_fqdnMyDomain))
                {
                    domainAttributeMatchAnyCookieVariant.Add(fqdnRemote + m_fqdnMyDomain);
                    // Grab the local domain itself
                    domainAttributeMatchAnyCookieVariant.Add(m_fqdnMyDomain);
                }
            }
            else
            {
                // Grab the host domain
                domainAttributeMatchAnyCookieVariant.Add(fqdnRemote.Substring(dot));
 
                // The following block is only for compatibility with Version0 spec.
                // Still, we'll add only Plain-Variant cookies if found under below keys
                if (fqdnRemote.Length > 2)
                {
                    // We ignore the '.' at the end on the name
                    int last = fqdnRemote.LastIndexOf('.', fqdnRemote.Length - 2);
                    // AND keys with <2 dots inside.
                    if (last > 0)
                    {
                        last = fqdnRemote.LastIndexOf('.', last - 1);
                    }
                    if (last != -1)
                    {
                        while ((dot < last) && (dot = fqdnRemote.IndexOf('.', dot + 1)) != -1)
                        {
                            // These candidates can only match CookieVariant.Plain cookies.
                            domainAttributeMatchOnlyCookieVariantPlain ??= new System.Collections.Generic.List<string>();
                            domainAttributeMatchOnlyCookieVariantPlain.Add(fqdnRemote.Substring(dot));
                        }
                    }
                }
            }
 
            BuildCookieCollectionFromDomainMatches(uri, isSecure, port, ref cookies, domainAttributeMatchAnyCookieVariant, false);
            if (domainAttributeMatchOnlyCookieVariantPlain != null)
            {
                BuildCookieCollectionFromDomainMatches(uri, isSecure, port, ref cookies, domainAttributeMatchOnlyCookieVariantPlain, true);
            }
 
            return cookies;
        }
 
        private void BuildCookieCollectionFromDomainMatches(Uri uri, bool isSecure, int port, ref CookieCollection? cookies, System.Collections.Generic.List<string> domainAttribute, bool matchOnlyPlainCookie)
        {
            for (int i = 0; i < domainAttribute.Count; i++)
            {
                PathList pathList;
                lock (m_domainTable.SyncRoot)
                {
                    pathList = (PathList)m_domainTable[domainAttribute[i]]!;
                    if (pathList == null)
                    {
                        continue;
                    }
                }
 
                lock (pathList.SyncRoot)
                {
                    SortedList list = pathList.List;
                    int listCount = list.Count;
                    for (int e = 0; e < listCount; e++)
                    {
                        string path = (string)list.GetKey(e);
                        if (PathMatch(uri.AbsolutePath, path))
                        {
                            CookieCollection cc = (CookieCollection)list.GetByIndex(e)!;
                            cc.TimeStamp(CookieCollection.Stamp.Set);
                            MergeUpdateCollections(ref cookies, cc, port, isSecure, matchOnlyPlainCookie);
                        }
                    }
                }
 
                // Remove unused domain.
                if (pathList.Count == 0)
                {
                    lock (m_domainTable.SyncRoot)
                    {
                        m_domainTable.Remove(domainAttribute[i]);
                    }
                }
            }
        }
 
        // Implement path-matching according to https://tools.ietf.org/html/rfc6265#section-5.1.4:
        // | A request-path path-matches a given cookie-path if at least one of the following conditions holds:
        // | - The cookie-path and the request-path are identical.
        // | - The cookie-path is a prefix of the request-path, and the last character of the cookie-path is %x2F ("/").
        // | - The cookie-path is a prefix of the request-path, and the first character of the request-path that is not included in the cookie-path is a %x2F ("/") character.
        // The latter conditions are needed to make sure that
        // PathMatch("/fooBar, "/foo") == false
        // but:
        // PathMatch("/foo/bar", "/foo") == true, PathMatch("/foo/bar", "/foo/") == true
        private static bool PathMatch(string requestPath, string cookiePath)
        {
            cookiePath = CookieParser.CheckQuoted(cookiePath);
 
            if (!requestPath.StartsWith(cookiePath, StringComparison.Ordinal))
                return false;
            return requestPath.Length == cookiePath.Length ||
                   cookiePath.EndsWith('/') ||
                   requestPath[cookiePath.Length] == '/';
        }
 
        private void MergeUpdateCollections(ref CookieCollection? destination, CookieCollection source, int port, bool isSecure, bool isPlainOnly)
        {
            lock (source)
            {
                // Cannot use foreach as we are going to update 'source'
                for (int idx = 0; idx < source.Count; ++idx)
                {
                    bool to_add = false;
 
                    Cookie cookie = source[idx];
 
                    if (cookie.Expired)
                    {
                        // If expired, remove from container and don't add to the destination
                        source.RemoveAt(idx);
                        --m_count;
                        --idx;
                    }
                    else
                    {
                        // Add only if port does match to this request URI
                        // or was not present in the original response.
                        if (isPlainOnly && cookie.Variant != CookieVariant.Plain)
                        {
                            ; // Don't add
                        }
                        else if (cookie.PortList != null)
                        {
                            foreach (int p in cookie.PortList)
                            {
                                if (p == port)
                                {
                                    to_add = true;
                                    break;
                                }
                            }
                        }
                        else
                        {
                            // It was implicit Port, always OK to add.
                            to_add = true;
                        }
 
                        // Refuse to add a secure cookie into an 'unsecure' destination
                        if (cookie.Secure && !isSecure)
                        {
                            to_add = false;
                        }
 
                        if (to_add)
                        {
                            // In 'source' are already ordered.
                            // If two same cookies come from different 'source' then they
                            // will follow (not replace) each other.
                            destination ??= new CookieCollection();
                            destination.InternalAdd(cookie, false);
                        }
                    }
                }
            }
        }
 
        public string GetCookieHeader(Uri uri)
        {
            ArgumentNullException.ThrowIfNull(uri);
 
            return GetCookieHeader(uri, out _);
        }
 
        internal string GetCookieHeader(Uri uri, out string optCookie2)
        {
            CookieCollection? cookies = InternalGetCookies(uri);
            if (cookies == null)
            {
                optCookie2 = string.Empty;
                return string.Empty;
            }
 
            string delimiter = string.Empty;
 
            StringBuilder builder = StringBuilderCache.Acquire();
            for (int i = 0; i < cookies.Count; i++)
            {
                builder.Append(delimiter);
                cookies[i].ToString(builder);
 
                delimiter = "; ";
            }
 
            optCookie2 = cookies.IsOtherVersionSeen ?
                          (Cookie.SpecialAttributeLiteral +
                           CookieFields.VersionAttributeName +
                           Cookie.EqualsLiteral +
                           Cookie.MaxSupportedVersionString) : string.Empty;
 
            return StringBuilderCache.GetStringAndRelease(builder);
        }
 
        public void SetCookies(Uri uri, string cookieHeader)
        {
            ArgumentNullException.ThrowIfNull(uri);
            ArgumentNullException.ThrowIfNull(cookieHeader);
 
            CookieCutter(uri, null, cookieHeader, true); // Will throw on error
        }
    }
 
    // PathList needs to be public in order to maintain binary serialization compatibility as the System shim
    // needs to have access to type-forward it.
    [Serializable]
    [System.Runtime.CompilerServices.TypeForwardedFrom("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
    public sealed class PathList
    {
        // Usage of PathList depends on it being shallowly immutable;
        // adding any mutable fields to it would result in breaks.
        private readonly SortedList m_list = SortedList.Synchronized(new SortedList(PathListComparer.StaticInstance)); // Do not rename (binary serialization)
 
        internal int Count => m_list.Count;
 
        internal int GetCookiesCount()
        {
            int count = 0;
            lock (SyncRoot)
            {
                IList list = m_list.GetValueList();
                int listCount = list.Count;
                for (int i = 0; i < listCount; i++)
                {
                    count += ((CookieCollection)list[i]!).Count;
                }
            }
            return count;
        }
 
        internal ICollection Values
        {
            get
            {
                return m_list.Values;
            }
        }
 
        internal object? this[string s]
        {
            get
            {
                lock (SyncRoot)
                {
                    return m_list[s];
                }
            }
            set
            {
                lock (SyncRoot)
                {
                    Debug.Assert(value != null);
                    m_list[s] = value;
                }
            }
        }
 
        internal IDictionaryEnumerator GetEnumerator()
        {
            lock (SyncRoot)
            {
                return m_list.GetEnumerator();
            }
        }
 
        internal void Remove(object key)
        {
            lock (SyncRoot)
            {
                m_list.Remove(key);
            }
        }
 
        internal SortedList List => m_list;
 
        internal object SyncRoot => m_list.SyncRoot;
 
        [Serializable]
        [System.Runtime.CompilerServices.TypeForwardedFrom("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
        private sealed class PathListComparer : IComparer
        {
            internal static readonly PathListComparer StaticInstance = new PathListComparer();
 
            int IComparer.Compare(object? ol, object? or)
            {
                string pathLeft = CookieParser.CheckQuoted((string)ol!);
                string pathRight = CookieParser.CheckQuoted((string)or!);
                int ll = pathLeft.Length;
                int lr = pathRight.Length;
                int length = Math.Min(ll, lr);
 
                for (int i = 0; i < length; ++i)
                {
                    if (pathLeft[i] != pathRight[i])
                    {
                        return pathLeft[i] - pathRight[i];
                    }
                }
                return lr - ll;
            }
        }
    }
}