Cookie.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. #if !BESTHTTP_DISABLE_COOKIES
  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. /// <summary>
  72. /// SameSite prevents the browser from sending this cookie along with cross-site requests.
  73. /// The main goal is mitigate the risk of cross-origin information leakage.
  74. /// It also provides some protection against cross-site request forgery attacks. Possible values for the flag are lax or strict.
  75. /// <seealso cref="https://web.dev/samesite-cookies-explained/"/>
  76. /// </summary>
  77. public string SameSite { get; private set; }
  78. #endregion
  79. #region Public Constructors
  80. public Cookie(string name, string value)
  81. :this(name, value, "/", string.Empty)
  82. {}
  83. public Cookie(string name, string value, string path)
  84. : this(name, value, path, string.Empty)
  85. {}
  86. public Cookie(string name, string value, string path, string domain)
  87. :this() // call the parameter-less constructor to set default values
  88. {
  89. this.Name = name;
  90. this.Value = value;
  91. this.Path = path;
  92. this.Domain = domain;
  93. }
  94. public Cookie(Uri uri, string name, string value, DateTime expires, bool isSession = true)
  95. :this(name, value, uri.AbsolutePath, uri.Host)
  96. {
  97. this.Expires = expires;
  98. this.IsSession = isSession;
  99. this.Date = DateTime.UtcNow;
  100. }
  101. public Cookie(Uri uri, string name, string value, long maxAge = -1, bool isSession = true)
  102. :this(name, value, uri.AbsolutePath, uri.Host)
  103. {
  104. this.MaxAge = maxAge;
  105. this.IsSession = isSession;
  106. this.Date = DateTime.UtcNow;
  107. this.SameSite = "none";
  108. }
  109. #endregion
  110. internal Cookie()
  111. {
  112. // If a cookie has neither the Max-Age nor the Expires attribute, the user agent will retain the cookie
  113. // until "the current session is over" (as defined by the user agent).
  114. IsSession = true;
  115. MaxAge = -1;
  116. LastAccess = DateTime.UtcNow;
  117. }
  118. public bool WillExpireInTheFuture()
  119. {
  120. // 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
  121. if (IsSession)
  122. return true;
  123. // 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.
  124. return MaxAge != -1 ?
  125. Math.Max(0, (long)(DateTime.UtcNow - Date).TotalSeconds) < MaxAge :
  126. Expires > DateTime.UtcNow;
  127. }
  128. /// <summary>
  129. /// Guess the storage size of the cookie.
  130. /// </summary>
  131. /// <returns></returns>
  132. public uint GuessSize()
  133. {
  134. return (uint)((this.Name != null ? this.Name.Length * sizeof(char) : 0) +
  135. (this.Value != null ? this.Value.Length * sizeof(char) : 0) +
  136. (this.Domain != null ? this.Domain.Length * sizeof(char) : 0) +
  137. (this.Path != null ? this.Path.Length * sizeof(char) : 0) +
  138. (this.SameSite != null ? this.SameSite.Length * sizeof(char) : 0) +
  139. (sizeof(long) * 4) +
  140. (sizeof(bool) * 3));
  141. }
  142. public static Cookie Parse(string header, Uri defaultDomain, Logger.LoggingContext context)
  143. {
  144. Cookie cookie = new Cookie();
  145. try
  146. {
  147. var kvps = ParseCookieHeader(header);
  148. foreach (var kvp in kvps)
  149. {
  150. switch (kvp.Key.ToLowerInvariant())
  151. {
  152. case "path":
  153. // If the attribute-value is empty or if the first character of the attribute-value is not %x2F ("/"):
  154. // Let cookie-path be the default-path.
  155. cookie.Path = string.IsNullOrEmpty(kvp.Value) || !kvp.Value.StartsWith("/") ? "/" : cookie.Path = kvp.Value;
  156. break;
  157. case "domain":
  158. // If the attribute-value is empty, the behavior is undefined. However, the user agent SHOULD ignore the cookie-av entirely.
  159. if (string.IsNullOrEmpty(kvp.Value))
  160. return null;
  161. // If the first character of the attribute-value string is %x2E ("."):
  162. // Let cookie-domain be the attribute-value without the leading %x2E (".") character.
  163. cookie.Domain = kvp.Value.StartsWith(".") ? kvp.Value.Substring(1) : kvp.Value;
  164. break;
  165. case "expires":
  166. cookie.Expires = kvp.Value.ToDateTime(DateTime.FromBinary(0));
  167. cookie.IsSession = false;
  168. break;
  169. case "max-age":
  170. cookie.MaxAge = kvp.Value.ToInt64(-1);
  171. cookie.IsSession = false;
  172. break;
  173. case "secure":
  174. cookie.IsSecure = true;
  175. break;
  176. case "httponly":
  177. cookie.IsHttpOnly = true;
  178. break;
  179. case "samesite":
  180. cookie.SameSite = kvp.Value;
  181. break;
  182. default:
  183. // check whether name is already set to avoid overwriting it with a non-listed setting
  184. if (string.IsNullOrEmpty(cookie.Name))
  185. {
  186. cookie.Name = kvp.Key;
  187. cookie.Value = kvp.Value;
  188. }
  189. break;
  190. }
  191. }
  192. // Some user agents provide users the option of preventing persistent storage of cookies across sessions.
  193. // When configured thusly, user agents MUST treat all received cookies as if the persistent-flag were set to false.
  194. if (HTTPManager.EnablePrivateBrowsing)
  195. cookie.IsSession = true;
  196. // http://tools.ietf.org/html/rfc6265#section-4.1.2.3
  197. // WARNING: Some existing user agents treat an absent Domain attribute as if the Domain attribute were present and contained the current host name.
  198. // 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.
  199. if (string.IsNullOrEmpty(cookie.Domain))
  200. cookie.Domain = defaultDomain.Host;
  201. // http://tools.ietf.org/html/rfc6265#section-5.3 section 7:
  202. // If the cookie-attribute-list contains an attribute with an attribute-name of "Path",
  203. // set the cookie's path to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Path".
  204. // __Otherwise, set the cookie's path to the default-path of the request-uri.__
  205. if (string.IsNullOrEmpty(cookie.Path))
  206. cookie.Path = defaultDomain.AbsolutePath;
  207. cookie.Date = cookie.LastAccess = DateTime.UtcNow;
  208. }
  209. catch (Exception ex)
  210. {
  211. HTTPManager.Logger.Warning("Cookie", "Parse - Couldn't parse header: " + header + " exception: " + ex.ToString() + " " + ex.StackTrace, context);
  212. }
  213. return cookie;
  214. }
  215. #region Save & Load
  216. internal void SaveTo(BinaryWriter stream)
  217. {
  218. stream.Write(Version);
  219. stream.Write(Name ?? string.Empty);
  220. stream.Write(Value ?? string.Empty);
  221. stream.Write(Date.ToBinary());
  222. stream.Write(LastAccess.ToBinary());
  223. stream.Write(Expires.ToBinary());
  224. stream.Write(MaxAge);
  225. stream.Write(IsSession);
  226. stream.Write(Domain ?? string.Empty);
  227. stream.Write(Path ?? string.Empty);
  228. stream.Write(IsSecure);
  229. stream.Write(IsHttpOnly);
  230. }
  231. internal void LoadFrom(BinaryReader stream)
  232. {
  233. /*int version = */stream.ReadInt32();
  234. this.Name = stream.ReadString();
  235. this.Value = stream.ReadString();
  236. this.Date = DateTime.FromBinary(stream.ReadInt64());
  237. this.LastAccess = DateTime.FromBinary(stream.ReadInt64());
  238. this.Expires = DateTime.FromBinary(stream.ReadInt64());
  239. this.MaxAge = stream.ReadInt64();
  240. this.IsSession = stream.ReadBoolean();
  241. this.Domain = stream.ReadString();
  242. this.Path = stream.ReadString();
  243. this.IsSecure = stream.ReadBoolean();
  244. this.IsHttpOnly = stream.ReadBoolean();
  245. }
  246. #endregion
  247. #region Overrides and new Equals function
  248. public override string ToString()
  249. {
  250. return string.Concat(this.Name, "=", this.Value);
  251. }
  252. public override bool Equals(object obj)
  253. {
  254. if (obj == null)
  255. return false;
  256. return this.Equals(obj as Cookie);
  257. }
  258. public bool Equals(Cookie cookie)
  259. {
  260. if (cookie == null)
  261. return false;
  262. if (Object.ReferenceEquals(this, cookie))
  263. return true;
  264. return this.Name.Equals(cookie.Name, StringComparison.Ordinal) &&
  265. ((this.Domain == null && cookie.Domain == null) || this.Domain.Equals(cookie.Domain, StringComparison.Ordinal)) &&
  266. ((this.Path == null && cookie.Path == null) || this.Path.Equals(cookie.Path, StringComparison.Ordinal));
  267. }
  268. public override int GetHashCode()
  269. {
  270. return this.ToString().GetHashCode();
  271. }
  272. #endregion
  273. #region Private Helper Functions
  274. private static string ReadValue(string str, ref int pos)
  275. {
  276. string result = string.Empty;
  277. if (str == null)
  278. return result;
  279. return str.Read(ref pos, ';');
  280. }
  281. private static List<HeaderValue> ParseCookieHeader(string str)
  282. {
  283. List<HeaderValue> result = new List<HeaderValue>();
  284. if (str == null)
  285. return result;
  286. int idx = 0;
  287. // process the rest of the text
  288. while (idx < str.Length)
  289. {
  290. // Read key
  291. string key = str.Read(ref idx, (ch) => ch != '=' && ch != ';').Trim();
  292. HeaderValue qp = new HeaderValue(key);
  293. if (idx < str.Length && str[idx - 1] == '=')
  294. qp.Value = ReadValue(str, ref idx);
  295. result.Add(qp);
  296. }
  297. return result;
  298. }
  299. #endregion
  300. #region IComparable<Cookie> implementation
  301. public int CompareTo(Cookie other)
  302. {
  303. return this.LastAccess.CompareTo(other.LastAccess);
  304. }
  305. #endregion
  306. }
  307. }
  308. #endif