/******************************************************************************* ηMatrix - a browser extension to black/white list requests. Copyright (C) 2013-2019 Raymond Hill Copyright (C) 2019-2020 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://libregit.spks.xyz/heckyel/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 }; })();