File: System\Net\Cookie.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.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
 
namespace System.Net
{
    [System.Runtime.CompilerServices.TypeForwardedFrom("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
    public enum CookieVariant
    {
        Unknown,
        Plain,
        Rfc2109,
        Rfc2965,
        Default = Rfc2109
    }
 
    // Cookie class
    //
    // Adheres to RFC 2965
    //
    // Currently, only represents client-side cookies. The cookie classes know
    // how to parse a set-cookie format string, but not a cookie format string
    // (e.g. "Cookie: $Version=1; name=value; $Path=/foo; $Secure")
    [Serializable]
    [System.Runtime.CompilerServices.TypeForwardedFrom("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
    public sealed class Cookie
    {
        // NOTE: these two constants must change together.
        internal const int MaxSupportedVersion = 1;
        internal const string MaxSupportedVersionString = "1";
 
        internal const string SeparatorLiteral = "; ";
        internal const char EqualsLiteral = '=';
        internal const string QuotesLiteral = "\"";
        internal const string SpecialAttributeLiteral = "$";
 
        internal static readonly char[] PortSplitDelimiters = new char[] { ' ', ',', '\"' };
        // Space (' ') should be reserved as well per RFCs, but major web browsers support it and some web sites use it - so we support it too
        private static readonly SearchValues<char> s_reservedToNameChars = SearchValues.Create("\t\r\n=;,");
 
        private static readonly SearchValues<char> s_domainChars =
            SearchValues.Create("-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz");
 
        private string m_comment = string.Empty; // Do not rename (binary serialization)
        private Uri? m_commentUri; // Do not rename (binary serialization)
        private CookieVariant m_cookieVariant = CookieVariant.Plain; // Do not rename (binary serialization)
        private bool m_discard; // Do not rename (binary serialization)
        private string m_domain = string.Empty; // Do not rename (binary serialization)
        private bool m_domain_implicit = true; // Do not rename (binary serialization)
        private DateTime m_expires = DateTime.MinValue; // Do not rename (binary serialization)
        private string m_name = string.Empty; // Do not rename (binary serialization)
        private string m_path = string.Empty; // Do not rename (binary serialization)
        private bool m_path_implicit = true; // Do not rename (binary serialization)
        private string m_port = string.Empty; // Do not rename (binary serialization)
        private bool m_port_implicit = true; // Do not rename (binary serialization)
        private int[]? m_port_list; // Do not rename (binary serialization)
        private bool m_secure; // Do not rename (binary serialization)
        [System.Runtime.Serialization.OptionalField]
        private bool m_httpOnly = false; // Do not rename (binary serialization)
        private DateTime m_timeStamp = DateTime.UtcNow; // Do not rename (binary serialization)
        private string m_value = string.Empty; // Do not rename (binary serialization)
        private int m_version; // Do not rename (binary serialization)
 
        private string m_domainKey = string.Empty; // Do not rename (binary serialization)
 
#pragma warning disable 0649 // set via reflection by CookieParser: https://github.com/dotnet/runtime/issues/19348
        internal bool IsQuotedVersion; // Do not rename (binary serialization)
        internal bool IsQuotedDomain; // Do not rename (binary serialization)
#pragma warning restore 0649
 
#if DEBUG
        static Cookie()
        {
            Debug.Assert(MaxSupportedVersion.ToString(NumberFormatInfo.InvariantInfo).Equals(MaxSupportedVersionString, StringComparison.Ordinal));
        }
#endif
 
        // These DynamicDependency attributes are a workaround for https://github.com/dotnet/runtime/issues/19348.
        // HttpListener uses the non-public ToServerString, which isn't used by anything else in this assembly,
        // and which accesses other internals and can't be moved to HttpListener (at least not without incurring
        // functional differences).  However, once we do our initial System.Net.Primitives build and ToServerString
        // survives to it, we no longer want the DynamicDependencyAttribute to remain around, so that ToServerString
        // can be trimmed out if the relevant functionality from HttpListener isn't used when performing whole-app
        // analysis.  As such, when trimming System.Net.Primitives, we build the assembly with ILLinkKeepDepAttributes=false,
        // such that when this assembly is compiled, ToServerString will remain but the DynamicDependency attributes
        // will be removed.  This hack will need to be revisited if anything else in the assembly starts using
        // DynamicDependencyAttribute.
        // https://github.com/mono/linker/issues/802
 
        [DynamicDependency("ToServerString")]
        public Cookie()
        {
        }
 
        [DynamicDependency("ToServerString")] // Workaround for https://github.com/dotnet/runtime/issues/19348
        public Cookie(string name, string? value)
        {
            Name = name;
            Value = value;
        }
 
        public Cookie(string name, string? value, string? path)
            : this(name, value)
        {
            Path = path;
        }
 
        public Cookie(string name, string? value, string? path, string? domain)
            : this(name, value, path)
        {
            Domain = domain;
        }
 
        [AllowNull]
        public string Comment
        {
            get
            {
                return m_comment;
            }
            set
            {
                m_comment = value ?? string.Empty;
            }
        }
 
        public Uri? CommentUri
        {
            get
            {
                return m_commentUri;
            }
            set
            {
                m_commentUri = value;
            }
        }
 
 
        public bool HttpOnly
        {
            get
            {
                return m_httpOnly;
            }
            set
            {
                m_httpOnly = value;
            }
        }
 
 
        public bool Discard
        {
            get
            {
                return m_discard;
            }
            set
            {
                m_discard = value;
            }
        }
 
        [AllowNull]
        public string Domain
        {
            get
            {
                return m_domain;
            }
            set
            {
                m_domain = value ?? string.Empty;
                m_domain_implicit = false;
                m_domainKey = string.Empty; // _domainKey will be set when adding this cookie to a container.
            }
        }
 
        internal bool DomainImplicit
        {
            get
            {
                return m_domain_implicit;
            }
            set
            {
                m_domain_implicit = value;
            }
        }
 
        public bool Expired
        {
            get
            {
                return (m_expires != DateTime.MinValue) && (m_expires.ToUniversalTime() <= DateTime.UtcNow);
            }
            set
            {
                if (value)
                {
                    m_expires = DateTime.UtcNow;
                }
            }
        }
 
        public DateTime Expires
        {
            get
            {
                return m_expires;
            }
            set
            {
                m_expires = value;
            }
        }
 
        public string Name
        {
            get
            {
                return m_name;
            }
            set
            {
                if (string.IsNullOrEmpty(value) || !InternalSetName(value))
                {
                    throw new CookieException(SR.Format(SR.net_cookie_attribute, "Name", value ?? "<null>"));
                }
            }
        }
        internal bool InternalSetName(string? value)
        {
            if (string.IsNullOrEmpty(value)
                || value.StartsWith('$')
                || value.StartsWith(' ')
                || value.EndsWith(' ')
                || value.AsSpan().ContainsAny(s_reservedToNameChars))
            {
                m_name = string.Empty;
                return false;
            }
            m_name = value;
            return true;
        }
 
        [AllowNull]
        public string Path
        {
            get
            {
                return m_path;
            }
            set
            {
                m_path = value ?? string.Empty;
                m_path_implicit = false;
            }
        }
 
        internal bool Plain
        {
            get
            {
                return Variant == CookieVariant.Plain;
            }
        }
 
        internal Cookie Clone()
        {
            Cookie clonedCookie = new Cookie(m_name, m_value);
 
            // Copy over all the properties from the original cookie
            if (!m_port_implicit)
            {
                clonedCookie.Port = m_port;
            }
            if (!m_path_implicit)
            {
                clonedCookie.Path = m_path;
            }
            clonedCookie.Domain = m_domain;
 
            // If the domain in the original cookie was implicit, we should preserve that property
            clonedCookie.DomainImplicit = m_domain_implicit;
            clonedCookie.m_timeStamp = m_timeStamp;
            clonedCookie.Comment = m_comment;
            clonedCookie.CommentUri = m_commentUri;
            clonedCookie.HttpOnly = m_httpOnly;
            clonedCookie.Discard = m_discard;
            clonedCookie.Expires = m_expires;
            clonedCookie.Version = m_version;
            clonedCookie.Secure = m_secure;
 
            // The variant is set when we set properties like port/version. So,
            // we should copy over the variant from the original cookie after
            // we set all other properties
            clonedCookie.m_cookieVariant = m_cookieVariant;
 
            return clonedCookie;
        }
 
        private static bool IsDomainEqualToHost(string domain, string host)
        {
            // +1 in the host length is to account for the leading dot in domain
            return (host.Length + 1 == domain.Length) &&
                   (string.Compare(host, 0, domain, 1, host.Length, StringComparison.OrdinalIgnoreCase) == 0);
        }
 
        // According to spec we must assume default values for attributes but still
        // keep in mind that we must not include them into the requests.
        // We also check the validity of all attributes based on the version and variant (read RFC)
        //
        // To work properly this function must be called after cookie construction with
        // default (response) URI AND setDefault == true
        //
        // Afterwards, the function can be called many times with other URIs and
        // setDefault == false to check whether this cookie matches given uri
        internal bool VerifySetDefaults(CookieVariant variant, Uri uri, bool isLocalDomain, string localDomain, bool setDefault, bool shouldThrow)
        {
            string host = uri.Host;
            int port = uri.Port;
            string path = uri.AbsolutePath;
            bool valid = true;
 
            if (setDefault)
            {
                // Set Variant. If version is zero => reset cookie to Version0 style
                if (Version == 0)
                {
                    variant = CookieVariant.Plain;
                }
                else if (Version == 1 && variant == CookieVariant.Unknown)
                {
                    // Since we don't expose Variant to an app, set it to Default
                    variant = CookieVariant.Default;
                }
                m_cookieVariant = variant;
            }
 
            // Check the name
            if (string.IsNullOrEmpty(m_name) ||
                m_name.StartsWith('$') ||
                m_name.StartsWith(' ') ||
                m_name.EndsWith(' ') ||
                m_name.AsSpan().ContainsAny(s_reservedToNameChars))
            {
                if (shouldThrow)
                {
                    throw new CookieException(SR.Format(SR.net_cookie_attribute, "Name", m_name ?? "<null>"));
                }
                return false;
            }
 
            // Check the value
            if (m_value == null ||
                (!(m_value.Length > 2 && m_value.StartsWith('\"') && m_value.EndsWith('\"')) && m_value.AsSpan().ContainsAny(';', ',')))
            {
                if (shouldThrow)
                {
                    throw new CookieException(SR.Format(SR.net_cookie_attribute, "Value", m_value ?? "<null>"));
                }
                return false;
            }
 
            // Check Comment syntax
            if (Comment != null && !(Comment.Length > 2 && Comment.StartsWith('\"') && Comment.EndsWith('\"'))
                && (Comment.AsSpan().ContainsAny(';', ',')))
            {
                if (shouldThrow)
                {
                    throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.CommentAttributeName, Comment));
                }
                return false;
            }
 
            // Check Path syntax
            if (Path != null && !(Path.Length > 2 && Path.StartsWith('\"') && Path.EndsWith('\"'))
                && (Path.AsSpan().ContainsAny(';', ',')))
            {
                if (shouldThrow)
                {
                    throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PathAttributeName, Path));
                }
                return false;
            }
 
            // Check/set domain
            //
            // If domain is implicit => assume a) uri is valid, b) just set domain to uri hostname.
            if (setDefault && m_domain_implicit)
            {
                m_domain = host;
            }
            else
            {
                if (!m_domain_implicit)
                {
                    // Forwarding note: If Uri.Host is of IP address form then the only supported case
                    // is for IMPLICIT domain property of a cookie.
                    // The code below (explicit cookie.Domain value) will try to parse Uri.Host IP string
                    // as a fqdn and reject the cookie.
 
                    // Aliasing since we might need the KeyValue (but not the original one).
                    string domain = m_domain;
 
                    // Syntax check for Domain charset plus empty string.
                    if (!DomainCharsTest(domain))
                    {
                        if (shouldThrow)
                        {
                            throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.DomainAttributeName, domain ?? "<null>"));
                        }
                        return false;
                    }
 
                    // Domain must start with '.' if set explicitly.
                    if (domain[0] != '.')
                    {
                        domain = '.' + domain;
                    }
 
                    int host_dot = host.IndexOf('.');
 
                    // First quick check is for pushing a cookie into the local domain.
                    if (isLocalDomain && string.Equals(localDomain, domain, StringComparison.OrdinalIgnoreCase))
                    {
                        valid = true;
                    }
                    else if (domain.IndexOf('.', 1, domain.Length - 2) == -1)
                    {
                        // A single label domain is valid only if the domain is exactly the same as the host specified in the URI.
                        if (!IsDomainEqualToHost(domain, host))
                        {
                            valid = false;
                        }
                    }
                    else if (variant == CookieVariant.Plain)
                    {
                        // We distinguish between Version0 cookie and other versions on domain issue.
                        // According to Version0 spec a domain must be just a substring of the hostname.
 
                        if (!IsDomainEqualToHost(domain, host))
                        {
                            if (host.Length <= domain.Length ||
                                (string.Compare(host, host.Length - domain.Length, domain, 0, domain.Length, StringComparison.OrdinalIgnoreCase) != 0))
                            {
                                valid = false;
                            }
                        }
                    }
                    else if (host_dot == -1 ||
                             domain.Length != host.Length - host_dot ||
                             (string.Compare(host, host_dot, domain, 0, domain.Length, StringComparison.OrdinalIgnoreCase) != 0))
                    {
                        // Starting from the first dot, the host must match the domain.
                        //
                        // For null hosts, the host must match the domain exactly.
                        if (!IsDomainEqualToHost(domain, host))
                        {
                            valid = false;
                        }
                    }
 
                    if (valid)
                    {
                        m_domainKey = domain.ToLowerInvariant();
                    }
                }
                else
                {
                    // For implicitly set domain AND at the set_default == false time
                    // we simply need to match uri.Host against m_domain.
                    if (!string.Equals(host, m_domain, StringComparison.OrdinalIgnoreCase))
                    {
                        valid = false;
                    }
                }
                if (!valid)
                {
                    if (shouldThrow)
                    {
                        throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.DomainAttributeName, m_domain));
                    }
                    return false;
                }
            }
 
            // Check/Set Path
            if (setDefault && m_path_implicit)
            {
                // This code assumes that the URI path is always valid and contains at least one '/'.
                switch (m_cookieVariant)
                {
                    case CookieVariant.Plain:
                        // As per RFC6265 5.1.4. (https://tools.ietf.org/html/rfc6265#section-5.1.4):
                        // | 2. If the uri-path is empty or if the first character of the uri-
                        // |    path is not a %x2F ("/") character, output %x2F ("/") and skip
                        // |    the remaining steps.
                        // | 3. If the uri-path contains no more than one %x2F ("/") character,
                        // |    output %x2F ("/") and skip the remaining step.
                        // Note: Normally Uri.AbsolutePath contains at least one "/" after parsing,
                        //       but it's possible construct Uri with an empty path using a custom UriParser
                        int lastSlash;
                        if (!path.StartsWith('/') || (lastSlash = path.LastIndexOf('/')) == 0)
                        {
                            m_path = "/";
                            break;
                        }
 
                        // | 4. Output the characters of the uri-path from the first character up
                        // |    to, but not including, the right-most %x2F ("/").
                        m_path = path.Substring(0, lastSlash);
                        break;
                    case CookieVariant.Rfc2109:
                        m_path = path.Substring(0, path.LastIndexOf('/')); // May be empty
                        break;
 
                    case CookieVariant.Rfc2965:
                    default:
                        // NOTE: this code is not resilient against future versions with different 'Path' semantics.
                        m_path = path.Substring(0, path.LastIndexOf('/') + 1);
                        break;
                }
            }
 
            // Set the default port if Port attribute was present but had no value.
            if (setDefault && (m_port_implicit == false && m_port.Length == 0))
            {
                m_port_list = new int[1] { port };
            }
 
            if (m_port_implicit == false)
            {
                // Port must match against the one from the uri.
                valid = false;
                foreach (int p in m_port_list!)
                {
                    if (p == port)
                    {
                        valid = true;
                        break;
                    }
                }
                if (!valid)
                {
                    if (shouldThrow)
                    {
                        throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, m_port));
                    }
                    return false;
                }
            }
            return true;
        }
 
        // Very primitive test to make sure that the name does not have illegal characters
        // as per RFC 952 (relaxed on first char could be a digit and string can have '_').
        private static bool DomainCharsTest(string name) =>
            !string.IsNullOrEmpty(name) &&
            !name.AsSpan().ContainsAnyExcept(s_domainChars);
 
        [AllowNull]
        public string Port
        {
            get
            {
                return m_port;
            }
            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    // "Port" is present but has no value.
                    // Therefore; the effective port value is implicit.
                    m_port_implicit = true;
                    m_port = string.Empty;
                }
                else
                {
                    // "Port" value is present, so we use the provided value rather than an implicit one.
                    m_port_implicit = false;
                    // Parse port list
                    if (!value.StartsWith('\"') || !value.EndsWith('\"'))
                    {
                        throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value));
                    }
                    string[] ports = value.Split(PortSplitDelimiters, StringSplitOptions.RemoveEmptyEntries);
                    int[] parsedPorts = new int[ports.Length];
 
                    for (int i = 0; i < ports.Length; ++i)
                    {
                        if (!int.TryParse(ports[i], out int port))
                        {
                            throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value));
                        }
 
                        // valid values for port 0 - 0xFFFF
                        if ((port < 0) || (port > 0xFFFF))
                        {
                            throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value));
                        }
 
                        parsedPorts[i] = port;
                    }
                    m_port_list = parsedPorts;
                    m_port = value;
                    m_version = MaxSupportedVersion;
                    m_cookieVariant = CookieVariant.Rfc2965;
                }
            }
        }
 
 
        internal int[]? PortList
        {
            get
            {
                // PortList will be null if Port Attribute was omitted in the response.
                return m_port_list;
            }
        }
 
        public bool Secure
        {
            get
            {
                return m_secure;
            }
            set
            {
                m_secure = value;
            }
        }
 
        public DateTime TimeStamp
        {
            get
            {
                return m_timeStamp;
            }
        }
 
        [AllowNull]
        public string Value
        {
            get
            {
                return m_value;
            }
            set
            {
                m_value = value ?? string.Empty;
            }
        }
 
        internal CookieVariant Variant
        {
            get
            {
                return m_cookieVariant;
            }
        }
 
        // _domainKey member is set internally in VerifySetDefaults().
        // If it is not set then verification function was not called;
        // this should never happen.
        internal string DomainKey
        {
            get
            {
                return m_domain_implicit ? Domain : m_domainKey;
            }
        }
 
        public int Version
        {
            get
            {
                return m_version;
            }
            set
            {
                ArgumentOutOfRangeException.ThrowIfNegative(value);
                m_version = value;
                if (value > 0 && m_cookieVariant < CookieVariant.Rfc2109)
                {
                    m_cookieVariant = CookieVariant.Rfc2109;
                }
            }
        }
 
        public override bool Equals([NotNullWhen(true)] object? comparand)
        {
            return comparand is Cookie other
                    && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase)
                    && string.Equals(Value, other.Value, StringComparison.Ordinal)
                    && string.Equals(Path, other.Path, StringComparison.Ordinal)
                    && CookieComparer.EqualDomains(Domain, other.Domain)
                    && (Version == other.Version);
        }
 
        public override int GetHashCode()
        {
            return HashCode.Combine(
                StringComparer.OrdinalIgnoreCase.GetHashCode(Name),
                StringComparer.Ordinal.GetHashCode(Value),
                StringComparer.Ordinal.GetHashCode(Path),
                StringComparer.OrdinalIgnoreCase.GetHashCode(Domain),
                Version);
        }
 
        public override string ToString()
        {
            StringBuilder sb = StringBuilderCache.Acquire();
            ToString(sb);
            return StringBuilderCache.GetStringAndRelease(sb);
        }
 
        internal void ToString(StringBuilder sb)
        {
            int beforeLength = sb.Length;
 
            // Add the Cookie version if necessary.
            if (Version != 0)
            {
                sb.Append(SpecialAttributeLiteral + CookieFields.VersionAttributeName + EqualsLiteral); // const strings
                if (IsQuotedVersion) sb.Append('"');
                sb.Append(NumberFormatInfo.InvariantInfo, $"{m_version}");
                if (IsQuotedVersion) sb.Append('"');
                sb.Append(SeparatorLiteral);
            }
 
            // Add the Cookie Name=Value pair.
            sb.Append(Name).Append(EqualsLiteral).Append(Value);
 
            if (!Plain)
            {
                // Add the Path if necessary.
                if (!m_path_implicit && m_path.Length > 0)
                {
                    sb.Append(SeparatorLiteral + SpecialAttributeLiteral + CookieFields.PathAttributeName + EqualsLiteral); // const strings
                    sb.Append(m_path);
                }
 
                // Add the Domain if necessary.
                if (!m_domain_implicit && m_domain.Length > 0)
                {
                    sb.Append(SeparatorLiteral + SpecialAttributeLiteral + CookieFields.DomainAttributeName + EqualsLiteral); // const strings
                    if (IsQuotedDomain) sb.Append('"');
                    sb.Append(m_domain);
                    if (IsQuotedDomain) sb.Append('"');
                }
            }
 
            // Add the Port if necessary.
            if (!m_port_implicit)
            {
                sb.Append(SeparatorLiteral + SpecialAttributeLiteral + CookieFields.PortAttributeName); // const strings
                if (m_port.Length > 0)
                {
                    sb.Append(EqualsLiteral);
                    sb.Append(m_port);
                }
            }
 
            // Check to see whether the only thing we added was "=", and if so,
            // remove it so that we leave the StringBuilder unchanged in contents.
            int afterLength = sb.Length;
            if (afterLength == (1 + beforeLength) && sb[beforeLength] == '=')
            {
                sb.Length = beforeLength;
            }
        }
 
        internal string? ToServerString()
        {
            string result = Name + EqualsLiteral + Value;
            if (m_comment != null && m_comment.Length > 0)
            {
                result += SeparatorLiteral + CookieFields.CommentAttributeName + EqualsLiteral + m_comment;
            }
            if (m_commentUri != null)
            {
                result += SeparatorLiteral + CookieFields.CommentUrlAttributeName + EqualsLiteral + QuotesLiteral + m_commentUri.ToString() + QuotesLiteral;
            }
            if (m_discard)
            {
                result += SeparatorLiteral + CookieFields.DiscardAttributeName;
            }
            if (!m_domain_implicit && m_domain != null && m_domain.Length > 0)
            {
                result += SeparatorLiteral + CookieFields.DomainAttributeName + EqualsLiteral + m_domain;
            }
            if (Expires != DateTime.MinValue)
            {
                int seconds = (int)(Expires.ToUniversalTime() - DateTime.UtcNow).TotalSeconds;
                if (seconds < 0)
                {
                    // This means that the cookie has already expired. Set Max-Age to 0
                    // so that the client will discard the cookie immediately.
                    seconds = 0;
                }
                result += SeparatorLiteral + CookieFields.MaxAgeAttributeName + EqualsLiteral + seconds.ToString(NumberFormatInfo.InvariantInfo);
            }
            if (!m_path_implicit && m_path != null && m_path.Length > 0)
            {
                result += SeparatorLiteral + CookieFields.PathAttributeName + EqualsLiteral + m_path;
            }
            if (!Plain && !m_port_implicit && m_port != null && m_port.Length > 0)
            {
                // QuotesLiteral are included in _port.
                result += SeparatorLiteral + CookieFields.PortAttributeName + EqualsLiteral + m_port;
            }
            if (m_version > 0)
            {
                result += SeparatorLiteral + CookieFields.VersionAttributeName + EqualsLiteral + m_version.ToString(NumberFormatInfo.InvariantInfo);
            }
            return result == "=" ? null : result;
        }
    }
}