#region License /* * Cookie.cs * * This code is derived from Cookie.cs (System.Net) of Mono * (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2004,2009 Novell, Inc. (http://www.novell.com) * Copyright (c) 2012-2019 sta.blockhead * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #endregion #region Authors /* * Authors: * - Lawrence Pit * - Gonzalo Paniagua Javier * - Daniel Nauck * - Sebastien Pouliot */ #endregion using System; using System.Globalization; using System.Text; namespace WebSocketSharp.Net { /// /// Provides a set of methods and properties used to manage an HTTP cookie. /// /// /// /// This class refers to the following specifications: /// /// /// /// /// Netscape specification /// /// /// /// /// RFC 2109 /// /// /// /// /// RFC 2965 /// /// /// /// /// RFC 6265 /// /// /// /// /// This class cannot be inherited. /// /// [Serializable] public sealed class Cookie { #region Private Fields private string _comment; private Uri _commentUri; private bool _discard; private string _domain; private static readonly int[] _emptyPorts; private DateTime _expires; private bool _httpOnly; private string _name; private string _path; private string _port; private int[] _ports; private static readonly char[] _reservedCharsForValue; private string _sameSite; private bool _secure; private DateTime _timeStamp; private string _value; private int _version; #endregion #region Static Constructor static Cookie () { _emptyPorts = new int[0]; _reservedCharsForValue = new[] { ';', ',' }; } #endregion #region Internal Constructors /// /// Initializes a new instance of the class. /// internal Cookie () { init (String.Empty, String.Empty, String.Empty, String.Empty); } #endregion #region Public Constructors /// /// Initializes a new instance of the class with /// the specified name and value. /// /// /// /// A that specifies the name of the cookie. /// /// /// The name must be a token defined in /// /// RFC 2616. /// /// /// /// A that specifies the value of the cookie. /// /// /// is . /// /// /// /// is an empty string. /// /// /// - or - /// /// /// starts with a dollar sign. /// /// /// - or - /// /// /// contains an invalid character. /// /// /// - or - /// /// /// is a string not enclosed in double quotes /// that contains an invalid character. /// /// public Cookie (string name, string value) : this (name, value, String.Empty, String.Empty) { } /// /// Initializes a new instance of the class with /// the specified name, value, and path. /// /// /// /// A that specifies the name of the cookie. /// /// /// The name must be a token defined in /// /// RFC 2616. /// /// /// /// A that specifies the value of the cookie. /// /// /// A that specifies the value of the Path /// attribute of the cookie. /// /// /// is . /// /// /// /// is an empty string. /// /// /// - or - /// /// /// starts with a dollar sign. /// /// /// - or - /// /// /// contains an invalid character. /// /// /// - or - /// /// /// is a string not enclosed in double quotes /// that contains an invalid character. /// /// public Cookie (string name, string value, string path) : this (name, value, path, String.Empty) { } /// /// Initializes a new instance of the class with /// the specified name, value, path, and domain. /// /// /// /// A that specifies the name of the cookie. /// /// /// The name must be a token defined in /// /// RFC 2616. /// /// /// /// A that specifies the value of the cookie. /// /// /// A that specifies the value of the Path /// attribute of the cookie. /// /// /// A that specifies the value of the Domain /// attribute of the cookie. /// /// /// is . /// /// /// /// is an empty string. /// /// /// - or - /// /// /// starts with a dollar sign. /// /// /// - or - /// /// /// contains an invalid character. /// /// /// - or - /// /// /// is a string not enclosed in double quotes /// that contains an invalid character. /// /// public Cookie (string name, string value, string path, string domain) { if (name == null) throw new ArgumentNullException ("name"); if (name.Length == 0) throw new ArgumentException ("An empty string.", "name"); if (name[0] == '$') { var msg = "It starts with a dollar sign."; throw new ArgumentException (msg, "name"); } if (!name.IsToken ()) { var msg = "It contains an invalid character."; throw new ArgumentException (msg, "name"); } if (value == null) value = String.Empty; if (value.Contains (_reservedCharsForValue)) { if (!value.IsEnclosedIn ('"')) { var msg = "A string not enclosed in double quotes."; throw new ArgumentException (msg, "value"); } } init (name, value, path ?? String.Empty, domain ?? String.Empty); } #endregion #region Internal Properties internal bool ExactDomain { get { return _domain.Length == 0 || _domain[0] != '.'; } } internal int MaxAge { get { if (_expires == DateTime.MinValue) return 0; var expires = _expires.Kind != DateTimeKind.Local ? _expires.ToLocalTime () : _expires; var span = expires - DateTime.Now; return span > TimeSpan.Zero ? (int) span.TotalSeconds : 0; } set { _expires = value > 0 ? DateTime.Now.AddSeconds ((double) value) : DateTime.Now; } } internal int[] Ports { get { return _ports ?? _emptyPorts; } } internal string SameSite { get { return _sameSite; } set { _sameSite = value; } } #endregion #region Public Properties /// /// Gets the value of the Comment attribute of the cookie. /// /// /// /// A that represents the comment to document /// intended use of the cookie. /// /// /// if not present. /// /// /// The default value is . /// /// public string Comment { get { return _comment; } internal set { _comment = value; } } /// /// Gets the value of the CommentURL attribute of the cookie. /// /// /// /// A that represents the URI that provides /// the comment to document intended use of the cookie. /// /// /// if not present. /// /// /// The default value is . /// /// public Uri CommentUri { get { return _commentUri; } internal set { _commentUri = value; } } /// /// Gets a value indicating whether the client discards the cookie /// unconditionally when the client terminates. /// /// /// /// true if the client discards the cookie unconditionally /// when the client terminates; otherwise, false. /// /// /// The default value is false. /// /// public bool Discard { get { return _discard; } internal set { _discard = value; } } /// /// Gets or sets the value of the Domain attribute of the cookie. /// /// /// /// A that represents the domain name that /// the cookie is valid for. /// /// /// An empty string if this attribute is not needed. /// /// public string Domain { get { return _domain; } set { _domain = value ?? String.Empty; } } /// /// Gets or sets a value indicating whether the cookie has expired. /// /// /// /// true if the cookie has expired; otherwise, false. /// /// /// The default value is false. /// /// public bool Expired { get { return _expires != DateTime.MinValue && _expires <= DateTime.Now; } set { _expires = value ? DateTime.Now : DateTime.MinValue; } } /// /// Gets or sets the value of the Expires attribute of the cookie. /// /// /// /// A that represents the date and time that /// the cookie expires on. /// /// /// if this attribute is not needed. /// /// /// The default value is . /// /// public DateTime Expires { get { return _expires; } set { _expires = value; } } /// /// Gets or sets a value indicating whether non-HTTP APIs can access /// the cookie. /// /// /// /// true if non-HTTP APIs cannot access the cookie; otherwise, /// false. /// /// /// The default value is false. /// /// public bool HttpOnly { get { return _httpOnly; } set { _httpOnly = value; } } /// /// Gets or sets the name of the cookie. /// /// /// /// A that represents the name of the cookie. /// /// /// The name must be a token defined in /// /// RFC 2616. /// /// /// /// The value specified for a set operation is . /// /// /// /// The value specified for a set operation is an empty string. /// /// /// - or - /// /// /// The value specified for a set operation starts with a dollar sign. /// /// /// - or - /// /// /// The value specified for a set operation contains an invalid character. /// /// public string Name { get { return _name; } set { if (value == null) throw new ArgumentNullException ("value"); if (value.Length == 0) throw new ArgumentException ("An empty string.", "value"); if (value[0] == '$') { var msg = "It starts with a dollar sign."; throw new ArgumentException (msg, "value"); } if (!value.IsToken ()) { var msg = "It contains an invalid character."; throw new ArgumentException (msg, "value"); } _name = value; } } /// /// Gets or sets the value of the Path attribute of the cookie. /// /// /// A that represents the subset of URI on /// the origin server that the cookie applies to. /// public string Path { get { return _path; } set { _path = value ?? String.Empty; } } /// /// Gets the value of the Port attribute of the cookie. /// /// /// /// A that represents the list of TCP ports /// that the cookie applies to. /// /// /// if not present. /// /// /// The default value is . /// /// public string Port { get { return _port; } internal set { int[] ports; if (!tryCreatePorts (value, out ports)) return; _port = value; _ports = ports; } } /// /// Gets or sets a value indicating whether the security level of /// the cookie is secure. /// /// /// When this property is true, the cookie may be included in /// the request only if the request is transmitted over HTTPS. /// /// /// /// true if the security level of the cookie is secure; /// otherwise, false. /// /// /// The default value is false. /// /// public bool Secure { get { return _secure; } set { _secure = value; } } /// /// Gets the time when the cookie was issued. /// /// /// A that represents the time when /// the cookie was issued. /// public DateTime TimeStamp { get { return _timeStamp; } } /// /// Gets or sets the value of the cookie. /// /// /// A that represents the value of the cookie. /// /// /// The value specified for a set operation is a string not enclosed in /// double quotes that contains an invalid character. /// public string Value { get { return _value; } set { if (value == null) value = String.Empty; if (value.Contains (_reservedCharsForValue)) { if (!value.IsEnclosedIn ('"')) { var msg = "A string not enclosed in double quotes."; throw new ArgumentException (msg, "value"); } } _value = value; } } /// /// Gets the value of the Version attribute of the cookie. /// /// /// /// An that represents the version of HTTP state /// management that the cookie conforms to. /// /// /// 0 or 1. 0 if not present. /// /// /// The default value is 0. /// /// public int Version { get { return _version; } internal set { if (value < 0 || value > 1) return; _version = value; } } #endregion #region Private Methods private static int hash (int i, int j, int k, int l, int m) { return i ^ (j << 13 | j >> 19) ^ (k << 26 | k >> 6) ^ (l << 7 | l >> 25) ^ (m << 20 | m >> 12); } private void init (string name, string value, string path, string domain) { _name = name; _value = value; _path = path; _domain = domain; _expires = DateTime.MinValue; _timeStamp = DateTime.Now; } private string toResponseStringVersion0 () { var buff = new StringBuilder (64); buff.AppendFormat ("{0}={1}", _name, _value); if (_expires != DateTime.MinValue) { buff.AppendFormat ( "; Expires={0}", _expires.ToUniversalTime ().ToString ( "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", CultureInfo.CreateSpecificCulture ("en-US") ) ); } if (!_path.IsNullOrEmpty ()) buff.AppendFormat ("; Path={0}", _path); if (!_domain.IsNullOrEmpty ()) buff.AppendFormat ("; Domain={0}", _domain); if (!_sameSite.IsNullOrEmpty ()) buff.AppendFormat ("; SameSite={0}", _sameSite); if (_secure) buff.Append ("; Secure"); if (_httpOnly) buff.Append ("; HttpOnly"); return buff.ToString (); } private string toResponseStringVersion1 () { var buff = new StringBuilder (64); buff.AppendFormat ("{0}={1}; Version={2}", _name, _value, _version); if (_expires != DateTime.MinValue) buff.AppendFormat ("; Max-Age={0}", MaxAge); if (!_path.IsNullOrEmpty ()) buff.AppendFormat ("; Path={0}", _path); if (!_domain.IsNullOrEmpty ()) buff.AppendFormat ("; Domain={0}", _domain); if (_port != null) { if (_port != "\"\"") buff.AppendFormat ("; Port={0}", _port); else buff.Append ("; Port"); } if (_comment != null) buff.AppendFormat ("; Comment={0}", HttpUtility.UrlEncode (_comment)); if (_commentUri != null) { var url = _commentUri.OriginalString; buff.AppendFormat ( "; CommentURL={0}", !url.IsToken () ? url.Quote () : url ); } if (_discard) buff.Append ("; Discard"); if (_secure) buff.Append ("; Secure"); return buff.ToString (); } private static bool tryCreatePorts (string value, out int[] result) { result = null; var arr = value.Trim ('"').Split (','); var len = arr.Length; var res = new int[len]; for (var i = 0; i < len; i++) { var s = arr[i].Trim (); if (s.Length == 0) { res[i] = Int32.MinValue; continue; } if (!Int32.TryParse (s, out res[i])) return false; } result = res; return true; } #endregion #region Internal Methods internal bool EqualsWithoutValue (Cookie cookie) { var caseSensitive = StringComparison.InvariantCulture; var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; return _name.Equals (cookie._name, caseInsensitive) && _path.Equals (cookie._path, caseSensitive) && _domain.Equals (cookie._domain, caseInsensitive) && _version == cookie._version; } internal bool EqualsWithoutValueAndVersion (Cookie cookie) { var caseSensitive = StringComparison.InvariantCulture; var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; return _name.Equals (cookie._name, caseInsensitive) && _path.Equals (cookie._path, caseSensitive) && _domain.Equals (cookie._domain, caseInsensitive); } internal string ToRequestString (Uri uri) { if (_name.Length == 0) return String.Empty; if (_version == 0) return String.Format ("{0}={1}", _name, _value); var buff = new StringBuilder (64); buff.AppendFormat ("$Version={0}; {1}={2}", _version, _name, _value); if (!_path.IsNullOrEmpty ()) buff.AppendFormat ("; $Path={0}", _path); else if (uri != null) buff.AppendFormat ("; $Path={0}", uri.GetAbsolutePath ()); else buff.Append ("; $Path=/"); if (!_domain.IsNullOrEmpty ()) { if (uri == null || uri.Host != _domain) buff.AppendFormat ("; $Domain={0}", _domain); } if (_port != null) { if (_port != "\"\"") buff.AppendFormat ("; $Port={0}", _port); else buff.Append ("; $Port"); } return buff.ToString (); } /// /// Returns a string that represents the current cookie instance. /// /// /// A that is suitable for the Set-Cookie response /// header. /// internal string ToResponseString () { return _name.Length == 0 ? String.Empty : _version == 0 ? toResponseStringVersion0 () : toResponseStringVersion1 (); } internal static bool TryCreate ( string name, string value, out Cookie result ) { result = null; try { result = new Cookie (name, value); } catch { return false; } return true; } #endregion #region Public Methods /// /// Determines whether the current cookie instance is equal to /// the specified instance. /// /// /// /// An instance to compare with /// the current cookie instance. /// /// /// An reference to a instance. /// /// /// /// true if the current cookie instance is equal to /// ; otherwise, false. /// public override bool Equals (object comparand) { var cookie = comparand as Cookie; if (cookie == null) return false; var caseSensitive = StringComparison.InvariantCulture; var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; return _name.Equals (cookie._name, caseInsensitive) && _value.Equals (cookie._value, caseSensitive) && _path.Equals (cookie._path, caseSensitive) && _domain.Equals (cookie._domain, caseInsensitive) && _version == cookie._version; } /// /// Gets a hash code for the current cookie instance. /// /// /// An that represents the hash code. /// public override int GetHashCode () { return hash ( StringComparer.InvariantCultureIgnoreCase.GetHashCode (_name), _value.GetHashCode (), _path.GetHashCode (), StringComparer.InvariantCultureIgnoreCase.GetHashCode (_domain), _version ); } /// /// Returns a string that represents the current cookie instance. /// /// /// A that is suitable for the Cookie request header. /// public override string ToString () { return ToRequestString (null); } #endregion } }