cookies.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. /*******************************************************************************
  2. ηMatrix - a browser extension to black/white list requests.
  3. Copyright (C) 2013-2019 Raymond Hill
  4. Copyright (C) 2019-2022 Alessio Vanni
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with this program. If not, see {http://www.gnu.org/licenses/}.
  15. Home: https://gitlab.com/vannilla/ematrix
  16. uMatrix Home: https://github.com/gorhill/uMatrix
  17. */
  18. // rhill 2013-12-14: the whole cookie management has been rewritten so as
  19. // to avoid having to call chrome API whenever a single cookie changes, and
  20. // to record cookie for a web page *only* when its value changes.
  21. // https://github.com/gorhill/httpswitchboard/issues/79
  22. "use strict";
  23. // Isolate from global namespace
  24. // Use cached-context approach rather than object-based approach, as details
  25. // of the implementation do not need to be visible
  26. ηMatrix.cookieHunter = (function () {
  27. Cu.import('chrome://ematrix/content/lib/UriTools.jsm');
  28. Cu.import('chrome://ematrix/content/lib/CookieCache.jsm');
  29. let ηm = ηMatrix;
  30. let recordPageCookiesQueue = new Map();
  31. let removePageCookiesQueue = new Map();
  32. let removeCookieQueue = new Set();
  33. let processRemoveQueuePeriod = 2 * 60 * 1000;
  34. let processCleanPeriod = 10 * 60 * 1000;
  35. let processPageRecordQueueTimer = null;
  36. let processPageRemoveQueueTimer = null;
  37. // Look for cookies to record for a specific web page
  38. let recordPageCookiesAsync = function (pageStats) {
  39. // Store the page stats objects so that it doesn't go away
  40. // before we handle the job.
  41. // rhill 2013-10-19: pageStats could be nil, for example, this can
  42. // happens if a file:// ... makes an xmlHttpRequest
  43. if (!pageStats) {
  44. return;
  45. }
  46. recordPageCookiesQueue.set(pageStats.pageUrl, pageStats);
  47. if (processPageRecordQueueTimer === null) {
  48. processPageRecordQueueTimer =
  49. vAPI.setTimeout(processPageRecordQueue, 1000);
  50. }
  51. };
  52. let cookieLogEntryBuilder = [
  53. '',
  54. '{',
  55. '',
  56. '-cookie:',
  57. '',
  58. '}'
  59. ];
  60. let recordPageCookie = function (pageStore, key) {
  61. if (vAPI.isBehindTheSceneTabId(pageStore.tabId)) {
  62. return;
  63. }
  64. let entry = CookieCache.get(key);
  65. let pageHostname = pageStore.pageHostname;
  66. let block = ηm.mustBlock(pageHostname, entry.hostname, 'cookie');
  67. cookieLogEntryBuilder[0] = CookieUtils.urlFromEntry(entry);
  68. cookieLogEntryBuilder[2] = entry.session ? 'session' : 'persistent';
  69. cookieLogEntryBuilder[4] = encodeURIComponent(entry.name);
  70. let cookieURL = cookieLogEntryBuilder.join('');
  71. // rhill 2013-11-20:
  72. // https://github.com/gorhill/httpswitchboard/issues/60
  73. // Need to URL-encode cookie name
  74. pageStore.recordRequest('cookie', cookieURL, block);
  75. ηm.logger.writeOne(pageStore.tabId, 'net',
  76. pageHostname, cookieURL, 'cookie', block);
  77. entry.usedOn.add(pageHostname);
  78. // rhill 2013-11-21:
  79. // https://github.com/gorhill/httpswitchboard/issues/65
  80. // Leave alone cookies from behind-the-scene requests if
  81. // behind-the-scene processing is disabled.
  82. if (!block) {
  83. return;
  84. }
  85. if (!ηm.userSettings.deleteCookies) {
  86. return;
  87. }
  88. removeCookieAsync(key);
  89. };
  90. // Look for cookies to potentially remove for a specific web page
  91. let removePageCookiesAsync = function (pageStats) {
  92. // Hold onto pageStats objects so that it doesn't go away
  93. // before we handle the job.
  94. // rhill 2013-10-19: pageStats could be nil, for example, this can
  95. // happens if a file:// ... makes an xmlHttpRequest
  96. if (!pageStats) {
  97. return;
  98. }
  99. removePageCookiesQueue.set(pageStats.pageUrl, pageStats);
  100. if (processPageRemoveQueueTimer === null) {
  101. processPageRemoveQueueTimer =
  102. vAPI.setTimeout(processPageRemoveQueue, 15 * 1000);
  103. }
  104. };
  105. // Candidate for removal
  106. let removeCookieAsync = function (key) {
  107. removeCookieQueue.add(key);
  108. };
  109. let chromeCookieRemove = function (entry, name) {
  110. let url = CookieUtils.urlFromEntry(entry);
  111. if (url === '') {
  112. return;
  113. }
  114. let sessionKey = CookieUtils.keyFromURL(UriTools.set(url),
  115. 'session', name);
  116. let persistKey = CookieUtils.keyFromURL(UriTools.set(url),
  117. 'persistent', name);
  118. let callback = function(details) {
  119. let success = !!details;
  120. let template = success ?
  121. i18nCookieDeleteSuccess :
  122. i18nCookieDeleteFailure;
  123. if (CookieCache.remove(sessionKey)) {
  124. if (success) {
  125. ηm.cookieRemovedCounter += 1;
  126. }
  127. ηm.logger.writeOne('', 'info', 'cookie',
  128. template.replace('{{value}}',
  129. sessionKey));
  130. }
  131. if (CookieCache.remove(persistKey)) {
  132. if (success) {
  133. ηm.cookieRemovedCounter += 1;
  134. }
  135. ηm.logger.writeOne('', 'info', 'cookie',
  136. template.replace('{{value}}',
  137. persistKey));
  138. }
  139. };
  140. vAPI.cookies.remove({
  141. url: url, name: name
  142. }, callback);
  143. };
  144. let i18nCookieDeleteSuccess = vAPI.i18n('loggerEntryCookieDeleted');
  145. let i18nCookieDeleteFailure = vAPI.i18n('loggerEntryDeleteCookieError');
  146. let processPageRecordQueue = function () {
  147. processPageRecordQueueTimer = null;
  148. for (let pageStore of recordPageCookiesQueue.values()) {
  149. findAndRecordPageCookies(pageStore);
  150. }
  151. recordPageCookiesQueue.clear();
  152. };
  153. let processPageRemoveQueue = function () {
  154. processPageRemoveQueueTimer = null;
  155. for (let pageStore of removePageCookiesQueue.values()) {
  156. findAndRemovePageCookies(pageStore);
  157. }
  158. removePageCookiesQueue.clear();
  159. };
  160. // Effectively remove cookies.
  161. let processRemoveQueue = function () {
  162. let userSettings = ηm.userSettings;
  163. let deleteCookies = userSettings.deleteCookies;
  164. // Session cookies which timestamp is *after* tstampObsolete will
  165. // be left untouched
  166. // https://github.com/gorhill/httpswitchboard/issues/257
  167. let dusc = userSettings.deleteUnusedSessionCookies;
  168. let dusca = userSettings.deleteUnusedSessionCookiesAfter;
  169. let tstampObsolete = dusc ?
  170. Date.now() - dusca * 60 * 1000 :
  171. 0;
  172. let srcHostnames;
  173. let entry;
  174. for (let key of removeCookieQueue) {
  175. // rhill 2014-05-12: Apparently this can happen. I have to
  176. // investigate how (A session cookie has same name as a
  177. // persistent cookie?)
  178. entry = CookieCache.get(key);
  179. if (entry === undefined) {
  180. continue;
  181. }
  182. // Delete obsolete session cookies: enabled.
  183. if (tstampObsolete !== 0 && entry.session) {
  184. if (entry.tstamp < tstampObsolete) {
  185. chromeCookieRemove(entry, entry.name);
  186. continue;
  187. }
  188. }
  189. // Delete all blocked cookies: disabled.
  190. if (deleteCookies === false) {
  191. continue;
  192. }
  193. // Query scopes only if we are going to use them
  194. if (srcHostnames === undefined) {
  195. srcHostnames = ηm.tMatrix.extractAllSourceHostnames();
  196. }
  197. // Ensure cookie is not allowed on ALL current web pages: It can
  198. // happen that a cookie is blacklisted on one web page while
  199. // being whitelisted on another (because of per-page permissions).
  200. if (canRemoveCookie(key, srcHostnames)) {
  201. chromeCookieRemove(entry, entry.name);
  202. }
  203. }
  204. removeCookieQueue.clear();
  205. vAPI.setTimeout(processRemoveQueue, processRemoveQueuePeriod);
  206. };
  207. // Once in a while, we go ahead and clean everything that might have been
  208. // left behind.
  209. // Remove only some of the cookies which are candidate for removal: who knows,
  210. // maybe a user has 1000s of cookies sitting in his browser...
  211. let processClean = function () {
  212. let us = ηm.userSettings;
  213. if (us.deleteCookies || us.deleteUnusedSessionCookies) {
  214. let keys = Array.from(CookieCache.keys());
  215. let len = keys.length;
  216. let step, offset, n;
  217. if (len > 25) {
  218. step = len / 25;
  219. offset = Math.floor(Math.random() * len);
  220. n = 25;
  221. } else {
  222. step = 1;
  223. offset = 0;
  224. n = len;
  225. }
  226. let i = offset;
  227. while (n--) {
  228. removeCookieAsync(keys[Math.floor(i % len)]);
  229. i += step;
  230. }
  231. }
  232. vAPI.setTimeout(processClean, processCleanPeriod);
  233. };
  234. let findAndRecordPageCookies = function (pageStore) {
  235. for (let key of CookieCache.keys()) {
  236. if (CookieUtils.matchDomains(key, pageStore.allHostnamesString)) {
  237. recordPageCookie(pageStore, key);
  238. }
  239. }
  240. };
  241. let findAndRemovePageCookies = function (pageStore) {
  242. for (let key of CookieCache.keys()) {
  243. if (CookieUtils.matchDomains(key, pageStore.allHostnamesString)) {
  244. removeCookieAsync(key);
  245. }
  246. }
  247. };
  248. let canRemoveCookie = function (key, srcHostnames) {
  249. let entry = CookieCache.get(key);
  250. if (entry === undefined) {
  251. return false;
  252. }
  253. let cookieHostname = entry.hostname;
  254. let srcHostname;
  255. for (srcHostname of entry.usedOn) {
  256. if (ηm.mustAllow(srcHostname, cookieHostname, 'cookie')) {
  257. return false;
  258. }
  259. }
  260. // Maybe there is a scope in which the cookie is 1st-party-allowed.
  261. // For example, if I am logged in into `github.com`, I do not want to be
  262. // logged out just because I did not yet open a `github.com` page after
  263. // re-starting the browser.
  264. srcHostname = cookieHostname;
  265. let pos;
  266. for (;;) {
  267. if (srcHostnames.has(srcHostname)) {
  268. if (ηm.mustAllow(srcHostname, cookieHostname, 'cookie')) {
  269. return false;
  270. }
  271. }
  272. if (srcHostname === entry.domain) {
  273. break;
  274. }
  275. pos = srcHostname.indexOf('.');
  276. if (pos === -1) {
  277. break;
  278. }
  279. srcHostname = srcHostname.slice(pos + 1);
  280. }
  281. return true;
  282. };
  283. // Listen to any change in cookieland, we will update page stats accordingly.
  284. vAPI.cookies.onChanged = function (cookie) {
  285. // rhill 2013-12-11: If cookie value didn't change, no need to record.
  286. // https://github.com/gorhill/httpswitchboard/issues/79
  287. let key = CookieUtils.keyFromCookie(cookie);
  288. let entry = CookieCache.get(key);
  289. if (entry === undefined) {
  290. entry = CookieCache.add(cookie);
  291. } else {
  292. entry.tstamp = Date.now();
  293. if (cookie.value === entry.value) {
  294. return;
  295. }
  296. entry.value = cookie.value;
  297. }
  298. // Go through all pages and update if needed, as one cookie can be used
  299. // by many web pages, so they need to be recorded for all these pages.
  300. let pageStores = ηm.pageStores;
  301. let pageStore;
  302. for (let tabId in pageStores) {
  303. if (pageStores.hasOwnProperty(tabId) === false) {
  304. continue;
  305. }
  306. pageStore = pageStores[tabId];
  307. if (!CookieUtils.matchDomains(key, pageStore.allHostnamesString)) {
  308. continue;
  309. }
  310. recordPageCookie(pageStore, key);
  311. }
  312. };
  313. // Listen to any change in cookieland, we will update page stats accordingly.
  314. vAPI.cookies.onRemoved = function (cookie) {
  315. let key = CookieUtils.keyFromCookie(cookie);
  316. if (CookieCache.remove(key)) {
  317. ηm.logger.writeOne('', 'info', 'cookie',
  318. i18nCookieDeleteSuccess.replace('{{value}}',
  319. key));
  320. }
  321. };
  322. // Listen to any change in cookieland, we will update page stats accordingly.
  323. vAPI.cookies.onAllRemoved = function () {
  324. for (let key of CookieCache.keys()) {
  325. if (CookieCache.remove(key)) {
  326. ηm.logger.writeOne('', 'info', 'cookie',
  327. i18nCookieDeleteSuccess.replace('{{value}}',
  328. key));
  329. }
  330. }
  331. };
  332. vAPI.cookies.getAll(CookieCache.addVector);
  333. vAPI.cookies.start();
  334. vAPI.setTimeout(processRemoveQueue, processRemoveQueuePeriod);
  335. vAPI.setTimeout(processClean, processCleanPeriod);
  336. // Expose only what is necessary
  337. return {
  338. recordPageCookies: recordPageCookiesAsync,
  339. removePageCookies: removePageCookiesAsync
  340. };
  341. })();