diff --git a/src/js/cookies.js b/src/js/cookies.js index 2870dda..abd2e16 100644 --- a/src/js/cookies.js +++ b/src/js/cookies.js @@ -19,6 +19,8 @@ Home: https://github.com/gorhill/uMatrix */ +/* global µMatrix */ + // 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. @@ -35,6 +37,8 @@ /******************************************************************************/ +var µm = µMatrix; + var recordPageCookiesQueue = {}; var removePageCookiesQueue = {}; var removeCookieQueue = {}; @@ -51,21 +55,24 @@ CookieEntry.prototype.set = function(cookie) { this.secure = cookie.secure; this.session = cookie.session; this.anySubdomain = cookie.domain.charAt(0) === '.'; - this.domain = this.anySubdomain ? cookie.domain.slice(1) : cookie.domain; + this.hostname = this.anySubdomain ? cookie.domain.slice(1) : cookie.domain; + this.domain = µm.URI.domainFromHostname(this.hostname) || this.hostname; this.path = cookie.path; this.name = cookie.name; this.value = cookie.value; this.tstamp = Date.now(); + this.usedOn = {}; return this; }; // Release anything which may consume too much memory CookieEntry.prototype.unset = function() { - this.domain = ''; + this.hostname = ''; this.path = ''; this.name = ''; this.value = ''; + this.usedOn = {}; return this; }; @@ -134,7 +141,7 @@ var cookieKeyFromCookie = function(cookie) { }; var cookieKeyFromCookieURL = function(url, type, name) { - var µmuri = µMatrix.URI.set(url); + var µmuri = µm.URI.set(url); var cb = cookieKeyBuilder; cb[0] = µmuri.scheme; cb[2] = µmuri.hostname; @@ -156,21 +163,21 @@ var cookieURLFromCookieEntry = function(entry) { if ( !entry ) { return ''; } - return (entry.secure ? 'https://' : 'http://') + entry.domain + entry.path; + return (entry.secure ? 'https://' : 'http://') + entry.hostname + entry.path; }; /******************************************************************************/ -var cookieMatchDomains = function(cookieKey, domains) { +var cookieMatchDomains = function(cookieKey, allHostnamesString) { var cookieEntry = cookieDict[cookieKey]; if ( !cookieEntry ) { return false; } - if ( domains.indexOf(' ' + cookieEntry.domain + ' ') < 0 ) { + if ( allHostnamesString.indexOf(' ' + cookieEntry.hostname + ' ') < 0 ) { if ( !cookieEntry.anySubdomain ) { return false; } - if ( domains.indexOf('.' + cookieEntry.domain + ' ') < 0 ) { + if ( allHostnamesString.indexOf('.' + cookieEntry.hostname + ' ') < 0 ) { return false; } } @@ -189,9 +196,9 @@ var recordPageCookiesAsync = function(pageStats) { if ( !pageStats ) { return; } - var pageURL = µMatrix.pageUrlFromPageStats(pageStats); + var pageURL = µm.pageUrlFromPageStats(pageStats); recordPageCookiesQueue[pageURL] = pageStats; - µMatrix.asyncJobs.add( + µm.asyncJobs.add( 'cookieHunterPageRecord', null, processPageRecordQueue, @@ -211,11 +218,9 @@ var cookieLogEntryBuilder = [ '}' ]; -var recordPageCookie = function(pageStats, cookieKey) { - var µm = µMatrix; +var recordPageCookie = function(pageStore, cookieKey) { var cookieEntry = cookieDict[cookieKey]; - var pageURL = pageStats.pageUrl; - var block = µm.mustBlock(µm.scopeFromURL(pageURL), cookieEntry.domain, 'cookie'); + var block = µm.mustBlock(pageStore.pageHostname, cookieEntry.hostname, 'cookie'); cookieLogEntryBuilder[0] = cookieURLFromCookieEntry(cookieEntry); cookieLogEntryBuilder[2] = cookieEntry.session ? 'session' : 'persistent'; @@ -224,12 +229,14 @@ var recordPageCookie = function(pageStats, cookieKey) { // rhill 2013-11-20: // https://github.com/gorhill/httpswitchboard/issues/60 // Need to URL-encode cookie name - pageStats.recordRequest( + pageStore.recordRequest( 'cookie', cookieLogEntryBuilder.join(''), block ); + cookieEntry.usedOn[pageStore.pageHostname] = true; + // rhill 2013-11-21: // https://github.com/gorhill/httpswitchboard/issues/65 // Leave alone cookies from behind-the-scene requests if @@ -255,9 +262,9 @@ var removePageCookiesAsync = function(pageStats) { if ( !pageStats ) { return; } - var pageURL = µMatrix.pageUrlFromPageStats(pageStats); + var pageURL = µm.pageUrlFromPageStats(pageStats); removePageCookiesQueue[pageURL] = pageStats; - µMatrix.asyncJobs.add( + µm.asyncJobs.add( 'cookieHunterPageRemove', null, processPageRemoveQueue, @@ -284,12 +291,12 @@ var chromeCookieRemove = function(url, name) { } var cookieKey = cookieKeyFromCookieURL(details.url, 'session', details.name); if ( removeCookieFromDict(cookieKey) ) { - µMatrix.cookieRemovedCounter += 1; + µm.cookieRemovedCounter += 1; return; } cookieKey = cookieKeyFromCookieURL(details.url, 'persistent', details.name); if ( removeCookieFromDict(cookieKey) ) { - µMatrix.cookieRemovedCounter += 1; + µm.cookieRemovedCounter += 1; } }; @@ -325,7 +332,7 @@ var processPageRemoveQueue = function() { // Effectively remove cookies. var processRemoveQueue = function() { - var userSettings = µMatrix.userSettings; + var userSettings = µm.userSettings; var deleteCookies = userSettings.deleteCookies; // Session cookies which timestamp is *after* tstampObsolete will @@ -335,7 +342,9 @@ var processRemoveQueue = function() { Date.now() - userSettings.deleteUnusedSessionCookiesAfter * 60 * 1000 : 0; + var srcHostnames; var cookieEntry; + for ( var cookieKey in removeCookieQueue ) { if ( removeCookieQueue.hasOwnProperty(cookieKey) === false ) { continue; @@ -348,7 +357,7 @@ var processRemoveQueue = function() { // investigate how (A session cookie has same name as a // persistent cookie?) if ( !cookieEntry ) { - console.error('HTTP Switchboard> cookies.js/processRemoveQueue(): no cookieEntry for "%s"', cookieKey); + // console.error('cookies.js > processRemoveQueue(): no cookieEntry for "%s"', cookieKey); continue; } @@ -357,10 +366,15 @@ var processRemoveQueue = function() { 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(cookieKey) === false ) { + if ( canRemoveCookie(cookieKey, srcHostnames) === false ) { // Exception: session cookie may have to be removed even though // they are seen as being whitelisted. if ( cookieEntry.session === false || cookieEntry.tstamp > tstampObsolete ) { @@ -373,7 +387,7 @@ var processRemoveQueue = function() { continue; } - console.debug('µMatrix> cookies.js/processRemoveQueue(): removing "%s" (age=%s min)', cookieKey, ((Date.now() - cookieEntry.tstamp) / 60000).toFixed(1)); + // console.debug('cookies.js > processRemoveQueue(): removing "%s" (age=%s min)', cookieKey, ((Date.now() - cookieEntry.tstamp) / 60000).toFixed(1)); chromeCookieRemove(url, cookieEntry.name); } }; @@ -399,12 +413,11 @@ var processClean = function() { /******************************************************************************/ var findAndRecordPageCookies = function(pageStats) { - var domains = ' ' + Object.keys(pageStats.domains).join(' ') + ' '; for ( var cookieKey in cookieDict ) { if ( !cookieDict.hasOwnProperty(cookieKey) ) { continue; } - if ( !cookieMatchDomains(cookieKey, domains) ) { + if ( cookieMatchDomains(cookieKey, pageStats.allHostnamesString) === false ) { continue; } recordPageCookie(pageStats, cookieKey); @@ -414,12 +427,11 @@ var findAndRecordPageCookies = function(pageStats) { /******************************************************************************/ var findAndRemovePageCookies = function(pageStats) { - var domains = ' ' + Object.keys(pageStats.domains).join(' ') + ' '; for ( var cookieKey in cookieDict ) { - if ( !cookieDict.hasOwnProperty(cookieKey, domains) ) { + if ( !cookieDict.hasOwnProperty(cookieKey) ) { continue; } - if ( !cookieMatchDomains(cookieKey, domains) ) { + if ( !cookieMatchDomains(cookieKey, pageStats.allHostnamesString) ) { continue; } removeCookieAsync(cookieKey); @@ -428,69 +440,44 @@ var findAndRemovePageCookies = function(pageStats) { /******************************************************************************/ -// Check all scopes to ensure none of them fulfill the following -// conditions: -// - The hostname of the target cookie matches the hostname of the scope -// - The target cookie is allowed in the scope -// Check all pages to ensure none of them fulfill both following -// conditions: -// - refers to the target cookie -// - the target cookie is is allowed -// If one of the above set of conditions is fulfilled at least once, -// the cookie can NOT be removed. -// TODO: cache the joining of hostnames into a single string for search -// purpose. - -var canRemoveCookie = function(cookieKey) { - var entry = cookieDict[cookieKey]; - if ( !entry ) { +var canRemoveCookie = function(cookieKey, srcHostnames) { + var cookieEntry = cookieDict[cookieKey]; + if ( !cookieEntry ) { return false; } - var µm = µMatrix; - var cookieHostname = entry.domain; - var cookieDomain = µm.URI.domainFromHostname(cookieHostname); - - // rhill 2014-01-11: Do not delete cookies which are whitelisted - // in at least one scope. Limitation: this can be done only - // for cookies which domain matches domain of scope. This is - // because a scope with whitelist *|* would cause all cookies to not - // be removable. - // https://github.com/gorhill/httpswitchboard/issues/126 - var srcHostnames = µm.tMatrix.extractAllSourceHostnames(); - var i = srcHostnames.length; + var cookieHostname = cookieEntry.hostname; var srcHostname; - while ( i-- ) { - // Cookie related to scope domain? - srcHostname = µm.URI.domainFromHostname(srcHostnames[i]); - if ( srcHostname === '' || srcHostname !== cookieDomain ) { + + for ( srcHostname in cookieEntry.usedOn ) { + if ( cookieEntry.usedOn.hasOwnProperty(srcHostname) === false ) { continue; } - if ( µm.mustBlock(srcHostname, cookieHostname, 'cookie') === false ) { - // console.log('cookies.js/canRemoveCookie()> can NOT remove "%s" because of scope "%s"', cookieKey, scopeKey); + if ( µm.mustAllow(srcHostname, cookieHostname, 'cookie') ) { return false; } } - - // If we reach this point, we will check whether the cookie is actually - // in use for a currently opened web page. This is necessary to - // prevent the deletion of 3rd-party cookies which might be whitelisted - // for a currently opened web page. - var pageStats = µm.pageStats; - for ( var pageURL in pageStats ) { - if ( pageStats.hasOwnProperty(pageURL) === false ) { - continue; + // 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; + var pos; + for (;;) { + if ( srcHostnames.hasOwnProperty(srcHostname) ) { + if ( µm.mustAllow(srcHostname, cookieHostname, 'cookie') ) { + return false; + } } - if ( !cookieMatchDomains(cookieKey, ' ' + Object.keys(pageStats[pageURL].domains).join(' ') + ' ') ) { - continue; + if ( srcHostname === cookieEntry.domain ) { + break; } - if ( µm.mustAllow(µm.scopeFromURL(pageURL), cookieHostname, 'cookie') ) { - // console.log('cookies.js/canRemoveCookie()> can NOT remove "%s" because of scope "%s"', cookieKey, scopeKey); - return false; + pos = srcHostname.indexOf('.'); + if ( pos === -1 ) { + break; } + srcHostname = srcHostname.slice(pos + 1); } - - // console.log('cookies.js/canRemoveCookie()> can remove "%s"', cookieKey); - return true; + return true; }; /******************************************************************************/ @@ -520,17 +507,17 @@ var onChromeCookieChanged = function(changeInfo) { // 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. - var allPageStats = µMatrix.pageStats; - var pageStats; - for ( var pageURL in allPageStats ) { - if ( !allPageStats.hasOwnProperty(pageURL) ) { + var pageStores = µm.pageStats; + var pageStore; + for ( var pageURL in pageStores ) { + if ( pageStores.hasOwnProperty(pageURL) === false ) { continue; } - pageStats = allPageStats[pageURL]; - if ( !cookieMatchDomains(cookieKey, ' ' + Object.keys(pageStats.domains).join(' ') + ' ') ) { + pageStore = pageStores[pageURL]; + if ( !cookieMatchDomains(cookieKey, pageStore.allHostnamesString) ) { continue; } - recordPageCookie(pageStats, cookieKey); + recordPageCookie(pageStore, cookieKey); } }; @@ -539,8 +526,8 @@ var onChromeCookieChanged = function(changeInfo) { chrome.cookies.getAll({}, addCookiesToDict); chrome.cookies.onChanged.addListener(onChromeCookieChanged); -// µMatrix.asyncJobs.add('cookieHunterRemove', null, processRemoveQueue, 2 * 60 * 1000, true); -// µMatrix.asyncJobs.add('cookieHunterClean', null, processClean, 10 * 60 * 1000, true); +µm.asyncJobs.add('cookieHunterRemove', null, processRemoveQueue, 2 * 60 * 1000, true); +µm.asyncJobs.add('cookieHunterClean', null, processClean, 10 * 60 * 1000, true); /******************************************************************************/ diff --git a/src/js/matrix.js b/src/js/matrix.js index cbf53cb..64ed450 100644 --- a/src/js/matrix.js +++ b/src/js/matrix.js @@ -498,7 +498,24 @@ Matrix.prototype.extractAllSourceHostnames = function() { } srcHostnames[rule.slice(0, rule.indexOf(' '))] = true; } - return Object.keys(srcHostnames); + return srcHostnames; +}; + +/******************************************************************************/ + +// TODO: In all likelyhood, will have to optmize here, i.e. keeping an +// up-to-date collection of src hostnames with reference count etc. + +Matrix.prototype.extractAllDestinationHostnames = function() { + var desHostnames = {}; + var rules = this.rules; + for ( var rule in rules ) { + if ( rules.hasOwnProperty(rule) === false ) { + continue; + } + desHostnames[this.desHostnameFromRule(rule)] = true; + } + return desHostnames; }; /******************************************************************************/ diff --git a/src/js/pagestats.js b/src/js/pagestats.js index 783a307..73916c5 100644 --- a/src/js/pagestats.js +++ b/src/js/pagestats.js @@ -435,21 +435,9 @@ var pageStoreFactory = function(pageUrl) { /******************************************************************************/ function PageStore(pageUrl) { - this.pageUrl = ''; - this.pageHostname = ''; - this.pageDomain = ''; - this.pageScriptBlocked = false; - this.thirdpartyScript = false; - this.requests = µm.PageRequestStats.factory(); - this.domains = {}; - this.state = {}; this.visible = false; this.requestStats = new WebRequestStats(); - this.distinctRequestCount = 0; - this.perLoadAllowedRequestCount = 0; - this.perLoadBlockedRequestCount = 0; this.off = false; - this.abpBlockCount = 0; this.init(pageUrl); } @@ -458,17 +446,17 @@ function PageStore(pageUrl) { PageStore.prototype.init = function(pageUrl) { this.pageUrl = pageUrl; this.pageHostname = µm.URI.hostnameFromURI(pageUrl); - this.pageDomain = µm.URI.domainFromHostname(this.pageHostname) || this.pageHostname; + this.pageDomain = µm.URI.domainFromHostname(this.pageHostname) || this.pageHostname; this.pageScriptBlocked = false; this.thirdpartyScript = false; this.requests = µm.PageRequestStats.factory(); this.domains = {}; + this.allHostnamesString = ' '; this.state = {}; this.requestStats.reset(); this.distinctRequestCount = 0; this.perLoadAllowedRequestCount = 0; this.perLoadBlockedRequestCount = 0; - this.abpBlockCount = 0; return this; }; @@ -485,6 +473,7 @@ PageStore.prototype.dispose = function() { this.pageHostname = ''; this.pageDomain = ''; this.domains = {}; + this.allHostnamesString = ' '; this.state = {}; if ( pageStoreJunkyard.length < 8 ) { @@ -545,7 +534,10 @@ PageStore.prototype.recordRequest = function(type, url, block) { } this.distinctRequestCount++; - this.domains[hostname] = true; + if ( this.domains.hasOwnProperty(hostname) === false ) { + this.domains[hostname] = true; + this.allHostnamesString += hostname + ' '; + } µm.urlStatsChanged(this.pageUrl); // console.debug("HTTP Switchboard> PageStore.recordRequest(): %o: %s @ %s", this, type, url);