123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- /*******************************************************************************
- ηMatrix - a browser extension to black/white list requests.
- Copyright (C) 2013-2019 Raymond Hill
- Copyright (C) 2019-2022 Alessio Vanni
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see {http://www.gnu.org/licenses/}.
- Home: https://gitlab.com/vannilla/ematrix
- uMatrix Home: https://github.com/gorhill/uMatrix
- */
- // rhill 2013-12-14: the whole cookie management has been rewritten so as
- // to avoid having to call chrome API whenever a single cookie changes, and
- // to record cookie for a web page *only* when its value changes.
- // https://github.com/gorhill/httpswitchboard/issues/79
- "use strict";
- // Isolate from global namespace
- // Use cached-context approach rather than object-based approach, as details
- // of the implementation do not need to be visible
- ηMatrix.cookieHunter = (function () {
- Cu.import('chrome://ematrix/content/lib/UriTools.jsm');
- Cu.import('chrome://ematrix/content/lib/CookieCache.jsm');
- let ηm = ηMatrix;
- let recordPageCookiesQueue = new Map();
- let removePageCookiesQueue = new Map();
- let removeCookieQueue = new Set();
- let processRemoveQueuePeriod = 2 * 60 * 1000;
- let processCleanPeriod = 10 * 60 * 1000;
- let processPageRecordQueueTimer = null;
- let processPageRemoveQueueTimer = null;
- // Look for cookies to record for a specific web page
- let recordPageCookiesAsync = function (pageStats) {
- // Store the page stats objects so that it doesn't go away
- // before we handle the job.
- // rhill 2013-10-19: pageStats could be nil, for example, this can
- // happens if a file:// ... makes an xmlHttpRequest
- if (!pageStats) {
- return;
- }
- recordPageCookiesQueue.set(pageStats.pageUrl, pageStats);
- if (processPageRecordQueueTimer === null) {
- processPageRecordQueueTimer =
- vAPI.setTimeout(processPageRecordQueue, 1000);
- }
- };
- let cookieLogEntryBuilder = [
- '',
- '{',
- '',
- '-cookie:',
- '',
- '}'
- ];
- let recordPageCookie = function (pageStore, key) {
- if (vAPI.isBehindTheSceneTabId(pageStore.tabId)) {
- return;
- }
- let entry = CookieCache.get(key);
- let pageHostname = pageStore.pageHostname;
- let block = ηm.mustBlock(pageHostname, entry.hostname, 'cookie');
- cookieLogEntryBuilder[0] = CookieUtils.urlFromEntry(entry);
- cookieLogEntryBuilder[2] = entry.session ? 'session' : 'persistent';
- cookieLogEntryBuilder[4] = encodeURIComponent(entry.name);
- let cookieURL = cookieLogEntryBuilder.join('');
- // rhill 2013-11-20:
- // https://github.com/gorhill/httpswitchboard/issues/60
- // Need to URL-encode cookie name
- pageStore.recordRequest('cookie', cookieURL, block);
- ηm.logger.writeOne(pageStore.tabId, 'net',
- pageHostname, cookieURL, 'cookie', block);
- entry.usedOn.add(pageHostname);
- // rhill 2013-11-21:
- // https://github.com/gorhill/httpswitchboard/issues/65
- // Leave alone cookies from behind-the-scene requests if
- // behind-the-scene processing is disabled.
- if (!block) {
- return;
- }
- if (!ηm.userSettings.deleteCookies) {
- return;
- }
- removeCookieAsync(key);
- };
- // Look for cookies to potentially remove for a specific web page
- let removePageCookiesAsync = function (pageStats) {
- // Hold onto pageStats objects so that it doesn't go away
- // before we handle the job.
- // rhill 2013-10-19: pageStats could be nil, for example, this can
- // happens if a file:// ... makes an xmlHttpRequest
- if (!pageStats) {
- return;
- }
- removePageCookiesQueue.set(pageStats.pageUrl, pageStats);
- if (processPageRemoveQueueTimer === null) {
- processPageRemoveQueueTimer =
- vAPI.setTimeout(processPageRemoveQueue, 15 * 1000);
- }
- };
- // Candidate for removal
- let removeCookieAsync = function (key) {
- removeCookieQueue.add(key);
- };
- let chromeCookieRemove = function (entry, name) {
- let url = CookieUtils.urlFromEntry(entry);
- if (url === '') {
- return;
- }
- let sessionKey = CookieUtils.keyFromURL(UriTools.set(url),
- 'session', name);
- let persistKey = CookieUtils.keyFromURL(UriTools.set(url),
- 'persistent', name);
- let callback = function(details) {
- let success = !!details;
- let template = success ?
- i18nCookieDeleteSuccess :
- i18nCookieDeleteFailure;
- if (CookieCache.remove(sessionKey)) {
- if (success) {
- ηm.cookieRemovedCounter += 1;
- }
- ηm.logger.writeOne('', 'info', 'cookie',
- template.replace('{{value}}',
- sessionKey));
- }
- if (CookieCache.remove(persistKey)) {
- if (success) {
- ηm.cookieRemovedCounter += 1;
- }
- ηm.logger.writeOne('', 'info', 'cookie',
- template.replace('{{value}}',
- persistKey));
- }
- };
- vAPI.cookies.remove({
- url: url, name: name
- }, callback);
- };
- let i18nCookieDeleteSuccess = vAPI.i18n('loggerEntryCookieDeleted');
- let i18nCookieDeleteFailure = vAPI.i18n('loggerEntryDeleteCookieError');
- let processPageRecordQueue = function () {
- processPageRecordQueueTimer = null;
- for (let pageStore of recordPageCookiesQueue.values()) {
- findAndRecordPageCookies(pageStore);
- }
- recordPageCookiesQueue.clear();
- };
- let processPageRemoveQueue = function () {
- processPageRemoveQueueTimer = null;
- for (let pageStore of removePageCookiesQueue.values()) {
- findAndRemovePageCookies(pageStore);
- }
- removePageCookiesQueue.clear();
- };
- // Effectively remove cookies.
- let processRemoveQueue = function () {
- let userSettings = ηm.userSettings;
- let deleteCookies = userSettings.deleteCookies;
- // Session cookies which timestamp is *after* tstampObsolete will
- // be left untouched
- // https://github.com/gorhill/httpswitchboard/issues/257
- let dusc = userSettings.deleteUnusedSessionCookies;
- let dusca = userSettings.deleteUnusedSessionCookiesAfter;
- let tstampObsolete = dusc ?
- Date.now() - dusca * 60 * 1000 :
- 0;
- let srcHostnames;
- let entry;
- for (let key of removeCookieQueue) {
- // rhill 2014-05-12: Apparently this can happen. I have to
- // investigate how (A session cookie has same name as a
- // persistent cookie?)
- entry = CookieCache.get(key);
- if (entry === undefined) {
- continue;
- }
- // Delete obsolete session cookies: enabled.
- if (tstampObsolete !== 0 && entry.session) {
- if (entry.tstamp < tstampObsolete) {
- chromeCookieRemove(entry, entry.name);
- continue;
- }
- }
- // Delete all blocked cookies: disabled.
- if (deleteCookies === false) {
- continue;
- }
- // Query scopes only if we are going to use them
- if (srcHostnames === undefined) {
- srcHostnames = ηm.tMatrix.extractAllSourceHostnames();
- }
- // Ensure cookie is not allowed on ALL current web pages: It can
- // happen that a cookie is blacklisted on one web page while
- // being whitelisted on another (because of per-page permissions).
- if (canRemoveCookie(key, srcHostnames)) {
- chromeCookieRemove(entry, entry.name);
- }
- }
- removeCookieQueue.clear();
- vAPI.setTimeout(processRemoveQueue, processRemoveQueuePeriod);
- };
- // Once in a while, we go ahead and clean everything that might have been
- // left behind.
- // Remove only some of the cookies which are candidate for removal: who knows,
- // maybe a user has 1000s of cookies sitting in his browser...
- let processClean = function () {
- let us = ηm.userSettings;
- if (us.deleteCookies || us.deleteUnusedSessionCookies) {
- let keys = Array.from(CookieCache.keys());
- let len = keys.length;
- let step, offset, n;
- if (len > 25) {
- step = len / 25;
- offset = Math.floor(Math.random() * len);
- n = 25;
- } else {
- step = 1;
- offset = 0;
- n = len;
- }
- let i = offset;
- while (n--) {
- removeCookieAsync(keys[Math.floor(i % len)]);
- i += step;
- }
- }
- vAPI.setTimeout(processClean, processCleanPeriod);
- };
- let findAndRecordPageCookies = function (pageStore) {
- for (let key of CookieCache.keys()) {
- if (CookieUtils.matchDomains(key, pageStore.allHostnamesString)) {
- recordPageCookie(pageStore, key);
- }
- }
- };
- let findAndRemovePageCookies = function (pageStore) {
- for (let key of CookieCache.keys()) {
- if (CookieUtils.matchDomains(key, pageStore.allHostnamesString)) {
- removeCookieAsync(key);
- }
- }
- };
- let canRemoveCookie = function (key, srcHostnames) {
- let entry = CookieCache.get(key);
- if (entry === undefined) {
- return false;
- }
- let cookieHostname = entry.hostname;
- let srcHostname;
- for (srcHostname of entry.usedOn) {
- if (ηm.mustAllow(srcHostname, cookieHostname, 'cookie')) {
- return false;
- }
- }
- // Maybe there is a scope in which the cookie is 1st-party-allowed.
- // For example, if I am logged in into `github.com`, I do not want to be
- // logged out just because I did not yet open a `github.com` page after
- // re-starting the browser.
- srcHostname = cookieHostname;
- let pos;
- for (;;) {
- if (srcHostnames.has(srcHostname)) {
- if (ηm.mustAllow(srcHostname, cookieHostname, 'cookie')) {
- return false;
- }
- }
- if (srcHostname === entry.domain) {
- break;
- }
- pos = srcHostname.indexOf('.');
- if (pos === -1) {
- break;
- }
- srcHostname = srcHostname.slice(pos + 1);
- }
- return true;
- };
- // Listen to any change in cookieland, we will update page stats accordingly.
- vAPI.cookies.onChanged = function (cookie) {
- // rhill 2013-12-11: If cookie value didn't change, no need to record.
- // https://github.com/gorhill/httpswitchboard/issues/79
- let key = CookieUtils.keyFromCookie(cookie);
- let entry = CookieCache.get(key);
- if (entry === undefined) {
- entry = CookieCache.add(cookie);
- } else {
- entry.tstamp = Date.now();
- if (cookie.value === entry.value) {
- return;
- }
- entry.value = cookie.value;
- }
- // Go through all pages and update if needed, as one cookie can be used
- // by many web pages, so they need to be recorded for all these pages.
- let pageStores = ηm.pageStores;
- let pageStore;
- for (let tabId in pageStores) {
- if (pageStores.hasOwnProperty(tabId) === false) {
- continue;
- }
- pageStore = pageStores[tabId];
- if (!CookieUtils.matchDomains(key, pageStore.allHostnamesString)) {
- continue;
- }
- recordPageCookie(pageStore, key);
- }
- };
- // Listen to any change in cookieland, we will update page stats accordingly.
- vAPI.cookies.onRemoved = function (cookie) {
- let key = CookieUtils.keyFromCookie(cookie);
- if (CookieCache.remove(key)) {
- ηm.logger.writeOne('', 'info', 'cookie',
- i18nCookieDeleteSuccess.replace('{{value}}',
- key));
- }
- };
- // Listen to any change in cookieland, we will update page stats accordingly.
- vAPI.cookies.onAllRemoved = function () {
- for (let key of CookieCache.keys()) {
- if (CookieCache.remove(key)) {
- ηm.logger.writeOne('', 'info', 'cookie',
- i18nCookieDeleteSuccess.replace('{{value}}',
- key));
- }
- }
- };
- vAPI.cookies.getAll(CookieCache.addVector);
- vAPI.cookies.start();
- vAPI.setTimeout(processRemoveQueue, processRemoveQueuePeriod);
- vAPI.setTimeout(processClean, processCleanPeriod);
- // Expose only what is necessary
- return {
- recordPageCookies: recordPageCookiesAsync,
- removePageCookies: removePageCookiesAsync
- };
- })();
|