CookieJar.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Threading;
  4. using Best.HTTP.Hosts.Connections;
  5. using Best.HTTP.Shared;
  6. using Best.HTTP.Shared.Extensions;
  7. using Best.HTTP.Shared.PlatformSupport.FileSystem;
  8. namespace Best.HTTP.Cookies
  9. {
  10. /// <summary>
  11. /// The Cookie Jar implementation based on <see href="http://tools.ietf.org/html/rfc6265">RFC 6265</see>.
  12. /// </summary>
  13. public static class CookieJar
  14. {
  15. /// <summary>
  16. /// Maximum size of the Cookie Jar in bytes. It's default value is 10485760 (10 MB).
  17. /// </summary>
  18. public static uint MaximumSize { get; set; } = 10 * 1024 * 1024;
  19. // Version of the cookie store. It may be used in a future version for maintaining compatibility.
  20. private const int Version = 1;
  21. /// <summary>
  22. /// Returns true if File apis are supported.
  23. /// </summary>
  24. public static bool IsSavingSupported
  25. {
  26. get
  27. {
  28. if (IsSupportCheckDone)
  29. return _isSavingSupported;
  30. try
  31. {
  32. #if UNITY_WEBGL && !UNITY_EDITOR
  33. _isSavingSupported = false;
  34. #else
  35. HTTPManager.IOService.DirectoryExists(HTTPManager.GetRootSaveFolder());
  36. _isSavingSupported = true;
  37. #endif
  38. }
  39. catch
  40. {
  41. _isSavingSupported = false;
  42. HTTPManager.Logger.Warning("CookieJar", "Cookie saving and loading disabled!");
  43. }
  44. finally
  45. {
  46. IsSupportCheckDone = true;
  47. }
  48. return _isSavingSupported;
  49. }
  50. }
  51. /// <summary>
  52. /// The plugin will delete cookies that are accessed this threshold ago. Its default value is 7 days.
  53. /// </summary>
  54. public static TimeSpan AccessThreshold = TimeSpan.FromDays(7);
  55. /// <summary>
  56. /// If this property is set to <c>true</c>, then new cookies treated as session cookies and these cookies are not saved to disk. Its default value is <c>false</c>.
  57. /// </summary>
  58. public static bool IsSessionOverride = false;
  59. /// <summary>
  60. /// Enabled or disables storing and handling of cookies. If set to false, <see cref="HTTPRequest">HTTPRequests</see> aren't searched for cookies and no cookies will be set for <see cref="HTTPResponse"/>s.
  61. /// </summary>
  62. public static bool IsEnabled = true;
  63. #region Privates
  64. /// <summary>
  65. /// List of the Cookies
  66. /// </summary>
  67. private static List<Cookie> Cookies = new List<Cookie>();
  68. private static string CookieFolder { get; set; }
  69. private static string LibraryPath { get; set; }
  70. /// <summary>
  71. /// Synchronization object for thread safety.
  72. /// </summary>
  73. private static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
  74. private static bool _isSavingSupported;
  75. private static bool IsSupportCheckDone;
  76. private static bool Loaded;
  77. private static RunOnceOnMainThread _saveLibraryRunner = new RunOnceOnMainThread(Persist, null);
  78. #endregion
  79. #region Internal Functions
  80. internal static void SetupFolder()
  81. {
  82. if (!CookieJar.IsSavingSupported)
  83. return;
  84. try
  85. {
  86. if (string.IsNullOrEmpty(CookieFolder) || string.IsNullOrEmpty(LibraryPath))
  87. {
  88. CookieFolder = System.IO.Path.Combine(HTTPManager.GetRootSaveFolder(), "Cookies");
  89. LibraryPath = System.IO.Path.Combine(CookieFolder, "Library");
  90. }
  91. }
  92. catch
  93. { }
  94. }
  95. /// <summary>
  96. /// Will set or update all cookies from the response object.
  97. /// </summary>
  98. internal static bool SetFromRequest(HTTPResponse response)
  99. {
  100. if (response == null || !IsEnabled)
  101. return false;
  102. List<Cookie> newCookies = new List<Cookie>();
  103. var setCookieHeaders = response.GetHeaderValues("set-cookie");
  104. // No cookies. :'(
  105. if (setCookieHeaders == null)
  106. return false;
  107. foreach (var cookieHeader in setCookieHeaders)
  108. {
  109. Cookie cookie = Cookie.Parse(cookieHeader, response.Request.CurrentUri, response.Request.Context);
  110. if (cookie != null)
  111. {
  112. rwLock.EnterWriteLock();
  113. try
  114. {
  115. int idx;
  116. var old = Find(cookie, out idx);
  117. // if no value for the cookie or already expired then the server asked us to delete the cookie
  118. bool expired = string.IsNullOrEmpty(cookie.Value) || !cookie.WillExpireInTheFuture();
  119. if (!expired)
  120. {
  121. // no old cookie, add it straight to the list
  122. if (old == null)
  123. {
  124. Cookies.Add(cookie);
  125. newCookies.Add(cookie);
  126. }
  127. else
  128. {
  129. // Update the creation-time of the newly created cookie to match the creation-time of the old-cookie.
  130. cookie.Date = old.Date;
  131. Cookies[idx] = cookie;
  132. newCookies.Add(cookie);
  133. }
  134. }
  135. else if (idx != -1) // delete the cookie
  136. Cookies.RemoveAt(idx);
  137. }
  138. catch
  139. {
  140. // Ignore cookie on error
  141. }
  142. finally
  143. {
  144. rwLock.ExitWriteLock();
  145. }
  146. }
  147. }
  148. _saveLibraryRunner.Subscribe();
  149. return true;
  150. }
  151. internal static void SetupRequest(HTTPRequest request)
  152. {
  153. if (!IsEnabled)
  154. return;
  155. // Cookies
  156. // User added cookies are sent even when IsCookiesEnabled is set to false
  157. List<Cookie> cookies = CookieJar.Get(request.CurrentUri);
  158. // http://tools.ietf.org/html/rfc6265#section-5.4
  159. // -When the user agent generates an HTTP request, the user agent MUST NOT attach more than one Cookie header field.
  160. if (cookies != null && cookies.Count > 0)
  161. {
  162. // Room for improvement:
  163. // 2. The user agent SHOULD sort the cookie-list in the following order:
  164. // * Cookies with longer paths are listed before cookies with shorter paths.
  165. // * Among cookies that have equal-length path fields, cookies with earlier creation-times are listed before cookies with later creation-times.
  166. bool first = true;
  167. string cookieStr = string.Empty;
  168. bool isSecureProtocolInUse = HTTPProtocolFactory.IsSecureProtocol(request.CurrentUri);
  169. foreach (var cookie in cookies)
  170. if (!cookie.IsSecure || (cookie.IsSecure && isSecureProtocolInUse))
  171. {
  172. if (!first)
  173. cookieStr += "; ";
  174. else
  175. first = false;
  176. cookieStr += cookie.ToString();
  177. // 3. Update the last-access-time of each cookie in the cookie-list to the current date and time.
  178. cookie.LastAccess = DateTime.Now;
  179. }
  180. if (!string.IsNullOrEmpty(cookieStr))
  181. request.SetHeader("Cookie", cookieStr);
  182. }
  183. }
  184. /// <summary>
  185. /// Deletes all expired or 'old' cookies, and will keep the sum size of cookies under the given size.
  186. /// </summary>
  187. internal static void Maintain(bool sendEvent)
  188. {
  189. // It's not the same as in the rfc:
  190. // http://tools.ietf.org/html/rfc6265#section-5.3
  191. rwLock.EnterWriteLock();
  192. try
  193. {
  194. uint size = 0;
  195. for (int i = 0; i < Cookies.Count; )
  196. {
  197. var cookie = Cookies[i];
  198. // Remove expired or not used cookies
  199. if (!cookie.WillExpireInTheFuture() || (cookie.LastAccess + AccessThreshold) < DateTime.Now)
  200. {
  201. Cookies.RemoveAt(i);
  202. }
  203. else
  204. {
  205. if (!cookie.IsSession)
  206. size += cookie.GuessSize();
  207. i++;
  208. }
  209. }
  210. if (size > MaximumSize)
  211. {
  212. Cookies.Sort();
  213. while (size > MaximumSize && Cookies.Count > 0)
  214. {
  215. var cookie = Cookies[0];
  216. Cookies.RemoveAt(0);
  217. size -= cookie.GuessSize();
  218. }
  219. }
  220. }
  221. catch
  222. { }
  223. finally
  224. {
  225. rwLock.ExitWriteLock();
  226. }
  227. if (sendEvent)
  228. _saveLibraryRunner.Subscribe();
  229. }
  230. /// <summary>
  231. /// Saves the Cookie Jar to a file.
  232. /// </summary>
  233. /// <remarks>Not implemented under Unity WebPlayer</remarks>
  234. internal static void Persist()
  235. {
  236. if (!IsSavingSupported)
  237. return;
  238. if (!Loaded)
  239. return;
  240. if (!IsEnabled)
  241. return;
  242. // Delete any expired cookie
  243. Maintain(false);
  244. rwLock.EnterWriteLock();
  245. try
  246. {
  247. if (!HTTPManager.IOService.DirectoryExists(CookieFolder))
  248. HTTPManager.IOService.DirectoryCreate(CookieFolder);
  249. using (var fs = HTTPManager.IOService.CreateFileStream(LibraryPath, FileStreamModes.Create))
  250. using (var bw = new System.IO.BinaryWriter(fs))
  251. {
  252. bw.Write(Version);
  253. // Count how many non-session cookies we have
  254. int count = 0;
  255. foreach (var cookie in Cookies)
  256. if (!cookie.IsSession)
  257. count++;
  258. bw.Write(count);
  259. // Save only the persistable cookies
  260. foreach (var cookie in Cookies)
  261. if (!cookie.IsSession)
  262. cookie.SaveTo(bw);
  263. }
  264. }
  265. catch
  266. { }
  267. finally
  268. {
  269. rwLock.ExitWriteLock();
  270. }
  271. }
  272. /// <summary>
  273. /// Load previously persisted cookie library from the file.
  274. /// </summary>
  275. internal static void Load()
  276. {
  277. if (!IsSavingSupported)
  278. return;
  279. if (Loaded)
  280. return;
  281. if (!IsEnabled)
  282. return;
  283. SetupFolder();
  284. rwLock.EnterWriteLock();
  285. try
  286. {
  287. Cookies.Clear();
  288. if (!HTTPManager.IOService.DirectoryExists(CookieFolder))
  289. HTTPManager.IOService.DirectoryCreate(CookieFolder);
  290. if (!HTTPManager.IOService.FileExists(LibraryPath))
  291. return;
  292. using (var fs = HTTPManager.IOService.CreateFileStream(LibraryPath, FileStreamModes.OpenRead))
  293. using (var br = new System.IO.BinaryReader(fs))
  294. {
  295. /*int version = */br.ReadInt32();
  296. int cookieCount = br.ReadInt32();
  297. for (int i = 0; i < cookieCount; ++i)
  298. {
  299. Cookie cookie = new Cookie();
  300. cookie.LoadFrom(br);
  301. if (cookie.WillExpireInTheFuture())
  302. Cookies.Add(cookie);
  303. }
  304. }
  305. }
  306. catch
  307. {
  308. Cookies.Clear();
  309. }
  310. finally
  311. {
  312. Loaded = true;
  313. rwLock.ExitWriteLock();
  314. }
  315. }
  316. #endregion
  317. #region Public Functions
  318. /// <summary>
  319. /// Returns all Cookies that corresponds to the given Uri.
  320. /// </summary>
  321. public static List<Cookie> Get(Uri uri)
  322. {
  323. Load();
  324. rwLock.EnterReadLock();
  325. try
  326. {
  327. List<Cookie> result = null;
  328. for (int i = 0; i < Cookies.Count; ++i)
  329. {
  330. Cookie cookie = Cookies[i];
  331. if (cookie == null)
  332. continue;
  333. bool willExpireInTheFuture = cookie.WillExpireInTheFuture();
  334. bool domainMatch = uri.Host.IndexOf(cookie.Domain) != -1 || string.Format("{0}:{1}", uri.Host, uri.Port).IndexOf(cookie.Domain) != -1;
  335. if (willExpireInTheFuture && domainMatch)
  336. {
  337. string requestPath = uri.AbsolutePath;
  338. // https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
  339. // A request-path path-matches a given cookie-path if at least one of
  340. // the following conditions holds:
  341. // o The cookie-path and the request-path are identical.
  342. bool exactMatch = cookie.Path.Equals(requestPath, StringComparison.Ordinal);
  343. // o The cookie-path is a prefix of the request-path, and the last
  344. // character of the cookie-path is %x2F ("/").
  345. bool prefixMatch = cookie.Path[cookie.Path.Length - 1] == '/' && requestPath.StartsWith(cookie.Path, StringComparison.Ordinal);
  346. // o The cookie-path is a prefix of the request-path, and the first
  347. // character of the request-path that is not included in the cookie-
  348. // path is a %x2F ("/") character.
  349. bool prefixMatch2 = requestPath.Length > cookie.Path.Length &&
  350. requestPath.StartsWith(cookie.Path, StringComparison.Ordinal) &&
  351. requestPath[cookie.Path.Length] == '/';
  352. if (exactMatch || prefixMatch || prefixMatch2)
  353. {
  354. if (result == null)
  355. result = new List<Cookie>();
  356. result.Add(cookie);
  357. }
  358. }
  359. }
  360. return result;
  361. }
  362. finally
  363. {
  364. rwLock.ExitReadLock();
  365. }
  366. }
  367. /// <summary>
  368. /// Will add a new, or overwrite an old cookie if already exists.
  369. /// </summary>
  370. public static void Set(Uri uri, Cookie cookie)
  371. {
  372. cookie.Domain = uri.Host;
  373. cookie.Path = uri.AbsolutePath;
  374. Set(cookie);
  375. }
  376. /// <summary>
  377. /// Will add a new, or overwrite an old cookie if already exists.
  378. /// </summary>
  379. public static void Set(Cookie cookie)
  380. {
  381. Load();
  382. rwLock.EnterWriteLock();
  383. try
  384. {
  385. int idx;
  386. Find(cookie, out idx);
  387. if (idx >= 0)
  388. Cookies[idx] = cookie;
  389. else
  390. Cookies.Add(cookie);
  391. }
  392. finally
  393. {
  394. rwLock.ExitWriteLock();
  395. }
  396. _saveLibraryRunner.Subscribe();
  397. }
  398. public static List<Cookie> GetAll()
  399. {
  400. Load();
  401. return Cookies;
  402. }
  403. /// <summary>
  404. /// Deletes all cookies from the Jar.
  405. /// </summary>
  406. public static void Clear()
  407. {
  408. Load();
  409. rwLock.EnterWriteLock();
  410. try
  411. {
  412. Cookies.Clear();
  413. }
  414. finally
  415. {
  416. rwLock.ExitWriteLock();
  417. }
  418. Persist();
  419. }
  420. /// <summary>
  421. /// Removes cookies that older than the given parameter.
  422. /// </summary>
  423. public static void Clear(TimeSpan olderThan)
  424. {
  425. Load();
  426. rwLock.EnterWriteLock();
  427. try
  428. {
  429. for (int i = 0; i < Cookies.Count; )
  430. {
  431. var cookie = Cookies[i];
  432. // Remove expired or not used cookies
  433. if (!cookie.WillExpireInTheFuture() || (cookie.Date + olderThan) < DateTime.Now)
  434. Cookies.RemoveAt(i);
  435. else
  436. i++;
  437. }
  438. }
  439. finally
  440. {
  441. rwLock.ExitWriteLock();
  442. }
  443. Persist();
  444. }
  445. /// <summary>
  446. /// Removes cookies that matches to the given domain.
  447. /// </summary>
  448. public static void Clear(string domain)
  449. {
  450. Load();
  451. rwLock.EnterWriteLock();
  452. try
  453. {
  454. for (int i = 0; i < Cookies.Count; )
  455. {
  456. var cookie = Cookies[i];
  457. // Remove expired or not used cookies
  458. if (!cookie.WillExpireInTheFuture() || cookie.Domain.IndexOf(domain) != -1)
  459. Cookies.RemoveAt(i);
  460. else
  461. i++;
  462. }
  463. }
  464. finally
  465. {
  466. rwLock.ExitWriteLock();
  467. }
  468. Persist();
  469. }
  470. public static void Remove(Uri uri, string name)
  471. {
  472. Load();
  473. rwLock.EnterWriteLock();
  474. try
  475. {
  476. for (int i = 0; i < Cookies.Count; )
  477. {
  478. var cookie = Cookies[i];
  479. if (cookie.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && uri.Host.IndexOf(cookie.Domain) != -1)
  480. Cookies.RemoveAt(i);
  481. else
  482. i++;
  483. }
  484. }
  485. finally
  486. {
  487. rwLock.ExitWriteLock();
  488. }
  489. _saveLibraryRunner.Subscribe();
  490. }
  491. #endregion
  492. #region Private Helper Functions
  493. /// <summary>
  494. /// Find and return a Cookie and his index in the list.
  495. /// </summary>
  496. private static Cookie Find(Cookie cookie, out int idx)
  497. {
  498. for (int i = 0; i < Cookies.Count; ++i)
  499. {
  500. Cookie c = Cookies[i];
  501. if (c.Equals(cookie))
  502. {
  503. idx = i;
  504. return c;
  505. }
  506. }
  507. idx = -1;
  508. return null;
  509. }
  510. #endregion
  511. }
  512. }