cookies.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. /*******************************************************************************
  2. ηMatrix - a browser extension to black/white list requests.
  3. Copyright (C) 2013-2019 Raymond Hill
  4. Copyright (C) 2019 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://libregit.org/heckyel/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. /******************************************************************************/
  24. // Isolate from global namespace
  25. // Use cached-context approach rather than object-based approach, as details
  26. // of the implementation do not need to be visible
  27. ηMatrix.cookieHunter = (function() {
  28. /******************************************************************************/
  29. var ηm = ηMatrix;
  30. var recordPageCookiesQueue = new Map();
  31. var removePageCookiesQueue = new Map();
  32. var removeCookieQueue = new Set();
  33. var cookieDict = new Map();
  34. var cookieEntryJunkyard = [];
  35. var processRemoveQueuePeriod = 2 * 60 * 1000;
  36. var processCleanPeriod = 10 * 60 * 1000;
  37. var processPageRecordQueueTimer = null;
  38. var processPageRemoveQueueTimer = null;
  39. /******************************************************************************/
  40. var CookieEntry = function(cookie) {
  41. this.usedOn = new Set();
  42. this.init(cookie);
  43. };
  44. CookieEntry.prototype.init = function(cookie) {
  45. this.secure = cookie.secure;
  46. this.session = cookie.session;
  47. this.anySubdomain = cookie.domain.charAt(0) === '.';
  48. this.hostname = this.anySubdomain ? cookie.domain.slice(1) : cookie.domain;
  49. this.domain = ηm.URI.domainFromHostname(this.hostname) || this.hostname;
  50. this.path = cookie.path;
  51. this.name = cookie.name;
  52. this.value = cookie.value;
  53. this.tstamp = Date.now();
  54. this.usedOn.clear();
  55. return this;
  56. };
  57. // Release anything which may consume too much memory
  58. CookieEntry.prototype.dispose = function() {
  59. this.hostname = '';
  60. this.domain = '';
  61. this.path = '';
  62. this.name = '';
  63. this.value = '';
  64. this.usedOn.clear();
  65. return this;
  66. };
  67. /******************************************************************************/
  68. var addCookieToDict = function(cookie) {
  69. var cookieKey = cookieKeyFromCookie(cookie),
  70. cookieEntry = cookieDict.get(cookieKey);
  71. if ( cookieEntry === undefined ) {
  72. cookieEntry = cookieEntryJunkyard.pop();
  73. if ( cookieEntry ) {
  74. cookieEntry.init(cookie);
  75. } else {
  76. cookieEntry = new CookieEntry(cookie);
  77. }
  78. cookieDict.set(cookieKey, cookieEntry);
  79. }
  80. return cookieEntry;
  81. };
  82. /******************************************************************************/
  83. var addCookiesToDict = function(cookies) {
  84. var i = cookies.length;
  85. while ( i-- ) {
  86. addCookieToDict(cookies[i]);
  87. }
  88. };
  89. /******************************************************************************/
  90. var removeCookieFromDict = function(cookieKey) {
  91. var cookieEntry = cookieDict.get(cookieKey);
  92. if ( cookieEntry === undefined ) { return false; }
  93. cookieDict.delete(cookieKey);
  94. if ( cookieEntryJunkyard.length < 25 ) {
  95. cookieEntryJunkyard.push(cookieEntry.dispose());
  96. }
  97. return true;
  98. };
  99. /******************************************************************************/
  100. var cookieKeyBuilder = [
  101. '', // 0 = scheme
  102. '://',
  103. '', // 2 = domain
  104. '', // 3 = path
  105. '{',
  106. '', // 5 = persistent or session
  107. '-cookie:',
  108. '', // 7 = name
  109. '}'
  110. ];
  111. var cookieKeyFromCookie = function(cookie) {
  112. var cb = cookieKeyBuilder;
  113. cb[0] = cookie.secure ? 'https' : 'http';
  114. cb[2] = cookie.domain.charAt(0) === '.' ? cookie.domain.slice(1) : cookie.domain;
  115. cb[3] = cookie.path;
  116. cb[5] = cookie.session ? 'session' : 'persistent';
  117. cb[7] = cookie.name;
  118. return cb.join('');
  119. };
  120. var cookieKeyFromCookieURL = function(url, type, name) {
  121. var ηmuri = ηm.URI.set(url);
  122. var cb = cookieKeyBuilder;
  123. cb[0] = ηmuri.scheme;
  124. cb[2] = ηmuri.hostname;
  125. cb[3] = ηmuri.path;
  126. cb[5] = type;
  127. cb[7] = name;
  128. return cb.join('');
  129. };
  130. /******************************************************************************/
  131. var cookieURLFromCookieEntry = function(entry) {
  132. if ( !entry ) {
  133. return '';
  134. }
  135. return (entry.secure ? 'https://' : 'http://') + entry.hostname + entry.path;
  136. };
  137. /******************************************************************************/
  138. var cookieMatchDomains = function(cookieKey, allHostnamesString) {
  139. var cookieEntry = cookieDict.get(cookieKey);
  140. if ( cookieEntry === undefined ) { return false; }
  141. if ( allHostnamesString.indexOf(' ' + cookieEntry.hostname + ' ') < 0 ) {
  142. if ( !cookieEntry.anySubdomain ) {
  143. return false;
  144. }
  145. if ( allHostnamesString.indexOf('.' + cookieEntry.hostname + ' ') < 0 ) {
  146. return false;
  147. }
  148. }
  149. return true;
  150. };
  151. /******************************************************************************/
  152. // Look for cookies to record for a specific web page
  153. var recordPageCookiesAsync = function(pageStats) {
  154. // Store the page stats objects so that it doesn't go away
  155. // before we handle the job.
  156. // rhill 2013-10-19: pageStats could be nil, for example, this can
  157. // happens if a file:// ... makes an xmlHttpRequest
  158. if ( !pageStats ) {
  159. return;
  160. }
  161. recordPageCookiesQueue.set(pageStats.pageUrl, pageStats);
  162. if ( processPageRecordQueueTimer === null ) {
  163. processPageRecordQueueTimer = vAPI.setTimeout(processPageRecordQueue, 1000);
  164. }
  165. };
  166. /******************************************************************************/
  167. var cookieLogEntryBuilder = [
  168. '',
  169. '{',
  170. '',
  171. '-cookie:',
  172. '',
  173. '}'
  174. ];
  175. var recordPageCookie = function(pageStore, cookieKey) {
  176. if ( vAPI.isBehindTheSceneTabId(pageStore.tabId) ) { return; }
  177. var cookieEntry = cookieDict.get(cookieKey);
  178. var pageHostname = pageStore.pageHostname;
  179. var block = ηm.mustBlock(pageHostname, cookieEntry.hostname, 'cookie');
  180. cookieLogEntryBuilder[0] = cookieURLFromCookieEntry(cookieEntry);
  181. cookieLogEntryBuilder[2] = cookieEntry.session ? 'session' : 'persistent';
  182. cookieLogEntryBuilder[4] = encodeURIComponent(cookieEntry.name);
  183. var cookieURL = cookieLogEntryBuilder.join('');
  184. // rhill 2013-11-20:
  185. // https://github.com/gorhill/httpswitchboard/issues/60
  186. // Need to URL-encode cookie name
  187. pageStore.recordRequest('cookie', cookieURL, block);
  188. ηm.logger.writeOne(pageStore.tabId, 'net', pageHostname, cookieURL, 'cookie', block);
  189. cookieEntry.usedOn.add(pageHostname);
  190. // rhill 2013-11-21:
  191. // https://github.com/gorhill/httpswitchboard/issues/65
  192. // Leave alone cookies from behind-the-scene requests if
  193. // behind-the-scene processing is disabled.
  194. if ( !block ) {
  195. return;
  196. }
  197. if ( !ηm.userSettings.deleteCookies ) {
  198. return;
  199. }
  200. removeCookieAsync(cookieKey);
  201. };
  202. /******************************************************************************/
  203. // Look for cookies to potentially remove for a specific web page
  204. var removePageCookiesAsync = function(pageStats) {
  205. // Hold onto pageStats objects so that it doesn't go away
  206. // before we handle the job.
  207. // rhill 2013-10-19: pageStats could be nil, for example, this can
  208. // happens if a file:// ... makes an xmlHttpRequest
  209. if ( !pageStats ) {
  210. return;
  211. }
  212. removePageCookiesQueue.set(pageStats.pageUrl, pageStats);
  213. if ( processPageRemoveQueueTimer === null ) {
  214. processPageRemoveQueueTimer = vAPI.setTimeout(processPageRemoveQueue, 15 * 1000);
  215. }
  216. };
  217. /******************************************************************************/
  218. // Candidate for removal
  219. var removeCookieAsync = function(cookieKey) {
  220. removeCookieQueue.add(cookieKey);
  221. };
  222. /******************************************************************************/
  223. var chromeCookieRemove = function(cookieEntry, name) {
  224. var url = cookieURLFromCookieEntry(cookieEntry);
  225. if ( url === '' ) {
  226. return;
  227. }
  228. var sessionCookieKey = cookieKeyFromCookieURL(url, 'session', name);
  229. var persistCookieKey = cookieKeyFromCookieURL(url, 'persistent', name);
  230. var callback = function(details) {
  231. var success = !!details;
  232. var template = success ? i18nCookieDeleteSuccess : i18nCookieDeleteFailure;
  233. if ( removeCookieFromDict(sessionCookieKey) ) {
  234. if ( success ) {
  235. ηm.cookieRemovedCounter += 1;
  236. }
  237. ηm.logger.writeOne('', 'info', 'cookie', template.replace('{{value}}', sessionCookieKey));
  238. }
  239. if ( removeCookieFromDict(persistCookieKey) ) {
  240. if ( success ) {
  241. ηm.cookieRemovedCounter += 1;
  242. }
  243. ηm.logger.writeOne('', 'info', 'cookie', template.replace('{{value}}', persistCookieKey));
  244. }
  245. };
  246. vAPI.cookies.remove({ url: url, name: name }, callback);
  247. };
  248. var i18nCookieDeleteSuccess = vAPI.i18n('loggerEntryCookieDeleted');
  249. var i18nCookieDeleteFailure = vAPI.i18n('loggerEntryDeleteCookieError');
  250. /******************************************************************************/
  251. var processPageRecordQueue = function() {
  252. processPageRecordQueueTimer = null;
  253. for ( var pageStore of recordPageCookiesQueue.values() ) {
  254. findAndRecordPageCookies(pageStore);
  255. }
  256. recordPageCookiesQueue.clear();
  257. };
  258. /******************************************************************************/
  259. var processPageRemoveQueue = function() {
  260. processPageRemoveQueueTimer = null;
  261. for ( var pageStore of removePageCookiesQueue.values() ) {
  262. findAndRemovePageCookies(pageStore);
  263. }
  264. removePageCookiesQueue.clear();
  265. };
  266. /******************************************************************************/
  267. // Effectively remove cookies.
  268. var processRemoveQueue = function() {
  269. var userSettings = ηm.userSettings;
  270. var deleteCookies = userSettings.deleteCookies;
  271. // Session cookies which timestamp is *after* tstampObsolete will
  272. // be left untouched
  273. // https://github.com/gorhill/httpswitchboard/issues/257
  274. var tstampObsolete = userSettings.deleteUnusedSessionCookies ?
  275. Date.now() - userSettings.deleteUnusedSessionCookiesAfter * 60 * 1000 :
  276. 0;
  277. var srcHostnames;
  278. var cookieEntry;
  279. for ( var cookieKey of removeCookieQueue ) {
  280. // rhill 2014-05-12: Apparently this can happen. I have to
  281. // investigate how (A session cookie has same name as a
  282. // persistent cookie?)
  283. cookieEntry = cookieDict.get(cookieKey);
  284. if ( cookieEntry === undefined ) { continue; }
  285. // Delete obsolete session cookies: enabled.
  286. if ( tstampObsolete !== 0 && cookieEntry.session ) {
  287. if ( cookieEntry.tstamp < tstampObsolete ) {
  288. chromeCookieRemove(cookieEntry, cookieEntry.name);
  289. continue;
  290. }
  291. }
  292. // Delete all blocked cookies: disabled.
  293. if ( deleteCookies === false ) {
  294. continue;
  295. }
  296. // Query scopes only if we are going to use them
  297. if ( srcHostnames === undefined ) {
  298. srcHostnames = ηm.tMatrix.extractAllSourceHostnames();
  299. }
  300. // Ensure cookie is not allowed on ALL current web pages: It can
  301. // happen that a cookie is blacklisted on one web page while
  302. // being whitelisted on another (because of per-page permissions).
  303. if ( canRemoveCookie(cookieKey, srcHostnames) ) {
  304. chromeCookieRemove(cookieEntry, cookieEntry.name);
  305. }
  306. }
  307. removeCookieQueue.clear();
  308. vAPI.setTimeout(processRemoveQueue, processRemoveQueuePeriod);
  309. };
  310. /******************************************************************************/
  311. // Once in a while, we go ahead and clean everything that might have been
  312. // left behind.
  313. // Remove only some of the cookies which are candidate for removal: who knows,
  314. // maybe a user has 1000s of cookies sitting in his browser...
  315. var processClean = function() {
  316. var us = ηm.userSettings;
  317. if ( us.deleteCookies || us.deleteUnusedSessionCookies ) {
  318. var cookieKeys = Array.from(cookieDict.keys()),
  319. len = cookieKeys.length,
  320. step, offset, n;
  321. if ( len > 25 ) {
  322. step = len / 25;
  323. offset = Math.floor(Math.random() * len);
  324. n = 25;
  325. } else {
  326. step = 1;
  327. offset = 0;
  328. n = len;
  329. }
  330. var i = offset;
  331. while ( n-- ) {
  332. removeCookieAsync(cookieKeys[Math.floor(i % len)]);
  333. i += step;
  334. }
  335. }
  336. vAPI.setTimeout(processClean, processCleanPeriod);
  337. };
  338. /******************************************************************************/
  339. var findAndRecordPageCookies = function(pageStore) {
  340. for ( var cookieKey of cookieDict.keys() ) {
  341. if ( cookieMatchDomains(cookieKey, pageStore.allHostnamesString) ) {
  342. recordPageCookie(pageStore, cookieKey);
  343. }
  344. }
  345. };
  346. /******************************************************************************/
  347. var findAndRemovePageCookies = function(pageStore) {
  348. for ( var cookieKey of cookieDict.keys() ) {
  349. if ( cookieMatchDomains(cookieKey, pageStore.allHostnamesString) ) {
  350. removeCookieAsync(cookieKey);
  351. }
  352. }
  353. };
  354. /******************************************************************************/
  355. var canRemoveCookie = function(cookieKey, srcHostnames) {
  356. var cookieEntry = cookieDict.get(cookieKey);
  357. if ( cookieEntry === undefined ) { return false; }
  358. var cookieHostname = cookieEntry.hostname;
  359. var srcHostname;
  360. for ( srcHostname of cookieEntry.usedOn ) {
  361. if ( ηm.mustAllow(srcHostname, cookieHostname, 'cookie') ) {
  362. return false;
  363. }
  364. }
  365. // Maybe there is a scope in which the cookie is 1st-party-allowed.
  366. // For example, if I am logged in into `github.com`, I do not want to be
  367. // logged out just because I did not yet open a `github.com` page after
  368. // re-starting the browser.
  369. srcHostname = cookieHostname;
  370. var pos;
  371. for (;;) {
  372. if ( srcHostnames.has(srcHostname) ) {
  373. if ( ηm.mustAllow(srcHostname, cookieHostname, 'cookie') ) {
  374. return false;
  375. }
  376. }
  377. if ( srcHostname === cookieEntry.domain ) {
  378. break;
  379. }
  380. pos = srcHostname.indexOf('.');
  381. if ( pos === -1 ) {
  382. break;
  383. }
  384. srcHostname = srcHostname.slice(pos + 1);
  385. }
  386. return true;
  387. };
  388. /******************************************************************************/
  389. // Listen to any change in cookieland, we will update page stats accordingly.
  390. vAPI.cookies.onChanged = function(cookie) {
  391. // rhill 2013-12-11: If cookie value didn't change, no need to record.
  392. // https://github.com/gorhill/httpswitchboard/issues/79
  393. var cookieKey = cookieKeyFromCookie(cookie);
  394. var cookieEntry = cookieDict.get(cookieKey);
  395. if ( cookieEntry === undefined ) {
  396. cookieEntry = addCookieToDict(cookie);
  397. } else {
  398. cookieEntry.tstamp = Date.now();
  399. if ( cookie.value === cookieEntry.value ) { return; }
  400. cookieEntry.value = cookie.value;
  401. }
  402. // Go through all pages and update if needed, as one cookie can be used
  403. // by many web pages, so they need to be recorded for all these pages.
  404. var pageStores = ηm.pageStores;
  405. var pageStore;
  406. for ( var tabId in pageStores ) {
  407. if ( pageStores.hasOwnProperty(tabId) === false ) {
  408. continue;
  409. }
  410. pageStore = pageStores[tabId];
  411. if ( !cookieMatchDomains(cookieKey, pageStore.allHostnamesString) ) {
  412. continue;
  413. }
  414. recordPageCookie(pageStore, cookieKey);
  415. }
  416. };
  417. /******************************************************************************/
  418. // Listen to any change in cookieland, we will update page stats accordingly.
  419. vAPI.cookies.onRemoved = function(cookie) {
  420. var cookieKey = cookieKeyFromCookie(cookie);
  421. if ( removeCookieFromDict(cookieKey) ) {
  422. ηm.logger.writeOne('', 'info', 'cookie', i18nCookieDeleteSuccess.replace('{{value}}', cookieKey));
  423. }
  424. };
  425. /******************************************************************************/
  426. // Listen to any change in cookieland, we will update page stats accordingly.
  427. vAPI.cookies.onAllRemoved = function() {
  428. for ( var cookieKey of cookieDict.keys() ) {
  429. if ( removeCookieFromDict(cookieKey) ) {
  430. ηm.logger.writeOne('', 'info', 'cookie', i18nCookieDeleteSuccess.replace('{{value}}', cookieKey));
  431. }
  432. }
  433. };
  434. /******************************************************************************/
  435. vAPI.cookies.getAll(addCookiesToDict);
  436. vAPI.cookies.start();
  437. vAPI.setTimeout(processRemoveQueue, processRemoveQueuePeriod);
  438. vAPI.setTimeout(processClean, processCleanPeriod);
  439. /******************************************************************************/
  440. // Expose only what is necessary
  441. return {
  442. recordPageCookies: recordPageCookiesAsync,
  443. removePageCookies: removePageCookiesAsync
  444. };
  445. /******************************************************************************/
  446. })();
  447. /******************************************************************************/