You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

547 lines
17 KiB

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