Cookie.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. #if !BESTHTTP_DISABLE_COOKIES && (!UNITY_WEBGL || UNITY_EDITOR)
  2. using System;
  3. using System.Collections.Generic;
  4. using BestHTTP.Extensions;
  5. using System.IO;
  6. namespace BestHTTP.Cookies
  7. {
  8. /// <summary>
  9. /// The Cookie implementation based on RFC 6265(http://tools.ietf.org/html/rfc6265).
  10. /// </summary>
  11. public sealed class Cookie : IComparable<Cookie>, IEquatable<Cookie>
  12. {
  13. private const int Version = 1;
  14. #region Public Properties
  15. /// <summary>
  16. /// The name of the cookie.
  17. /// </summary>
  18. public string Name { get; private set; }
  19. /// <summary>
  20. /// The value of the cookie.
  21. /// </summary>
  22. public string Value { get; private set; }
  23. /// <summary>
  24. /// The Date when the Cookie is registered.
  25. /// </summary>
  26. public DateTime Date { get; internal set; }
  27. /// <summary>
  28. /// When this Cookie last used in a request.
  29. /// </summary>
  30. public DateTime LastAccess { get; set; }
  31. /// <summary>
  32. /// The Expires attribute indicates the maximum lifetime of the cookie, represented as the date and time at which the cookie expires.
  33. /// The user agent is not required to retain the cookie until the specified date has passed.
  34. /// In fact, user agents often evict cookies due to memory pressure or privacy concerns.
  35. /// </summary>
  36. public DateTime Expires { get; private set; }
  37. /// <summary>
  38. /// The Max-Age attribute indicates the maximum lifetime of the cookie, represented as the number of seconds until the cookie expires.
  39. /// The user agent is not required to retain the cookie for the specified duration.
  40. /// In fact, user agents often evict cookies due to memory pressure or privacy concerns.
  41. /// </summary>
  42. public long MaxAge { get; private set; }
  43. /// <summary>
  44. /// If a cookie has neither the Max-Age nor the Expires attribute, the user agent will retain the cookie until "the current session is over".
  45. /// </summary>
  46. public bool IsSession { get; private set; }
  47. /// <summary>
  48. /// The Domain attribute specifies those hosts to which the cookie will be sent.
  49. /// For example, if the value of the Domain attribute is "example.com", the user agent will include the cookie
  50. /// in the Cookie header when making HTTP requests to example.com, www.example.com, and www.corp.example.com.
  51. /// If the server omits the Domain attribute, the user agent will return the cookie only to the origin server.
  52. /// </summary>
  53. public string Domain { get; private set; }
  54. /// <summary>
  55. /// The scope of each cookie is limited to a set of paths, controlled by the Path attribute.
  56. /// If the server omits the Path attribute, the user agent will use the "directory" of the request-uri's path component as the default value.
  57. /// </summary>
  58. public string Path { get; private set; }
  59. /// <summary>
  60. /// The Secure attribute limits the scope of the cookie to "secure" channels (where "secure" is defined by the user agent).
  61. /// When a cookie has the Secure attribute, the user agent will include the cookie in an HTTP request only if the request is
  62. /// transmitted over a secure channel (typically HTTP over Transport Layer Security (TLS)).
  63. /// </summary>
  64. public bool IsSecure { get; private set; }
  65. /// <summary>
  66. /// The HttpOnly attribute limits the scope of the cookie to HTTP requests.
  67. /// In particular, the attribute instructs the user agent to omit the cookie when providing access to
  68. /// cookies via "non-HTTP" APIs (such as a web browser API that exposes cookies to scripts).
  69. /// </summary>
  70. public bool IsHttpOnly { get; private set; }
  71. #endregion
  72. #region Public Constructors
  73. public Cookie(string name, string value)
  74. :this(name, value, "/", string.Empty)
  75. {}
  76. public Cookie(string name, string value, string path)
  77. : this(name, value, path, string.Empty)
  78. {}
  79. public Cookie(string name, string value, string path, string domain)
  80. :this() // call the parameter-less constructor to set default values
  81. {
  82. this.Name = name;
  83. this.Value = value;
  84. this.Path = path;
  85. this.Domain = domain;
  86. }
  87. public Cookie(Uri uri, string name, string value, DateTime expires, bool isSession = true)
  88. :this(name, value, uri.AbsolutePath, uri.Host)
  89. {
  90. this.Expires = expires;
  91. this.IsSession = isSession;
  92. this.Date = DateTime.UtcNow;
  93. }
  94. public Cookie(Uri uri, string name, string value, long maxAge = -1, bool isSession = true)
  95. :this(name, value, uri.AbsolutePath, uri.Host)
  96. {
  97. this.MaxAge = maxAge;
  98. this.IsSession = isSession;
  99. this.Date = DateTime.UtcNow;
  100. }
  101. #endregion
  102. internal Cookie()
  103. {
  104. // If a cookie has neither the Max-Age nor the Expires attribute, the user agent will retain the cookie
  105. // until "the current session is over" (as defined by the user agent).
  106. IsSession = true;
  107. MaxAge = -1;
  108. LastAccess = DateTime.UtcNow;
  109. }
  110. public bool WillExpireInTheFuture()
  111. {
  112. // No Expires or Max-Age value sent from the server, we will fake the return value so we will not delete the newly came Cookie
  113. if (IsSession)
  114. return true;
  115. // If a cookie has both the Max-Age and the Expires attribute, the Max-Age attribute has precedence and controls the expiration date of the cookie.
  116. return MaxAge != -1 ?
  117. Math.Max(0, (long)(DateTime.UtcNow - Date).TotalSeconds) < MaxAge :
  118. Expires > DateTime.UtcNow;
  119. }
  120. /// <summary>
  121. /// Guess the storage size of the cookie.
  122. /// </summary>
  123. /// <returns></returns>
  124. public uint GuessSize()
  125. {
  126. return (uint)((Name != null ? Name.Length * sizeof(char) : 0) +
  127. (Value != null ? Value.Length * sizeof(char) : 0) +
  128. (Domain != null ? Domain.Length * sizeof(char) : 0) +
  129. (Path != null ? Path.Length * sizeof(char) : 0) +
  130. (sizeof(long) * 4) +
  131. (sizeof(bool) * 3));
  132. }
  133. public static Cookie Parse(string header, Uri defaultDomain)
  134. {
  135. Cookie cookie = new Cookie();
  136. try
  137. {
  138. var kvps = ParseCookieHeader(header);
  139. foreach (var kvp in kvps)
  140. {
  141. switch (kvp.Key.ToLowerInvariant())
  142. {
  143. case "path":
  144. // If the attribute-value is empty or if the first character of the attribute-value is not %x2F ("/"):
  145. // Let cookie-path be the default-path.
  146. cookie.Path = string.IsNullOrEmpty(kvp.Value) || !kvp.Value.StartsWith("/") ? "/" : cookie.Path = kvp.Value;
  147. break;
  148. case "domain":
  149. // If the attribute-value is empty, the behavior is undefined. However, the user agent SHOULD ignore the cookie-av entirely.
  150. if (string.IsNullOrEmpty(kvp.Value))
  151. return null;
  152. // If the first character of the attribute-value string is %x2E ("."):
  153. // Let cookie-domain be the attribute-value without the leading %x2E (".") character.
  154. cookie.Domain = kvp.Value.StartsWith(".") ? kvp.Value.Substring(1) : kvp.Value;
  155. break;
  156. case "expires":
  157. cookie.Expires = kvp.Value.ToDateTime(DateTime.FromBinary(0));
  158. cookie.IsSession = false;
  159. break;
  160. case "max-age":
  161. cookie.MaxAge = kvp.Value.ToInt64(-1);
  162. cookie.IsSession = false;
  163. break;
  164. case "secure":
  165. cookie.IsSecure = true;
  166. break;
  167. case "httponly":
  168. cookie.IsHttpOnly = true;
  169. break;
  170. default:
  171. cookie.Name = kvp.Key;
  172. cookie.Value = kvp.Value;
  173. break;
  174. }
  175. }
  176. // Some user agents provide users the option of preventing persistent storage of cookies across sessions.
  177. // When configured thusly, user agents MUST treat all received cookies as if the persistent-flag were set to false.
  178. if (HTTPManager.EnablePrivateBrowsing)
  179. cookie.IsSession = true;
  180. // http://tools.ietf.org/html/rfc6265#section-4.1.2.3
  181. // WARNING: Some existing user agents treat an absent Domain attribute as if the Domain attribute were present and contained the current host name.
  182. // For example, if example.com returns a Set-Cookie header without a Domain attribute, these user agents will erroneously send the cookie to www.example.com as well.
  183. if (string.IsNullOrEmpty(cookie.Domain))
  184. cookie.Domain = defaultDomain.Host;
  185. // http://tools.ietf.org/html/rfc6265#section-5.3 section 7:
  186. // If the cookie-attribute-list contains an attribute with an attribute-name of "Path",
  187. // set the cookie's path to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Path".
  188. // __Otherwise, set the cookie's path to the default-path of the request-uri.__
  189. if (string.IsNullOrEmpty(cookie.Path))
  190. cookie.Path = defaultDomain.AbsolutePath;
  191. cookie.Date = cookie.LastAccess = DateTime.UtcNow;
  192. }
  193. catch
  194. {
  195. }
  196. return cookie;
  197. }
  198. #region Save & Load
  199. internal void SaveTo(BinaryWriter stream)
  200. {
  201. stream.Write(Version);
  202. stream.Write(Name ?? string.Empty);
  203. stream.Write(Value ?? string.Empty);
  204. stream.Write(Date.ToBinary());
  205. stream.Write(LastAccess.ToBinary());
  206. stream.Write(Expires.ToBinary());
  207. stream.Write(MaxAge);
  208. stream.Write(IsSession);
  209. stream.Write(Domain ?? string.Empty);
  210. stream.Write(Path ?? string.Empty);
  211. stream.Write(IsSecure);
  212. stream.Write(IsHttpOnly);
  213. }
  214. internal void LoadFrom(BinaryReader stream)
  215. {
  216. /*int version = */stream.ReadInt32();
  217. this.Name = stream.ReadString();
  218. this.Value = stream.ReadString();
  219. this.Date = DateTime.FromBinary(stream.ReadInt64());
  220. this.LastAccess = DateTime.FromBinary(stream.ReadInt64());
  221. this.Expires = DateTime.FromBinary(stream.ReadInt64());
  222. this.MaxAge = stream.ReadInt64();
  223. this.IsSession = stream.ReadBoolean();
  224. this.Domain = stream.ReadString();
  225. this.Path = stream.ReadString();
  226. this.IsSecure = stream.ReadBoolean();
  227. this.IsHttpOnly = stream.ReadBoolean();
  228. }
  229. #endregion
  230. #region Overrides and new Equals function
  231. public override string ToString()
  232. {
  233. return string.Concat(this.Name, "=", this.Value);
  234. }
  235. public override bool Equals(object obj)
  236. {
  237. if (obj == null)
  238. return false;
  239. return this.Equals(obj as Cookie);
  240. }
  241. public bool Equals(Cookie cookie)
  242. {
  243. if (cookie == null)
  244. return false;
  245. if (Object.ReferenceEquals(this, cookie))
  246. return true;
  247. return this.Name.Equals(cookie.Name, StringComparison.Ordinal) &&
  248. ((this.Domain == null && cookie.Domain == null) || this.Domain.Equals(cookie.Domain, StringComparison.Ordinal)) &&
  249. ((this.Path == null && cookie.Path == null) || this.Path.Equals(cookie.Path, StringComparison.Ordinal));
  250. }
  251. public override int GetHashCode()
  252. {
  253. return this.ToString().GetHashCode();
  254. }
  255. #endregion
  256. #region Private Helper Functions
  257. private static string ReadValue(string str, ref int pos)
  258. {
  259. string result = string.Empty;
  260. if (str == null)
  261. return result;
  262. return str.Read(ref pos, ';');
  263. }
  264. private static List<HeaderValue> ParseCookieHeader(string str)
  265. {
  266. List<HeaderValue> result = new List<HeaderValue>();
  267. if (str == null)
  268. return result;
  269. int idx = 0;
  270. // process the rest of the text
  271. while (idx < str.Length)
  272. {
  273. // Read key
  274. string key = str.Read(ref idx, (ch) => ch != '=' && ch != ';').Trim();
  275. HeaderValue qp = new HeaderValue(key);
  276. if (idx < str.Length && str[idx - 1] == '=')
  277. qp.Value = ReadValue(str, ref idx);
  278. result.Add(qp);
  279. }
  280. return result;
  281. }
  282. #endregion
  283. #region IComparable<Cookie> implementation
  284. public int CompareTo(Cookie other)
  285. {
  286. return this.LastAccess.CompareTo(other.LastAccess);
  287. }
  288. #endregion
  289. }
  290. }
  291. #endif