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.
459 lines
15 KiB
459 lines
15 KiB
/*******************************************************************************
|
|
|
|
µMatrix - a Chromium browser extension to black/white list requests.
|
|
Copyright (C) 2014 Raymond Hill
|
|
|
|
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://github.com/gorhill/uMatrix
|
|
*/
|
|
|
|
/* global chrome, µMatrix */
|
|
|
|
/******************************************************************************/
|
|
|
|
// Create a new page url stats store (if not already present)
|
|
|
|
µMatrix.createPageStore = function(pageURL) {
|
|
// https://github.com/gorhill/httpswitchboard/issues/303
|
|
// At this point, the URL has been page-URL-normalized
|
|
|
|
// do not create stats store for urls which are of no interest
|
|
if ( pageURL.search(/^https?/) !== 0 ) {
|
|
return;
|
|
}
|
|
var pageStore = null;
|
|
if ( this.pageStats.hasOwnProperty(pageURL) ) {
|
|
pageStore = this.pageStats[pageURL];
|
|
}
|
|
if ( pageStore === null ) {
|
|
pageStore = this.PageStore.factory(pageURL);
|
|
// These counters are used so that icon presents an overview of how
|
|
// much allowed/blocked.
|
|
pageStore.perLoadAllowedRequestCount =
|
|
pageStore.perLoadBlockedRequestCount = 0;
|
|
this.pageStats[pageURL] = pageStore;
|
|
}
|
|
|
|
// TODO: revisit code, need to account for those web pages for which the
|
|
// URL changes with the content only updated
|
|
if ( pageStore.pageUrl !== pageURL ) {
|
|
pageStore.init(pageURL);
|
|
}
|
|
|
|
return pageStore;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// https://github.com/gorhill/httpswitchboard/issues/303
|
|
// Some kind of trick going on here:
|
|
// Any scheme other than 'http' and 'https' is remapped into a fake
|
|
// URL which trick the rest of µMatrix into being able to process an
|
|
// otherwise unmanageable scheme. µMatrix needs web pages to have a proper
|
|
// hostname to work properly, so just like the 'chromium-behind-the-scene'
|
|
// fake domain name, we map unknown schemes into a fake '{scheme}-scheme'
|
|
// hostname. This way, for a specific scheme you can create scope with
|
|
// rules which will apply only to that scheme.
|
|
|
|
µMatrix.normalizePageURL = function(pageURL) {
|
|
var uri = this.URI.set(pageURL);
|
|
if ( uri.scheme === 'https' || uri.scheme === 'http' ) {
|
|
return uri.normalizedURI();
|
|
}
|
|
// If it is a scheme-based page URL, it is important it is crafted as a
|
|
// normalized URL just like above.
|
|
if ( uri.scheme !== '' ) {
|
|
return 'http://' + uri.scheme + '-scheme/';
|
|
}
|
|
return '';
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// Create an entry for the tab if it doesn't exist
|
|
|
|
µMatrix.bindTabToPageStats = function(tabId, pageURL, context) {
|
|
// https://github.com/gorhill/httpswitchboard/issues/303
|
|
// Don't rebind pages blocked by µMatrix.
|
|
var blockedRootFramePrefix = this.webRequest.blockedRootFramePrefix;
|
|
if ( pageURL.slice(0, blockedRootFramePrefix.length) === blockedRootFramePrefix ) {
|
|
return null;
|
|
}
|
|
|
|
// https://github.com/gorhill/httpswitchboard/issues/303
|
|
// Normalize to a page-URL.
|
|
pageURL = this.normalizePageURL(pageURL);
|
|
|
|
// The page URL, if any, currently associated with the tab
|
|
var previousPageURL = this.tabIdToPageUrl[tabId];
|
|
if ( previousPageURL === pageURL ) {
|
|
return this.pageStats[pageURL];
|
|
}
|
|
|
|
// https://github.com/gorhill/uMatrix/issues/37
|
|
// Just rebind: the URL changed, but the document itself is the same.
|
|
// Example: Google Maps, Github
|
|
var pageStore;
|
|
if ( context === 'pageUpdated' && this.pageStats.hasOwnProperty(previousPageURL) ) {
|
|
pageStore = this.pageStats[previousPageURL];
|
|
pageStore.pageUrl = pageURL;
|
|
delete this.pageStats[previousPageURL];
|
|
this.pageStats[pageURL] = pageStore;
|
|
delete this.pageUrlToTabId[previousPageURL];
|
|
this.pageUrlToTabId[pageURL] = tabId;
|
|
this.tabIdToPageUrl[tabId] = pageURL;
|
|
return pageStore;
|
|
}
|
|
|
|
pageStore = this.createPageStore(pageURL, context);
|
|
|
|
// console.debug('tab.js > bindTabToPageStats(): dispatching traffic in tab id %d to page store "%s"', tabId, pageUrl);
|
|
|
|
// rhill 2013-11-24: Never ever rebind chromium-behind-the-scene
|
|
// virtual tab.
|
|
// https://github.com/gorhill/httpswitchboard/issues/67
|
|
if ( tabId === this.behindTheSceneTabId ) {
|
|
return pageStore;
|
|
}
|
|
|
|
// https://github.com/gorhill/uMatrix/issues/37
|
|
this.updateBadgeAsync(pageURL);
|
|
|
|
this.unbindTabFromPageStats(tabId);
|
|
|
|
// rhill 2014-02-08: Do not create an entry if no page store
|
|
// exists (like when visiting about:blank)
|
|
// https://github.com/gorhill/httpswitchboard/issues/186
|
|
if ( !pageStore ) {
|
|
return null;
|
|
}
|
|
|
|
this.pageUrlToTabId[pageURL] = tabId;
|
|
this.tabIdToPageUrl[tabId] = pageURL;
|
|
pageStore.boundCount += 1;
|
|
|
|
return pageStore;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
µMatrix.unbindTabFromPageStats = function(tabId) {
|
|
if ( this.tabIdToPageUrl.hasOwnProperty(tabId) === false ) {
|
|
return;
|
|
}
|
|
var pageURL = this.tabIdToPageUrl[tabId];
|
|
if ( this.pageStats.hasOwnProperty(pageURL) ) {
|
|
var pageStore = this.pageStats[pageURL];
|
|
pageStore.boundCount -= 1;
|
|
if ( pageStore.boundCount === 0 ) {
|
|
pageStore.obsoleteAfter = Date.now() + (5 * 60 * 1000);
|
|
}
|
|
}
|
|
delete this.tabIdToPageUrl[tabId];
|
|
delete this.pageUrlToTabId[pageURL];
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// Log a request
|
|
|
|
µMatrix.recordFromTabId = function(tabId, type, url, blocked) {
|
|
var pageStats = this.pageStatsFromTabId(tabId);
|
|
if ( pageStats ) {
|
|
pageStats.recordRequest(type, url, blocked);
|
|
}
|
|
};
|
|
|
|
µMatrix.recordFromPageUrl = function(pageUrl, type, url, blocked, reason) {
|
|
var pageStats = this.pageStatsFromPageUrl(pageUrl);
|
|
if ( pageStats ) {
|
|
pageStats.recordRequest(type, url, blocked, reason);
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
µMatrix.onPageLoadCompleted = function(pageURL) {
|
|
var pageStats = this.pageStatsFromPageUrl(pageURL);
|
|
if ( !pageStats ) {
|
|
return;
|
|
}
|
|
|
|
// https://github.com/gorhill/httpswitchboard/issues/181
|
|
if ( pageStats.thirdpartyScript ) {
|
|
pageStats.recordRequest('script', pageURL + '{3rd-party_scripts}', pageStats.pageScriptBlocked);
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// Reload content of a tabs.
|
|
|
|
µMatrix.smartReloadTabs = function(which, tabId) {
|
|
if ( which === 'none' ) {
|
|
return;
|
|
}
|
|
|
|
if ( which === 'current' && typeof tabId === 'number' ) {
|
|
this.smartReloadTab(tabId);
|
|
return;
|
|
}
|
|
|
|
// which === 'all'
|
|
var reloadTabs = function(chromeTabs) {
|
|
var µm = µMatrix;
|
|
var tabId;
|
|
var i = chromeTabs.length;
|
|
while ( i-- ) {
|
|
tabId = chromeTabs[i].id;
|
|
if ( µm.tabExists(tabId) ) {
|
|
µm.smartReloadTab(tabId);
|
|
}
|
|
}
|
|
};
|
|
|
|
var getTabs = function() {
|
|
chrome.tabs.query({ status: 'complete' }, reloadTabs);
|
|
};
|
|
|
|
this.asyncJobs.add('smartReloadTabs', null, getTabs, 500);
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// Reload content of a tab
|
|
|
|
µMatrix.smartReloadTab = function(tabId) {
|
|
var pageStats = this.pageStatsFromTabId(tabId);
|
|
if ( !pageStats ) {
|
|
//console.error('HTTP Switchboard> µMatrix.smartReloadTab(): page stats for tab id %d not found', tabId);
|
|
return;
|
|
}
|
|
|
|
// rhill 2013-12-23: Reload only if something previously blocked is now
|
|
// unblocked.
|
|
var blockRule;
|
|
var oldState = pageStats.state;
|
|
var newState = this.computeTabState(tabId);
|
|
var mustReload = false;
|
|
for ( blockRule in oldState ) {
|
|
if ( !oldState.hasOwnProperty(blockRule) ) {
|
|
continue;
|
|
}
|
|
// General rule, reload...
|
|
// If something previously blocked is no longer blocked.
|
|
if ( !newState[blockRule] ) {
|
|
// console.debug('tab.js > µMatrix.smartReloadTab(): will reload because "%s" is no longer blocked', blockRule);
|
|
mustReload = true;
|
|
break;
|
|
}
|
|
}
|
|
// Exceptions: blocking these previously unblocked types must result in a
|
|
// reload:
|
|
// - a script
|
|
// - a frame
|
|
// Related issues:
|
|
// https://github.com/gorhill/httpswitchboard/issues/94
|
|
// https://github.com/gorhill/httpswitchboard/issues/141
|
|
if ( !mustReload ) {
|
|
var reloadNewlyBlockedTypes = {
|
|
'doc': true,
|
|
'script' : true,
|
|
'frame': true
|
|
};
|
|
var blockRuleType;
|
|
for ( blockRule in newState ) {
|
|
if ( !newState.hasOwnProperty(blockRule) ) {
|
|
continue;
|
|
}
|
|
blockRuleType = blockRule.slice(0, blockRule.indexOf('|'));
|
|
if ( !reloadNewlyBlockedTypes[blockRuleType] ) {
|
|
continue;
|
|
}
|
|
if ( !oldState[blockRule] ) {
|
|
// console.debug('tab.js > µMatrix.smartReloadTab(): will reload because "%s" is now blocked', blockRule);
|
|
mustReload = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// console.log('old state: %o\nnew state: %o', oldState, newState);
|
|
|
|
if ( mustReload ) {
|
|
chrome.tabs.reload(tabId);
|
|
}
|
|
// pageStats.state = newState;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// Required since not all tabs are of interests to HTTP Switchboard.
|
|
// Examples:
|
|
// `chrome://extensions/`
|
|
// `chrome-devtools://devtools/devtools.html`
|
|
// etc.
|
|
|
|
µMatrix.tabExists = function(tabId) {
|
|
return !!this.pageUrlFromTabId(tabId);
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
µMatrix.computeTabState = function(tabId) {
|
|
var pageStats = this.pageStatsFromTabId(tabId);
|
|
if ( !pageStats ) {
|
|
//console.error('tab.js > µMatrix.computeTabState(): page stats for tab id %d not found', tabId);
|
|
return {};
|
|
}
|
|
// Go through all recorded requests, apply filters to create state
|
|
// It is a critical error for a tab to not be defined here
|
|
var pageURL = pageStats.pageUrl;
|
|
var srcHostname = this.scopeFromURL(pageURL);
|
|
var requestDict = pageStats.requests.getRequestDict();
|
|
var computedState = {};
|
|
var desHostname, type;
|
|
for ( var reqKey in requestDict ) {
|
|
if ( !requestDict.hasOwnProperty(reqKey) ) {
|
|
continue;
|
|
}
|
|
|
|
// The evaluation code here needs to reflect the evaluation code in
|
|
// beforeRequestHandler()
|
|
desHostname = this.PageRequestStats.hostnameFromRequestKey(reqKey);
|
|
|
|
// rhill 2013-12-10: mind how stylesheets are to be evaluated:
|
|
// `stylesheet` or `other`? Depends of domain of request.
|
|
// https://github.com/gorhill/httpswitchboard/issues/85
|
|
type = this.PageRequestStats.typeFromRequestKey(reqKey);
|
|
if ( this.mustBlock(srcHostname, desHostname, type) ) {
|
|
computedState[type + '|' + desHostname] = true;
|
|
}
|
|
}
|
|
return computedState;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
µMatrix.tabIdFromPageUrl = function(pageURL) {
|
|
// https://github.com/gorhill/httpswitchboard/issues/303
|
|
// Normalize to a page-URL.
|
|
return this.pageUrlToTabId[this.normalizePageURL(pageURL)];
|
|
};
|
|
|
|
µMatrix.tabIdFromPageStats = function(pageStats) {
|
|
return this.tabIdFromPageUrl(pageStats.pageUrl);
|
|
};
|
|
|
|
µMatrix.pageUrlFromTabId = function(tabId) {
|
|
return this.tabIdToPageUrl[tabId];
|
|
};
|
|
|
|
µMatrix.pageUrlFromPageStats = function(pageStats) {
|
|
if ( pageStats ) {
|
|
return pageStats.pageUrl;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
µMatrix.pageStatsFromTabId = function(tabId) {
|
|
var pageUrl = this.tabIdToPageUrl[tabId];
|
|
if ( pageUrl ) {
|
|
return this.pageStats[pageUrl];
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
µMatrix.pageStatsFromPageUrl = function(pageURL) {
|
|
if ( pageURL ) {
|
|
return this.pageStats[this.normalizePageURL(pageURL)];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
µMatrix.resizeLogBuffers = function(size) {
|
|
var pageStores = this.pageStats;
|
|
for ( var pageURL in pageStores ) {
|
|
if ( pageStores.hasOwnProperty(pageURL) ) {
|
|
pageStores[pageURL].requests.resizeLogBuffer(size);
|
|
}
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
µMatrix.forceReload = function(pageURL) {
|
|
var tabId = this.tabIdFromPageUrl(pageURL);
|
|
if ( tabId ) {
|
|
chrome.tabs.reload(tabId, { bypassCache: true });
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// Garbage collect stale url stats entries
|
|
(function() {
|
|
var µm = µMatrix;
|
|
var gcPageStats = function() {
|
|
var pageStore;
|
|
var now = Date.now();
|
|
for ( var pageURL in µm.pageStats ) {
|
|
if ( µm.pageStats.hasOwnProperty(pageURL) === false ) {
|
|
continue;
|
|
}
|
|
pageStore = µm.pageStats[pageURL];
|
|
if ( pageStore.boundCount !== 0 ) {
|
|
continue;
|
|
}
|
|
if ( pageStore.obsoleteAfter > now ) {
|
|
continue;
|
|
}
|
|
µm.cookieHunter.removePageCookies(pageStore);
|
|
pageStore.dispose();
|
|
delete µm.pageStats[pageURL];
|
|
}
|
|
|
|
// Prune content of chromium-behind-the-scene virtual tab
|
|
// When `suggest-as-you-type` is on in Chromium, this can lead to a
|
|
// LOT of uninteresting behind the scene requests.
|
|
pageStore = µm.pageStats[µm.behindTheSceneURL];
|
|
if ( !pageStore ) {
|
|
return;
|
|
}
|
|
var reqKeys = pageStore.requests.getRequestKeys();
|
|
if ( reqKeys.length <= µm.behindTheSceneMaxReq ) {
|
|
return;
|
|
}
|
|
reqKeys = reqKeys.sort(function(a,b){
|
|
return pageStore.requests[b] - pageStore.requests[a];
|
|
}).slice(µm.behindTheSceneMaxReq);
|
|
var iReqKey = reqKeys.length;
|
|
while ( iReqKey-- ) {
|
|
pageStore.requests.disposeOne(reqKeys[iReqKey]);
|
|
}
|
|
};
|
|
|
|
// Time somewhat arbitrary: If a web page has not been in a tab
|
|
// for some time minutes, flush its stats.
|
|
µMatrix.asyncJobs.add(
|
|
'gcPageStats',
|
|
null,
|
|
gcPageStats,
|
|
(2.5 * 60 * 1000) | 0,
|
|
true
|
|
);
|
|
})();
|