File: net\System\Net\cookie.cs
Project: ndp\fx\src\System.csproj (System)
//------------------------------------------------------------------------------
// <copyright file="cookie.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
 
namespace System.Net {
    using System.Collections;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Threading;
 
    internal enum CookieVariant {
        Unknown,
        Plain,
        Rfc2109,
        Rfc2965,
        Default = Rfc2109
    }
 
    //
    // Cookie class
    //
    //  Adheres to RFC 2965
    //
    //  Currently, only 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")
    //
 
    /// <devdoc>
    ///    <para>[To be supplied.]</para>
    /// </devdoc>
    [Serializable]
    public sealed class Cookie {
 
        internal const int    MaxSupportedVersion = 1;
        internal const string CommentAttributeName = "Comment";
        internal const string CommentUrlAttributeName = "CommentURL";
        internal const string DiscardAttributeName = "Discard";
        internal const string DomainAttributeName = "Domain";
        internal const string ExpiresAttributeName = "Expires";
        internal const string MaxAgeAttributeName = "Max-Age";
        internal const string PathAttributeName = "Path";
        internal const string PortAttributeName = "Port";
        internal const string SecureAttributeName = "Secure";
        internal const string VersionAttributeName = "Version";
        internal const string HttpOnlyAttributeName = "HttpOnly";
 
        internal const string SeparatorLiteral = "; ";
        internal const string EqualsLiteral = "=";
        internal const string QuotesLiteral = "\"";
        internal const string SpecialAttributeLiteral = "$";
 
        internal static readonly char[] PortSplitDelimiters =  new char[] {' ', ',', '\"'};
        internal static readonly char[] Reserved2Name =  new char[] {' ', '\t', '\r', '\n', '=', ';', ',' };
        internal static readonly char[] Reserved2Value =  new char[] {';', ',' };
        private  static Comparer staticComparer = new Comparer();
 
    // fields
 
        string  m_comment = string.Empty;
        Uri     m_commentUri = null;
        CookieVariant m_cookieVariant = CookieVariant.Plain;
        bool    m_discard = false;
        string  m_domain = string.Empty;
        bool    m_domain_implicit = true;
        DateTime m_expires = DateTime.MinValue;
        string  m_name = string.Empty;
        string  m_path = string.Empty;
        bool    m_path_implicit = true;
        string  m_port = string.Empty;
        bool    m_port_implicit = true;
        int[]   m_port_list = null;
        bool    m_secure = false;
        [System.Runtime.Serialization.OptionalField]
        bool    m_httpOnly=false;
        DateTime m_timeStamp = DateTime.Now;
        string  m_value = string.Empty;
        int     m_version = 0;
 
        string  m_domainKey = string.Empty;
        internal bool IsQuotedVersion = false;
        internal bool IsQuotedDomain = false;
 
 
    // constructors
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public Cookie() {
        }
 
 
        //public Cookie(string cookie) {
        //    if ((cookie == null) || (cookie == String.Empty)) {
        //        throw new ArgumentException("cookie");
        //    }
        //    Parse(cookie.Trim());
        //    Validate();
        //}
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public Cookie(string name, string value) {
            Name = name;
            m_value = value;
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public Cookie(string name, string value, string path)
            : this(name, value) {
            Path = path;
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public Cookie(string name, string value, string path, string domain)
            : this(name, value, path) {
            Domain = domain;
        }
 
    // properties
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public string Comment {
            get {
                return m_comment;
            }
            set {
                if (value == null) {
                    value = string.Empty;
                }
                m_comment = value;
            }
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public Uri CommentUri {
            get {
                return m_commentUri;
            }
            set {
                m_commentUri = value;
            }
        }
 
 
        public bool HttpOnly{
            get{
                return m_httpOnly;
            }
            set{
                m_httpOnly = value;
            }
        }
 
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public bool Discard {
            get {
                return m_discard;
            }
            set {
                m_discard = value;
            }
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public string Domain {
            get {
                return m_domain;
            }
            set {
                m_domain = (value == null? String.Empty : value);
                m_domain_implicit = false;
                m_domainKey = string.Empty; //this will get it value when adding into the Container.
            }
        }
 
        private string _Domain {
            get {
                return (Plain || m_domain_implicit || (m_domain.Length == 0))
                    ? string.Empty
                    : (SpecialAttributeLiteral
                       + DomainAttributeName
                       + EqualsLiteral + (IsQuotedDomain? "\"": string.Empty)
                       + m_domain + (IsQuotedDomain? "\"": string.Empty)
                       );
            }
        }
 
        internal bool DomainImplicit {
            get {
                return m_domain_implicit;
            }
            set {
                m_domain_implicit = value;
            }
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public bool Expired {
            get {
                return (m_expires != DateTime.MinValue) && (m_expires.ToLocalTime() <= DateTime.Now);
            }
            set {
                if (value == true) {
                    m_expires = DateTime.Now;
                }
            }
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public DateTime Expires {
            get {
                return m_expires;
            }
            set {
                m_expires = value;
            }
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public string Name {
            get {
                return m_name;
            }
            set {
                if (ValidationHelper.IsBlankString(value) || !InternalSetName(value)) {
                    throw new CookieException(SR.GetString(SR.net_cookie_attribute, "Name", value == null? "<null>": value));
                }
            }
        }
 
        internal bool InternalSetName(string value) {
            if (ValidationHelper.IsBlankString(value) || value[0] == '$' || value.IndexOfAny(Reserved2Name) != -1) {
                m_name = string.Empty;
                return false;
            }
            m_name = value;
            return true;
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public string Path {
            get {
                return m_path;
            }
            set {
                m_path = (value == null? String.Empty : value);
                m_path_implicit = false;
            }
        }
 
        private string _Path {
            get {
                return (Plain || m_path_implicit || (m_path.Length == 0))
                    ? string.Empty
                    : (SpecialAttributeLiteral
                       + PathAttributeName
                       + EqualsLiteral
                       + m_path
                       );
            }
        }
 
        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
            if ((host.Length + 1 == domain.Length) && 
                (string.Compare(host, 0, domain, 1, host.Length, StringComparison.OrdinalIgnoreCase) == 0)) {
                return true;
            }
            else {
                return false;
            }
        }
 
        //
        // 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 validiy 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 set_default == true
        //
        // Afterwards, the function can be called many times with other URIs and
        // set_default == false to check whether this cookie matches given uri
        //
 
        internal bool VerifySetDefaults(CookieVariant variant, Uri uri, bool isLocalDomain, string localDomain, bool set_default, bool isThrow) {
 
            string host = uri.Host;
            int    port = uri.Port;
            string path = uri.AbsolutePath;
            bool   valid= true;
 
            if (set_default) {
                // 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 (m_name == null || m_name.Length == 0 || m_name[0] == '$' || m_name.IndexOfAny(Reserved2Name) != -1) {
                if (isThrow) {
                    throw new CookieException(SR.GetString(SR.net_cookie_attribute, "Name", m_name == null? "<null>": m_name));
                }
                return false;
            }
 
            //Check the value
            if (m_value == null ||
                (!(m_value.Length > 2 && m_value[0] == '\"' && m_value[m_value.Length-1] == '\"') && m_value.IndexOfAny(Reserved2Value) != -1)) {
                if (isThrow) {
                    throw new CookieException(SR.GetString(SR.net_cookie_attribute, "Value", m_value == null? "<null>": m_value));
                }
                return false;
            }
 
            //Check Comment syntax
            if (Comment != null && !(Comment.Length > 2 && Comment[0] == '\"' && Comment[Comment.Length-1] == '\"')
                && (Comment.IndexOfAny(Reserved2Value) != -1)) {
                if (isThrow)
                   throw new CookieException(SR.GetString(SR.net_cookie_attribute, CommentAttributeName, Comment));
                return false;
            }
 
            //Check Path syntax
            if (Path != null  && !(Path.Length > 2 && Path[0] == '\"' && Path[Path.Length-1] == '\"')
                && (Path.IndexOfAny(Reserved2Value) != -1)) {
                if (isThrow) {
                    throw new CookieException(SR.GetString(SR.net_cookie_attribute, PathAttributeName, Path));
                }
                return false;
            }
 
            //Check/set domain
            // if domain is implicit => assume a) uri is valid, b) just set domain to uri hostname
            if (set_default && m_domain_implicit == true) {
                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 below code (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 (isThrow) {
                            throw new CookieException(SR.GetString(SR.net_cookie_attribute, DomainAttributeName, domain == null? "<null>": domain));
                        }
                        return false;
                    }
 
                    //domain must start with '.' if set explicitly
                    if(domain[0] != '.' ) {
                        if (!(variant == CookieVariant.Rfc2965 || variant == CookieVariant.Plain)) {
                            if (isThrow) {
                                throw new CookieException(SR.GetString(SR.net_cookie_attribute, DomainAttributeName, m_domain));
                            }
                            return false;
                        }
                        domain = '.' + domain;
                    }
 
                    int  host_dot = host.IndexOf('.');
 
                    // First quick check is for pushing a cookie into the local domain
                    if (isLocalDomain && (string.Compare(localDomain, domain, StringComparison.OrdinalIgnoreCase ) == 0)) {
                        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 distiguish 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 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.ToLower(CultureInfo.InvariantCulture);
                    }
                }
                else {
                    // for implicitly set domain AND at the set_default==false time
                    // we simply got to match uri.Host against m_domain
                    if (string.Compare(host, m_domain, StringComparison.OrdinalIgnoreCase) != 0) {
                        valid = false;
                    }
 
                }
                if(!valid) {
                    if (isThrow) {
                        throw new CookieException(SR.GetString(SR.net_cookie_attribute, DomainAttributeName, m_domain));
                    }
                    return false;
                }
            }
 
 
            //Check/Set Path
 
            if (set_default && m_path_implicit == true) {
                //assuming that uri path is always valid and contains at least one '/'
                switch (m_cookieVariant) {
                case CookieVariant.Plain:
                                        m_path = path;
                                        break;
                case CookieVariant.Rfc2109:
                                        m_path = path.Substring(0, path.LastIndexOf('/')); //may be empty
                                        break;
 
                case CookieVariant.Rfc2965:
                default:                //hope future versions will have same 'Path' semantic?
                                        m_path = path.Substring(0, path.LastIndexOf('/')+1);
                                        break;
 
                }
            }
            else {
                //check current path (implicit/explicit) against given uri
                if (!path.StartsWith(CookieParser.CheckQuoted(m_path))) {
                    if (isThrow) {
                        throw new CookieException(SR.GetString(SR.net_cookie_attribute, PathAttributeName, m_path));
                    }
                    return false;
                }
            }
 
            // set the default port if Port attribute was present but had no value
            if (set_default && (m_port_implicit == false && m_port.Length == 0)) {
                m_port_list = new int[1] {port};
            }
 
            if(m_port_implicit == false) {
                // Port must match agaist the one from the uri
                valid = false;
                foreach (int p in m_port_list) {
                    if (p == port) {
                        valid = true;
                        break;
                    }
                }
                if (!valid) {
                    if (isThrow) {
                        throw new CookieException(SR.GetString(SR.net_cookie_attribute, 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) {
            if (name == null || name.Length == 0) {
                return false;
            }
            for(int i=0; i < name.Length; ++i) {
                char ch = name[i];
                if (!(
                      (ch >= '0' && ch <= '9') ||
                      (ch == '.' || ch == '-') ||
                      (ch >= 'a' && ch <= 'z') ||
                      (ch >= 'A' && ch <= 'Z') ||
                      (ch == '_')
                    ))
                    return false;
            }
            return true;
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public string Port {
            get {
                return m_port;
            }
            set {
                m_port_implicit = false;
                if ((value == null || value.Length == 0)) {
                    //"Port" is present but has no value.
                    m_port = string.Empty;
                }
                else {
                    // Parse port list
                    if (value[0] != '\"' || value[value.Length-1] != '\"') {
                        throw new CookieException(SR.GetString(SR.net_cookie_attribute, PortAttributeName, value));
                    }
                    string[] ports = value.Split(PortSplitDelimiters);
 
                    List<int> portList = new List<int>();
                    int port;
                    
                    for (int i = 0; i < ports.Length; ++i) {
                        // Skip spaces
                        if (ports[i] != string.Empty) {
                            if (!Int32.TryParse(ports[i], out port))
                                throw new CookieException(SR.GetString(SR.net_cookie_attribute, PortAttributeName, value));
                            // valid values for port 0 - 0xFFFF
                            if ((port < 0) || (port > 0xFFFF))
                                throw new CookieException(SR.GetString(SR.net_cookie_attribute, PortAttributeName, value));
                            portList.Add(port);
                        }
                    }
                    m_port_list = portList.ToArray();
                    m_port = value;
                    m_version = MaxSupportedVersion;
                    m_cookieVariant = CookieVariant.Rfc2965;
                }
            }
        }
 
 
        internal int[] PortList {
            get {
                //It must be null if Port Attribute was ommitted in the response
                return m_port_list;
            }
        }
 
        private string _Port {
            get {
                return m_port_implicit ? string.Empty :
                      (SpecialAttributeLiteral
                       + PortAttributeName
                       + ((m_port.Length == 0) ? string.Empty : (EqualsLiteral + m_port))
                       );
            }
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public bool Secure {
            get {
                return m_secure;
            }
            set {
                m_secure = value;
            }
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public DateTime TimeStamp {
            get {
                return m_timeStamp;
            }
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public string Value {
            get {
                return m_value;
            }
            set {
                m_value = (value == null? String.Empty : value);
            }
        }
 
        internal CookieVariant Variant {
            get {
                return m_cookieVariant;
            }
            set {
                // only set by HttpListenerRequest::Cookies_get()
                GlobalLog.Assert(value == CookieVariant.Rfc2965, "Cookie#{0}::set_Variant()|value:{1}" , ValidationHelper.HashString(this), value);
                m_cookieVariant = value;
            }
        }
 
        // m_domainKey member is set internally in VerifySetDefaults()
        // If it is not set then verification function was not called
        // and this should never happen
        internal string DomainKey {
            get {
                return m_domain_implicit ? Domain : m_domainKey;
            }
        }
 
 
        //public Version Version {
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public int Version {
            get {
                return m_version;
            }
            set {
                if (value<0) {
                    throw new ArgumentOutOfRangeException("value");
                }
                m_version = value;
                if (value>0 && m_cookieVariant<CookieVariant.Rfc2109) {
                    m_cookieVariant = CookieVariant.Rfc2109;
                }
            }
        }
 
        private string _Version {
            get {
                  return (Version == 0) ? string.Empty :
                                         ( SpecialAttributeLiteral
                                         + VersionAttributeName
                                         + EqualsLiteral + (IsQuotedVersion? "\"": string.Empty)
                                         + m_version.ToString(NumberFormatInfo.InvariantInfo) + (IsQuotedVersion? "\"": string.Empty));
            }
        }
 
    // methods
 
 
        internal static IComparer GetComparer() {
            //the class don't have any members, it's safe reuse the instance
            return staticComparer;
        }
 
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public override bool Equals(object comparand) {
            if (!(comparand is Cookie)) {
                return false;
            }
 
            Cookie other = (Cookie)comparand;
 
            return (string.Compare(Name, other.Name, StringComparison.OrdinalIgnoreCase) == 0)
                    && (string.Compare(Value, other.Value, StringComparison.Ordinal) == 0)
                    && (string.Compare(Path, other.Path, StringComparison.Ordinal) == 0)
                    && (string.Compare(Domain, other.Domain, StringComparison.OrdinalIgnoreCase) == 0)
                    && (Version == other.Version)
                    ;
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public override int GetHashCode() {
            //
            //string hashString = Name + "=" + Value + ";" + Path + "; " + Domain + "; " + Version;
            //int hash = 0;
            //
            //foreach (char ch in hashString) {
            //    hash = unchecked(hash << 1 ^ (int)ch);
            //}
            //return hash;
            return (Name + "=" + Value + ";" + Path + "; " + Domain + "; " + Version).GetHashCode();
        }
 
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        public override string ToString() {
 
            string domain = _Domain;
            string path = _Path;
            string port = _Port;
            string version = _Version;
 
            string result =
                    ((version.Length == 0)? string.Empty : (version + SeparatorLiteral))
                    + Name + EqualsLiteral + Value
                    + ((path.Length == 0)   ? string.Empty : (SeparatorLiteral + path))
                    + ((domain.Length == 0) ? string.Empty : (SeparatorLiteral + domain))
                    + ((port.Length == 0)   ? string.Empty : (SeparatorLiteral + port))
                    ;
            if (result == "=") {
                return string.Empty;
            }
            return result;
        }
 
        internal string ToServerString() {
            string result = Name + EqualsLiteral + Value;
            if (m_comment!=null && m_comment.Length>0) {
                result += SeparatorLiteral + CommentAttributeName + EqualsLiteral + m_comment;
            }
            if (m_commentUri!=null) {
                result += SeparatorLiteral + CommentUrlAttributeName + EqualsLiteral + QuotesLiteral + m_commentUri.ToString() + QuotesLiteral;
            }
            if (m_discard) {
                result += SeparatorLiteral + DiscardAttributeName;
            }
            if (!m_domain_implicit && m_domain!=null && m_domain.Length>0) {
                result += SeparatorLiteral + DomainAttributeName + EqualsLiteral + m_domain;
            }
            if (Expires != DateTime.MinValue) {
                int seconds = (int) (Expires.ToLocalTime() - DateTime.Now).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 + MaxAgeAttributeName + EqualsLiteral + seconds.ToString(NumberFormatInfo.InvariantInfo);
            }
            if (!m_path_implicit && m_path!=null && m_path.Length>0) {
                result += SeparatorLiteral + PathAttributeName + EqualsLiteral + m_path;
            }
            if (!Plain && !m_port_implicit && m_port!=null && m_port.Length>0) {
                // QuotesLiteral are included in m_port
                result += SeparatorLiteral + PortAttributeName + EqualsLiteral + m_port;
            }
            if (m_version>0) {
                result += SeparatorLiteral + VersionAttributeName + EqualsLiteral + m_version.ToString(NumberFormatInfo.InvariantInfo);
            }
            return result==EqualsLiteral ? null : result;
        }
 
        //private void Validate() {
        //    if ((m_name == String.Empty) && (m_value == String.Empty)) {
        //        throw new CookieException();
        //    }
        //    if ((m_name != String.Empty) && (m_name[0] == '$')) {
        //        throw new CookieException();
        //    }
        //}
 
#if DEBUG
        /// <devdoc>
        ///    <para>[To be supplied.]</para>
        /// </devdoc>
        internal void Dump() {
            GlobalLog.Print("Cookie: " + ToString() + "->\n"
                            + "\tComment    = " + Comment + "\n"
                            + "\tCommentUri = " + CommentUri + "\n"
                            + "\tDiscard    = " + Discard + "\n"
                            + "\tDomain     = " + Domain + "\n"
                            + "\tExpired    = " + Expired + "\n"
                            + "\tExpires    = " + Expires + "\n"
                            + "\tName       = " + Name + "\n"
                            + "\tPath       = " + Path + "\n"
                            + "\tPort       = " + Port + "\n"
                            + "\tSecure     = " + Secure + "\n"
                            + "\tTimeStamp  = " + TimeStamp + "\n"
                            + "\tValue      = " + Value + "\n"
                            + "\tVariant    = " + Variant + "\n"
                            + "\tVersion    = " + Version + "\n"
                            + "\tHttpOnly    = " + HttpOnly + "\n"
                            );
        }
#endif
    }
 
    internal enum CookieToken {
 
    // state types
 
        Nothing,
        NameValuePair,  // X=Y
        Attribute,      // X
        EndToken,       // ';'
        EndCookie,      // ','
        End,            // EOLN
        Equals,
 
    // value types
 
        Comment,
        CommentUrl,
        CookieName,
        Discard,
        Domain,
        Expires,
        MaxAge,
        Path,
        Port,
        Secure,
        HttpOnly,
        Unknown,
        Version
    }
 
    //
    // CookieTokenizer
    //
    //  Used to split a single or multi-cookie (header) string into individual
    //  tokens
    //
 
    internal class CookieTokenizer {
 
    // fields
 
        bool m_eofCookie;
        int m_index;
        int m_length;
        string m_name;
        bool m_quoted;
        int m_start;
        CookieToken m_token;
        int m_tokenLength;
        string m_tokenStream;
        string m_value;
 
    // constructors
 
        internal CookieTokenizer(string tokenStream) {
            m_length = tokenStream.Length;
            m_tokenStream = tokenStream;
        }
 
    // properties
 
        internal bool EndOfCookie {
            get {
                return m_eofCookie;
            }
            set {
                m_eofCookie = value;
            }
        }
 
        internal bool Eof {
            get {
                return m_index >= m_length;
            }
        }
 
        internal string Name {
            get {
                return m_name;
            }
            set {
                m_name = value;
            }
        }
 
        internal bool Quoted {
            get {
                return m_quoted;
            }
            set {
                m_quoted = value;
            }
        }
 
        internal CookieToken Token {
            get {
                return m_token;
            }
            set {
                m_token = value;
            }
        }
 
        internal string Value {
            get {
                return m_value;
            }
            set {
                m_value = value;
            }
        }
 
    // methods
 
        //
        // Extract
        //
        //  extract the current token
        //
 
        internal string Extract() {
 
            string tokenString = string.Empty;
 
            if (m_tokenLength != 0) {
                tokenString = m_tokenStream.Substring(m_start, m_tokenLength);
                if (!Quoted) {
                    tokenString = tokenString.Trim();
                }
            }
            return tokenString;
        }
 
        //
        // FindNext
        //
        //  Find the start and length of the next token. The token is terminated
        //  by one of:
        //
        //      - end-of-line
        //      - end-of-cookie: unquoted comma separates multiple cookies
        //      - end-of-token: unquoted semi-colon
        //      - end-of-name: unquoted equals
        //
        // Inputs:
        //  <argument>  ignoreComma
        //      true if parsing doesn't stop at a comma. This is only true when
        //      we know we're parsing an original cookie that has an expires=
        //      attribute, because the format of the time/date used in expires
        //      is:
        //          Wdy, dd-mmm-yyyy HH:MM:SS GMT
        //
        //  <argument>  ignoreEquals
        //      true if parsing doesn't stop at an equals sign. The LHS of the
        //      first equals sign is an attribute name. The next token may
        //      include one or more equals signs. E.g.,
        //
        //          SESSIONID=ID=MSNx45&q=33
        //
        // Outputs:
        //  <member>    m_index
        //      incremented to the last position in m_tokenStream contained by
        //      the current token
        //
        //  <member>    m_start
        //      incremented to the start of the current token
        //
        //  <member>    m_tokenLength
        //      set to the length of the current token
        //
        // Assumes:
        //  Nothing
        //
        // Returns:
        //  type of CookieToken found:
        //
        //      End         - end of the cookie string
        //      EndCookie   - end of current cookie in (potentially) a
        //                    multi-cookie string
        //      EndToken    - end of name=value pair, or end of an attribute
        //      Equals      - end of name=
        //
        // Throws:
        //  Nothing
        //
 
        internal CookieToken FindNext(bool ignoreComma, bool ignoreEquals) {
            m_tokenLength = 0;
            m_start = m_index;
            while ((m_index < m_length) && Char.IsWhiteSpace(m_tokenStream[m_index])) {
                ++m_index;
                ++m_start;
            }
 
            CookieToken token = CookieToken.End;
            int increment = 1;
 
            if (!Eof) {
                if (m_tokenStream[m_index] == '"') {
                    Quoted = true;
                    ++m_index;
                    bool quoteOn = false;
                    while (m_index < m_length) {
                        char currChar = m_tokenStream[m_index];
                        if (!quoteOn && currChar == '"')
                            break;
                        if (quoteOn)
                            quoteOn = false;
                        else if (currChar == '\\')
                            quoteOn = true;
                        ++m_index;
                    }
                    if (m_index < m_length) {
                        ++m_index;
                    }
                    m_tokenLength = m_index - m_start;
                    increment = 0;
                    // if we are here, reset ignoreComma
                    // In effect, we ignore everything after quoted string till next delimiter
                    ignoreComma = false;
                }
                while ((m_index < m_length)
                       && (m_tokenStream[m_index] != ';')
                       && (ignoreEquals || (m_tokenStream[m_index] != '='))
                       && (ignoreComma || (m_tokenStream[m_index] != ','))) {
 
                    // Fixing 2 things:
                    // 1) ignore day of week in cookie string
                    // 2) revert ignoreComma once meet it, so won't miss the next cookie)
                    if (m_tokenStream[m_index] == ',') {
                        m_start = m_index+1;
                        m_tokenLength = -1;
                        ignoreComma = false;
                    }
                    ++m_index;
                    m_tokenLength += increment;
 
                }
                if (!Eof) {
                    switch (m_tokenStream[m_index]) {
                        case ';':
                            token = CookieToken.EndToken;
                            break;
 
                        case '=':
                            token = CookieToken.Equals;
                            break;
 
                        default:
                            token = CookieToken.EndCookie;
                            break;
                    }
                    ++m_index;
                }
            }
            return token;
        }
 
        //
        // Next
        //
        //  Get the next cookie name/value or attribute
        //
        //  Cookies come in the following formats:
        //
        //      1. Version0
        //          Set-Cookie: [<name>][=][<value>]
        //                      [; expires=<date>]
        //                      [; path=<path>]
        //                      [; domain=<domain>]
        //                      [; secure]
        //          Cookie: <name>=<value>
        //
        //          Notes: <name> and/or <value> may be blank
        //                 <date> is the RFC 822/1123 date format that
        //                 incorporates commas, e.g.
        //                 "Wednesday, 09-Nov-99 23:12:40 GMT"
        //
        //      2. RFC 2109
        //          Set-Cookie: 1#{
        //                          <name>=<value>
        //                          [; comment=<comment>]
        //                          [; domain=<domain>]
        //                          [; max-age=<seconds>]
        //                          [; path=<path>]
        //                          [; secure]
        //                          ; Version=<version>
        //                      }
        //          Cookie: $Version=<version>
        //                  1#{
        //                      ; <name>=<value>
        //                      [; path=<path>]
        //                      [; domain=<domain>]
        //                  }
        //
        //      3. RFC 2965
        //          Set-Cookie2: 1#{
        //                          <name>=<value>
        //                          [; comment=<comment>]
        //                          [; commentURL=<comment>]
        //                          [; discard]
        //                          [; domain=<domain>]
        //                          [; max-age=<seconds>]
        //                          [; path=<path>]
        //                          [; ports=<portlist>]
        //                          [; secure]
        //                          ; Version=<version>
        //                       }
        //          Cookie: $Version=<version>
        //                  1#{
        //                      ; <name>=<value>
        //                      [; path=<path>]
        //                      [; domain=<domain>]
        //                      [; port="<port>"]
        //                  }
        //          [Cookie2: $Version=<version>]
        //
        // Inputs:
        //  <argument>  first
        //      true if this is the first name/attribute that we have looked for
        //      in the cookie stream
        //
        // Outputs:
        //
        // Assumes:
        //  Nothing
        //
        // Returns:
        //  type of CookieToken found:
        //
        //      - Attribute
        //          - token was single-value. May be empty. Caller should check
        //            Eof or EndCookie to determine if any more action needs to
        //            be taken
        //
        //      - NameValuePair
        //          - Name and Value are meaningful. Either may be empty
        //
        // Throws:
        //  Nothing
        //
 
        internal CookieToken Next(bool first, bool parseResponseCookies) {
 
            Reset();
 
            CookieToken terminator = FindNext(false, false);
            if (terminator == CookieToken.EndCookie) {
                EndOfCookie = true;
            }
 
            if ((terminator == CookieToken.End) || (terminator == CookieToken.EndCookie)) {
                if ((Name = Extract()).Length != 0) {
                    Token = TokenFromName(parseResponseCookies);
                    return CookieToken.Attribute;
                }
                return terminator;
            }
            Name = Extract();
            if (first) {
                Token = CookieToken.CookieName;
            }
            else {
                Token = TokenFromName(parseResponseCookies);
            }
            if (terminator == CookieToken.Equals) {
                terminator = FindNext(!first && (Token == CookieToken.Expires), true);
                if (terminator == CookieToken.EndCookie) {
                    EndOfCookie = true;
                }
                Value = Extract();
                return CookieToken.NameValuePair;
            }
            else {
                return CookieToken.Attribute;
            }
        }
 
        //
        // Reset
        //
        //  set up this tokenizer for finding the next name/value pair or
        //  attribute, or end-of-[token, cookie, or line]
        //
 
        internal void Reset() {
            m_eofCookie = false;
            m_name = string.Empty;
            m_quoted = false;
            m_start = m_index;
            m_token = CookieToken.Nothing;
            m_tokenLength = 0;
            m_value = string.Empty;
        }
 
        private struct RecognizedAttribute {
 
            string m_name;
            CookieToken m_token;
 
            internal RecognizedAttribute(string name, CookieToken token) {
                m_name = name;
                m_token = token;
            }
 
            internal CookieToken Token {
                get {
                    return m_token;
                }
            }
 
            internal bool IsEqualTo(string value) {
                return string.Compare(m_name, value, StringComparison.OrdinalIgnoreCase) == 0;
            }
        }
 
        //
        // recognized attributes in order of expected commonality
        //
 
        static RecognizedAttribute[] RecognizedAttributes = {
            new RecognizedAttribute(Cookie.PathAttributeName, CookieToken.Path),
            new RecognizedAttribute(Cookie.MaxAgeAttributeName, CookieToken.MaxAge),
            new RecognizedAttribute(Cookie.ExpiresAttributeName, CookieToken.Expires),
            new RecognizedAttribute(Cookie.VersionAttributeName, CookieToken.Version),
            new RecognizedAttribute(Cookie.DomainAttributeName, CookieToken.Domain),
            new RecognizedAttribute(Cookie.SecureAttributeName, CookieToken.Secure),
            new RecognizedAttribute(Cookie.DiscardAttributeName, CookieToken.Discard),
            new RecognizedAttribute(Cookie.PortAttributeName, CookieToken.Port),
            new RecognizedAttribute(Cookie.CommentAttributeName, CookieToken.Comment),
            new RecognizedAttribute(Cookie.CommentUrlAttributeName, CookieToken.CommentUrl),
            new RecognizedAttribute(Cookie.HttpOnlyAttributeName, CookieToken.HttpOnly),
        };
 
        static RecognizedAttribute[] RecognizedServerAttributes = {
            new RecognizedAttribute('$' + Cookie.PathAttributeName, CookieToken.Path),
            new RecognizedAttribute('$' + Cookie.VersionAttributeName, CookieToken.Version),
            new RecognizedAttribute('$' + Cookie.DomainAttributeName, CookieToken.Domain),
            new RecognizedAttribute('$' + Cookie.PortAttributeName, CookieToken.Port),
            new RecognizedAttribute('$' + Cookie.HttpOnlyAttributeName, CookieToken.HttpOnly),
        };
 
        internal CookieToken TokenFromName(bool parseResponseCookies) {
            if (!parseResponseCookies) {
                for (int i = 0; i < RecognizedServerAttributes.Length; ++i) {
                    if (RecognizedServerAttributes[i].IsEqualTo(Name)) {
                        return RecognizedServerAttributes[i].Token;
                    }
                }
            }
            else {
                for (int i = 0; i < RecognizedAttributes.Length; ++i) {
                    if (RecognizedAttributes[i].IsEqualTo(Name)) {
                        return RecognizedAttributes[i].Token;
                    }
                }
            }
            return CookieToken.Unknown;
        }
    }
 
    //
    // CookieParser
    //
    //  Takes a cookie header, makes cookies
    //
 
    internal class CookieParser {
 
    // fields
 
        CookieTokenizer m_tokenizer;
        Cookie m_savedCookie;
 
    // constructors
 
        internal CookieParser(string cookieString) {
            m_tokenizer = new CookieTokenizer(cookieString);
        }
 
    // properties
 
    // methods
 
        //
        // Get
        //
        //  Gets the next cookie
        //
        // Inputs:
        //  Nothing
        //
        // Outputs:
        //  Nothing
        //
        // Assumes:
        //  Nothing
        //
        // Returns:
        //  new cookie object, or null if there's no more
        //
        // Throws:
        //  Nothing
        //
 
        internal Cookie Get() {
 
            Cookie cookie = null;
 
            // only first ocurence of an attribute value must be counted
            bool   commentSet = false;
            bool   commentUriSet = false;
            bool   domainSet = false;
            bool   expiresSet = false;
            bool   pathSet = false;
            bool   portSet = false; //special case as it may have no value in header
            bool   versionSet = false;
            bool   secureSet = false;
            bool   discardSet = false;
 
            do {
                CookieToken token = m_tokenizer.Next(cookie==null, true);
                if (cookie==null && (token==CookieToken.NameValuePair || token==CookieToken.Attribute)) {
                    cookie = new Cookie();
                    if (cookie.InternalSetName(m_tokenizer.Name) == false) {
                        //will be rejected
                        cookie.InternalSetName(string.Empty);
                    }
                    cookie.Value = m_tokenizer.Value;
                }
                else {
                    switch (token) {
                    case CookieToken.NameValuePair:
                        switch (m_tokenizer.Token) {
                            case CookieToken.Comment:
                                if (!commentSet) {
                                    commentSet = true;
                                    cookie.Comment = m_tokenizer.Value;
                                }
                                break;
 
                            case CookieToken.CommentUrl:
                                if (!commentUriSet) {
                                    commentUriSet = true;
                                    Uri parsed;
                                    if (Uri.TryCreate(CheckQuoted(m_tokenizer.Value), UriKind.Absolute, out parsed)) {
                                        cookie.CommentUri = parsed;
                                    }
                                }
                                break;
 
                            case CookieToken.Domain:
                                if (!domainSet) {
                                    domainSet = true;
                                    cookie.Domain = CheckQuoted(m_tokenizer.Value);
                                    cookie.IsQuotedDomain = m_tokenizer.Quoted;
                                }
                                break;
 
                            case CookieToken.Expires:
                                if (!expiresSet) {
                                    expiresSet = true;
 
                                    DateTime expires;
                                    if (DateTime.TryParse(CheckQuoted(m_tokenizer.Value), 
                                        CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out expires)) {
                                        cookie.Expires = expires;
                                    }
                                    else {
                                        //this cookie will be rejected
                                        cookie.InternalSetName(string.Empty);
                                    }
                                }
                                break;
 
                            case CookieToken.MaxAge:
                                if (!expiresSet) {
                                    expiresSet = true;
                                    int parsed;
                                    if (int.TryParse(CheckQuoted(m_tokenizer.Value), out parsed)) {
                                        cookie.Expires = DateTime.Now.AddSeconds((double)parsed);
                                    }
                                    else {
                                        //this cookie will be rejected
                                        cookie.InternalSetName(string.Empty);
                                    }
                                }
                                break;
 
                            case CookieToken.Path:
                                if (!pathSet) {
                                    pathSet = true;
                                    cookie.Path = m_tokenizer.Value;
                                }
                                break;
 
                            case CookieToken.Port:
                                if (!portSet) {
                                    portSet = true;
                                    try {
                                        cookie.Port = m_tokenizer.Value;
                                    }
                                    catch {
                                        //this cookie will be rejected
                                        cookie.InternalSetName(string.Empty);
                                    }
                                }
                                break;
 
                            case CookieToken.Version:
                                if (!versionSet) {
                                    versionSet = true;
                                    int parsed;
                                    if (int.TryParse(CheckQuoted(m_tokenizer.Value), out parsed)) {
                                        cookie.Version = parsed;
                                        cookie.IsQuotedVersion = m_tokenizer.Quoted;
                                    }
                                    else {
                                        //this cookie will be rejected
                                        cookie.InternalSetName(string.Empty);
                                    }
                                }
                                break;
                        }
                        break;
 
                    case CookieToken.Attribute:
                        switch (m_tokenizer.Token) {
                            case CookieToken.Discard:
                                if (!discardSet) {
                                    discardSet = true;
                                    cookie.Discard = true;
                                }
                                break;
 
                            case CookieToken.Secure:
                                if (!secureSet) {
                                    secureSet = true;
                                    cookie.Secure = true;
                                }
                                break;
 
                            case CookieToken.HttpOnly:
                                cookie.HttpOnly = true;
                                break;
 
                            case CookieToken.Port:
                                if (!portSet) {
                                    portSet = true;
                                    cookie.Port  = string.Empty;
                                }
                                break;
                        }
                        break;
                    }
                }
            } while (!m_tokenizer.Eof && !m_tokenizer.EndOfCookie);
            return cookie;
        }
 
        // twin parsing method, different enough that it's better to split it into
        // a different method
        internal Cookie GetServer() {
 
            Cookie cookie = m_savedCookie;
            m_savedCookie = null;
 
            // only first ocurence of an attribute value must be counted
            bool   domainSet = false;
            bool   pathSet = false;
            bool   portSet = false; //special case as it may have no value in header
 
            do {
                bool first = cookie==null || cookie.Name==null || cookie.Name.Length==0;
                CookieToken token = m_tokenizer.Next(first, false);
 
                if (first && (token==CookieToken.NameValuePair || token==CookieToken.Attribute)) {
                    if (cookie==null) {
                        cookie = new Cookie();
                    }
                    if (cookie.InternalSetName(m_tokenizer.Name) == false) {
                        //will be rejected
                        cookie.InternalSetName(string.Empty);
                    }
                    cookie.Value = m_tokenizer.Value;
                }
                else {
                    switch (token) {
                    case CookieToken.NameValuePair:
                        switch (m_tokenizer.Token) {
                            case CookieToken.Domain:
                                if (!domainSet) {
                                    domainSet = true;
                                    cookie.Domain = CheckQuoted(m_tokenizer.Value);
                                    cookie.IsQuotedDomain = m_tokenizer.Quoted;
                                }
                                break;
 
                            case CookieToken.Path:
                                if (!pathSet) {
                                    pathSet = true;
                                    cookie.Path = m_tokenizer.Value;
                                }
                                break;
 
                            case CookieToken.Port:
                                if (!portSet) {
                                    portSet = true;
                                    try {
                                        cookie.Port = m_tokenizer.Value;
                                    }
                                    catch (CookieException) {
                                        //this cookie will be rejected
                                        cookie.InternalSetName(string.Empty);
                                    }
                                }
                                break;
 
                            case CookieToken.Version:
                                // this is a new cookie, this token is for the next cookie.
                                m_savedCookie = new Cookie();
                                int parsed;
                                if (int.TryParse(m_tokenizer.Value, out parsed)) {
                                    m_savedCookie.Version = parsed;
                                }
                                return cookie;
 
                            case CookieToken.Unknown:
                                // this is a new cookie, the token is for the next cookie.
                                m_savedCookie = new Cookie();
                                if (m_savedCookie.InternalSetName(m_tokenizer.Name) == false) {
                                    //will be rejected
                                    m_savedCookie.InternalSetName(string.Empty);
                                }
                                m_savedCookie.Value = m_tokenizer.Value;
                                return cookie;
 
                        }
                        break;
 
                    case CookieToken.Attribute:
                        switch (m_tokenizer.Token) {
                            case CookieToken.Port:
                                if (!portSet) {
                                    portSet = true;
                                    cookie.Port  = string.Empty;
                                }
                                break;
                        }
                        break;
                    }
                }
            } while (!m_tokenizer.Eof && !m_tokenizer.EndOfCookie);
            return cookie;
        }
 
        internal static string CheckQuoted(string value) {
            if (value.Length < 2 || value[0] != '\"' || value[value.Length-1] != '\"')
                return value;
 
            return value.Length == 2? string.Empty: value.Substring(1, value.Length-2);
        }
        
        internal bool EndofHeader()
        {
            return m_tokenizer.Eof;
        }
    }
 
 
    internal class Comparer: IComparer {
 
            int IComparer.Compare(object ol, object or) {
 
                Cookie left  = (Cookie) ol;
                Cookie right = (Cookie) or;
 
                int result;
 
                if ((result = string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase)) != 0) {
                    return result;
                }
 
                if ((result = string.Compare(left.Domain, right.Domain, StringComparison.OrdinalIgnoreCase)) != 0) {
                    return result;
                }
 
                //
                //NB: The only path is case sensitive as per spec.
                //    However, on Win Platform that may break some lazy applications.
                //
                if ((result = string.Compare(left.Path, right.Path, StringComparison.Ordinal)) != 0) {
                    return result;
                }
                // They are equal here even if variants are still different.
                return 0;
           }
   }
}