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.
941 lines
32 KiB
941 lines
32 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 */
|
|
|
|
/******************************************************************************/
|
|
|
|
// Start isolation from global scope
|
|
|
|
µMatrix.webRequest = (function() {
|
|
|
|
/******************************************************************************/
|
|
|
|
// The `id='uMatrix'` is important, it allows µMatrix to detect whether a
|
|
// specific data URI originates from itself.
|
|
|
|
var rootFrameReplacement = [
|
|
'<!DOCTYPE html><html id="uMatrix">',
|
|
'<head>',
|
|
'<meta charset="utf-8" />',
|
|
'<style>',
|
|
'@font-face {',
|
|
'font-family:httpsb;',
|
|
'font-style:normal;',
|
|
'font-weight:400;',
|
|
'src: local("httpsb"),url("',
|
|
µMatrix.fontCSSURL,
|
|
'") format("truetype");',
|
|
'}',
|
|
'body {',
|
|
'margin:0;',
|
|
'border:0;',
|
|
'padding:0;',
|
|
'font:15px httpsb,sans-serif;',
|
|
'width:100%;',
|
|
'height:100%;',
|
|
'background-color:transparent;',
|
|
'background-size:10px 10px;',
|
|
'background-image:',
|
|
'repeating-linear-gradient(',
|
|
'-45deg,',
|
|
'rgba(204,0,0,0.5),rgba(204,0,0,0.5) 24%,',
|
|
'transparent 26%,transparent 49%,',
|
|
'rgba(204,0,0,0.5) 51%,rgba(204,0,0,0.5) 74%,',
|
|
'transparent 76%,transparent',
|
|
');',
|
|
'text-align: center;',
|
|
'}',
|
|
'#p {',
|
|
'margin:8px;',
|
|
'padding:4px;',
|
|
'display:inline-block;',
|
|
'background-color:white;',
|
|
'}',
|
|
'#t {',
|
|
'margin:2px;',
|
|
'border:0;',
|
|
'padding:0 2px;',
|
|
'display:inline-block;',
|
|
'}',
|
|
'#t b {',
|
|
'padding:0 4px;',
|
|
'background-color:#eee;',
|
|
'font-weight:normal;',
|
|
'}',
|
|
'</style>',
|
|
'<link href="{{cssURL}}?url={{originalURL}}&hostname={{hostname}}&t={{now}}" rel="stylesheet" type="text/css">',
|
|
'<title>Blocked by µMatrix</title>',
|
|
'</head>',
|
|
'<body>',
|
|
'<div id="p">',
|
|
'<div id="t"><b>{{hostname}}</b> blocked by µMatrix</div>',
|
|
'</div>',
|
|
'</body>',
|
|
'</html>'
|
|
].join('');
|
|
|
|
var subFrameReplacement = [
|
|
'<!DOCTYPE html>',
|
|
'<html>',
|
|
'<head>',
|
|
'<meta charset="utf-8" />',
|
|
'<style>',
|
|
'@font-face{',
|
|
'font-family:httpsb;',
|
|
'font-style:normal;',
|
|
'font-weight:400;',
|
|
'src:local("httpsb"),url("',
|
|
µMatrix.fontCSSURL,
|
|
'") format("truetype");',
|
|
'}',
|
|
'body{',
|
|
'margin:0;',
|
|
'border:0;',
|
|
'padding:0;',
|
|
'font:13px httpsb,sans-serif;',
|
|
'}',
|
|
'#bg{',
|
|
'border:1px dotted {{subframeColor}};',
|
|
'position:absolute;',
|
|
'top:0;',
|
|
'right:0;',
|
|
'bottom:0;',
|
|
'left:0;',
|
|
'background-color:transparent;',
|
|
'background-size:10px 10px;',
|
|
'background-image:',
|
|
'repeating-linear-gradient(',
|
|
'-45deg,',
|
|
'{{subframeColor}},{{subframeColor}} 24%,',
|
|
'transparent 25%,transparent 49%,',
|
|
'{{subframeColor}} 50%,{{subframeColor}} 74%,',
|
|
'transparent 75%,transparent',
|
|
');',
|
|
'opacity:{{subframeOpacity}};',
|
|
'text-align:center;',
|
|
'}',
|
|
'#bg > div{',
|
|
'display:inline-block;',
|
|
'background-color:rgba(255,255,255,1);',
|
|
'}',
|
|
'#bg > div > a {',
|
|
'padding:0 2px;',
|
|
'display:inline-block;',
|
|
'color:white;',
|
|
'background-color:{{subframeColor}};',
|
|
'text-decoration:none;',
|
|
'}',
|
|
'</style>',
|
|
'<title>Blocked by µMatrix</title>',
|
|
'</head>',
|
|
'<body title="“{{hostname}}” frame\nblocked by µMatrix">',
|
|
'<div id="bg"><div><a href="{{frameSrc}}" target="_blank">{{hostname}}</a></div></div>',
|
|
'</body>',
|
|
'</html>'
|
|
].join('');
|
|
|
|
/******************************************************************************/
|
|
|
|
// If it is HTTP Switchboard's root frame replacement URL, verify that
|
|
// the page that was blacklisted is still blacklisted, and if not,
|
|
// redirect to the previously blacklisted page.
|
|
|
|
var onBeforeChromeExtensionRequestHandler = function(details) {
|
|
var requestURL = details.url;
|
|
|
|
// console.debug('onBeforeChromeExtensionRequestHandler()> "%s": %o', details.url, details);
|
|
|
|
// rhill 2013-12-10: Avoid regex whenever a faster indexOf() can be used:
|
|
// here we can use fast indexOf() as a first filter -- which is executed
|
|
// for every single request (so speed matters).
|
|
var matches = requestURL.match(/url=([^&]+)&hostname=([^&]+)/);
|
|
if ( !matches ) {
|
|
return;
|
|
}
|
|
|
|
var µm = µMatrix;
|
|
|
|
// Is the target page still blacklisted?
|
|
var pageURL = decodeURIComponent(matches[1]);
|
|
var hostname = decodeURIComponent(matches[2]);
|
|
if ( µm.mustBlock(µm.scopeFromURL(pageURL), hostname, 'doc') ) {
|
|
return;
|
|
}
|
|
|
|
µMatrix.asyncJobs.add(
|
|
'gotoURL-' + details.tabId,
|
|
{ tabId: details.tabId, url: pageURL },
|
|
µm.utils.gotoURL,
|
|
200,
|
|
false
|
|
);
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// Intercept and filter web requests according to white and black lists.
|
|
|
|
var onBeforeRootFrameRequestHandler = function(details) {
|
|
var µm = µMatrix;
|
|
|
|
// Do not ignore traffic outside tabs
|
|
var tabId = details.tabId;
|
|
if ( tabId < 0 ) {
|
|
tabId = µm.behindTheSceneTabId;
|
|
}
|
|
// It's a root frame, bind to a new page stats store
|
|
else {
|
|
µm.bindTabToPageStats(tabId, details.url);
|
|
}
|
|
|
|
var uri = µm.URI.set(details.url);
|
|
if ( uri.scheme !== 'http' && uri.scheme !== 'https' ) {
|
|
return;
|
|
}
|
|
|
|
var requestURL = uri.normalizedURI();
|
|
var requestHostname = uri.hostname;
|
|
var pageStats = µm.pageStatsFromTabId(tabId);
|
|
var pageURL = µm.pageUrlFromPageStats(pageStats);
|
|
var block = µm.mustBlock(pageStats.pageHostname, requestHostname, 'doc');
|
|
|
|
// console.debug('onBeforeRequestHandler()> block=%s "%s": %o', block, details.url, details);
|
|
|
|
// whitelisted?
|
|
if ( !block ) {
|
|
// rhill 2013-11-07: Senseless to do this for behind-the-scene requests.
|
|
// rhill 2013-12-03: Do this here only for root frames.
|
|
if ( tabId !== µm.behindTheSceneTabId ) {
|
|
µm.cookieHunter.recordPageCookies(pageStats);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// blacklisted
|
|
|
|
// rhill 2014-01-15: Delay logging of non-blocked top `main_frame`
|
|
// requests, in order to ensure any potential redirects is reported
|
|
// in proper chronological order.
|
|
// https://github.com/gorhill/httpswitchboard/issues/112
|
|
pageStats.recordRequest('doc', requestURL, block);
|
|
|
|
// If it's a blacklisted frame, redirect to frame.html
|
|
// rhill 2013-11-05: The root frame contains a link to noop.css, this
|
|
// allows to later check whether the root frame has been unblocked by the
|
|
// user, in which case we are able to force a reload using a redirect.
|
|
var html = rootFrameReplacement;
|
|
html = html.replace('{{cssURL}}', µm.noopCSSURL);
|
|
html = html.replace(/{{hostname}}/g, encodeURIComponent(requestHostname));
|
|
html = html.replace('{{originalURL}}', encodeURIComponent(requestURL));
|
|
html = html.replace('{{now}}', String(Date.now()));
|
|
var dataURI = 'data:text/html;base64,' + btoa(html);
|
|
|
|
return { 'redirectUrl': dataURI };
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// Process a request.
|
|
//
|
|
// This can be called from the context of onBeforeSendRequest() or
|
|
// onBeforeSendHeaders().
|
|
|
|
var processRequest = function(µm, details) {
|
|
var µmuri = µm.URI;
|
|
var requestType = requestTypeNormalizer[details.type];
|
|
var requestURL = µmuri.set(details.url).normalizedURI();
|
|
var requestHostname = µmuri.hostname;
|
|
var requestPath = µmuri.path;
|
|
|
|
// Re-classify orphan HTTP requests as behind-the-scene requests. There is
|
|
// not much else which can be done, because there are URLs
|
|
// which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`,
|
|
// as this would lead to complications with no obvious solution, like how
|
|
// to scope on unknown scheme? Etc.
|
|
// https://github.com/gorhill/httpswitchboard/issues/191
|
|
// https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
|
|
var pageStats = µm.pageStatsFromTabId(details.tabId);
|
|
if ( !pageStats ) {
|
|
pageStats = µm.pageStatsFromTabId(µm.behindTheSceneTabId);
|
|
}
|
|
var pageURL = µm.pageUrlFromPageStats(pageStats);
|
|
|
|
// rhill 2013-12-15:
|
|
// Try to transpose generic `other` category into something more
|
|
// meaningful.
|
|
if ( requestType === 'other' ) {
|
|
requestType = µm.transposeType(requestType, requestPath);
|
|
}
|
|
|
|
// Block request?
|
|
var block = µm.mustBlock(pageStats.pageHostname, requestHostname, requestType);
|
|
|
|
// Record request.
|
|
// https://github.com/gorhill/httpswitchboard/issues/342
|
|
// The way requests are handled now, it may happen at this point some
|
|
// processing has already been performed, and that a synthetic URL has
|
|
// been constructed for logging purpose. Use this synthetic URL if
|
|
// it is available.
|
|
pageStats.recordRequest(requestType, details.µmRequestURL || requestURL, block);
|
|
|
|
// whitelisted?
|
|
if ( !block ) {
|
|
// console.debug('onBeforeRequestHandler()> ALLOW "%s": %o', details.url, details);
|
|
return;
|
|
}
|
|
|
|
// blacklisted
|
|
// console.debug('onBeforeRequestHandler()> BLOCK "%s": %o', details.url, details);
|
|
|
|
// If it's a blacklisted frame, redirect to frame.html
|
|
// rhill 2013-11-05: The root frame contains a link to noop.css, this
|
|
// allows to later check whether the root frame has been unblocked by the
|
|
// user, in which case we are able to force a reload using a redirect.
|
|
if ( requestType === 'frame' ) {
|
|
var html = subFrameReplacement
|
|
.replace(/{{hostname}}/g, requestHostname)
|
|
.replace('{{frameSrc}}', requestURL)
|
|
.replace(/{{subframeColor}}/g, µm.userSettings.subframeColor)
|
|
.replace('{{subframeOpacity}}', (µm.userSettings.subframeOpacity / 100).toFixed(1));
|
|
return { 'redirectUrl': 'data:text/html,' + encodeURIComponent(html) };
|
|
}
|
|
|
|
return { 'cancel': true };
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// Intercept and filter web requests according to white and black lists.
|
|
|
|
var onBeforeRequestHandler = function(details) {
|
|
var µm = µMatrix;
|
|
var µmuri = µm.URI;
|
|
var requestURL = details.url;
|
|
var requestScheme = µmuri.schemeFromURI(requestURL);
|
|
|
|
// rhill 2014-02-17: Ignore 'filesystem:': this can happen when listening
|
|
// to 'chrome-extension://'.
|
|
if ( requestScheme === 'filesystem' ) {
|
|
return;
|
|
}
|
|
|
|
// console.debug('onBeforeRequestHandler()> "%s": %o', details.url, details);
|
|
|
|
var requestType = requestTypeNormalizer[details.type];
|
|
|
|
// https://github.com/gorhill/httpswitchboard/issues/303
|
|
// Wherever the main doc comes from, create a receiver page URL: synthetize
|
|
// one if needed.
|
|
if ( requestType === 'doc' && details.parentFrameId < 0 ) {
|
|
return onBeforeRootFrameRequestHandler(details);
|
|
}
|
|
|
|
// Is it µMatrix's noop css file?
|
|
if ( requestType === 'css' && requestURL.slice(0, µm.noopCSSURL.length) === µm.noopCSSURL ) {
|
|
return onBeforeChromeExtensionRequestHandler(details);
|
|
}
|
|
|
|
// Ignore non-http schemes
|
|
if ( requestScheme.indexOf('http') !== 0 ) {
|
|
return;
|
|
}
|
|
|
|
// Do not block myself from updating assets
|
|
// https://github.com/gorhill/httpswitchboard/issues/202
|
|
if ( requestType === 'xhr' && requestURL.slice(0, µm.projectServerRoot.length) === µm.projectServerRoot ) {
|
|
return;
|
|
}
|
|
|
|
// https://github.com/gorhill/httpswitchboard/issues/342
|
|
// If the request cannot be bound to a specific tab, delay request
|
|
// handling to onBeforeSendHeaders(), maybe there the referrer
|
|
// information will allow us to properly bind the request to the tab
|
|
// from which it originates.
|
|
// Important: since it is not possible to redirect requests at
|
|
// onBeforeSendHeaders() point, we can't delay when the request type is
|
|
// `sub_frame`.
|
|
if ( requestType !== 'frame' && details.tabId < 0 ) {
|
|
return;
|
|
}
|
|
|
|
return processRequest(µm, details);
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// This is where tabless requests are processed, as here there may be a chance
|
|
// we can bind a request to a specific tab, as headers may contain useful
|
|
// information to accomplish this.
|
|
//
|
|
// Also we sanitize outgoing headers as per user settings.
|
|
|
|
var onBeforeSendHeadersHandler = function(details) {
|
|
|
|
var µm = µMatrix;
|
|
var requestURL = details.url;
|
|
|
|
// console.debug('onBeforeSendHeadersHandler()> "%s": %o', details.url, details);
|
|
|
|
// Do not block myself from updating assets
|
|
// https://github.com/gorhill/httpswitchboard/issues/202
|
|
var requestType = requestTypeNormalizer[details.type];
|
|
if ( requestType === 'xhr' && requestURL.slice(0, µm.projectServerRoot.length) === µm.projectServerRoot ) {
|
|
return;
|
|
}
|
|
|
|
// https://github.com/gorhill/httpswitchboard/issues/342
|
|
// Is this hyperlink auditing?
|
|
// If yes, create a synthetic URL for reporting hyperlink auditing
|
|
// record requests. This way the user is better informed of what went
|
|
// on.
|
|
var linkAuditor = hyperlinkAuditorFromHeaders(details.requestHeaders);
|
|
if ( linkAuditor ) {
|
|
details.µmRequestURL = requestURL + '{Ping-To:' + linkAuditor + '}';
|
|
}
|
|
|
|
// If we are dealing with a behind-the-scene request, make a last attempt
|
|
// to bind the request to a specific tab by using the referrer or the
|
|
// `Ping-From` header if any of these exists.
|
|
var r;
|
|
if ( details.tabId < 0 ) {
|
|
details.tabId = tabIdFromHeaders(µm, details.requestHeaders) || -1;
|
|
// Do not process `main_frame`/`sub_frame` requests, these were handled
|
|
// unconditionally at onBeforeRequest() time (because of potential
|
|
// need to redirect).
|
|
if ( requestType !== 'doc' && requestType !== 'frame' ) {
|
|
r = processRequest(µm, details);
|
|
}
|
|
}
|
|
|
|
// If the request was not cancelled above, check whether hyperlink auditing
|
|
// is globally forbidden.
|
|
if ( !r ) {
|
|
if ( linkAuditor && µm.userSettings.processHyperlinkAuditing ) {
|
|
r = { 'cancel': true };
|
|
}
|
|
}
|
|
|
|
// Block request?
|
|
if ( r ) {
|
|
// Count number of hyperlink auditing foiled, even attempts blocked
|
|
// through the matrix.
|
|
if ( linkAuditor ) {
|
|
µm.hyperlinkAuditingFoiledCounter += 1;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
// If we reach this point, request is not blocked, so what is left to do
|
|
// is to sanitize headers.
|
|
|
|
// Re-classify orphan HTTP requests as behind-the-scene requests. There is
|
|
// not much else which can be done, because there are URLs
|
|
// which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`,
|
|
// as this would lead to complications with no obvious solution, like how
|
|
// to scope on unknown scheme? Etc.
|
|
// https://github.com/gorhill/httpswitchboard/issues/191
|
|
// https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
|
|
var tabId = details.tabId;
|
|
var pageStats = µm.pageStatsFromTabId(tabId);
|
|
if ( !pageStats ) {
|
|
tabId = µm.behindTheSceneTabId;
|
|
pageStats = µm.pageStatsFromTabId(tabId);
|
|
}
|
|
|
|
var pageURL = µm.pageUrlFromPageStats(pageStats);
|
|
var reqHostname = µm.hostnameFromURL(requestURL);
|
|
|
|
var changed = false;
|
|
|
|
if ( µm.mustBlock(pageStats.pageHostname, reqHostname, 'cookie') ) {
|
|
changed = foilCookieHeaders(µm, details) || changed;
|
|
}
|
|
|
|
// TODO: use cookie cell to determine whether the referrer info must be
|
|
// foiled.
|
|
if ( µm.userSettings.processReferer && µm.mustBlock(pageStats.pageHostname, reqHostname, '*') ) {
|
|
changed = foilRefererHeaders(µm, reqHostname, details) || changed;
|
|
}
|
|
|
|
if ( µm.userSettings.spoofUserAgent ) {
|
|
changed = foilUserAgent(µm, details) || changed;
|
|
// https://github.com/gorhill/httpswitchboard/issues/252
|
|
// To avoid potential mismatch between the user agent from HTTP headers
|
|
// and the user agent from subrequests and the window.navigator object,
|
|
// I could always store here the effective user agent, but I am really
|
|
// not convinced it is worth the added overhead given the low
|
|
// probability and the benign consequence if it ever happen. Can always
|
|
// be revised if ever I become aware a mismatch is a terrible thing
|
|
}
|
|
|
|
if ( changed ) {
|
|
// console.debug('onBeforeSendHeadersHandler()> CHANGED "%s": %o', requestURL, details);
|
|
return { requestHeaders: details.requestHeaders };
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#hyperlink-auditing
|
|
//
|
|
// Target URL = the href of the link
|
|
// Doc URL = URL of the document containing the target URL
|
|
// Ping URLs = servers which will be told that user clicked target URL
|
|
//
|
|
// `Content-Type` = `text/ping` (always present)
|
|
// `Ping-To` = target URL (always present)
|
|
// `Ping-From` = doc URL
|
|
// `Referer` = doc URL
|
|
// request URL = URL which will receive the information
|
|
//
|
|
// With hyperlink-auditing, removing header(s) is pointless, the whole
|
|
// request must be cancelled.
|
|
|
|
var hyperlinkAuditorFromHeaders = function(headers) {
|
|
var i = headers.length;
|
|
while ( i-- ) {
|
|
if ( headers[i].name.toLowerCase() === 'ping-to' ) {
|
|
return headers[i].value;
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
var tabIdFromHeaders = function(µm, headers) {
|
|
var header;
|
|
var i = headers.length;
|
|
while ( i-- ) {
|
|
header = headers[i];
|
|
if ( header.name.toLowerCase() === 'referer' ) {
|
|
return µm.tabIdFromPageUrl(header.value);
|
|
}
|
|
if ( header.name.toLowerCase() === 'ping-from' ) {
|
|
return µm.tabIdFromPageUrl(header.value);
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
var foilCookieHeaders = function(µm, details) {
|
|
var changed = false;
|
|
var headers = details.requestHeaders;
|
|
var header;
|
|
var i = headers.length;
|
|
while ( i-- ) {
|
|
header = headers[i];
|
|
if ( header.name.toLowerCase() !== 'cookie' ) {
|
|
continue;
|
|
}
|
|
// console.debug('foilCookieHeaders()> foiled browser attempt to send cookie(s) to "%s"', details.url);
|
|
headers.splice(i, 1);
|
|
µm.cookieHeaderFoiledCounter++;
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
var foilRefererHeaders = function(µm, toHostname, details) {
|
|
var headers = details.requestHeaders;
|
|
var header;
|
|
var fromDomain, toDomain;
|
|
var i = headers.length;
|
|
while ( i-- ) {
|
|
header = headers[i];
|
|
if ( header.name.toLowerCase() !== 'referer' ) {
|
|
continue;
|
|
}
|
|
fromDomain = µm.URI.domainFromURI(header.value);
|
|
if ( !toDomain ) {
|
|
toDomain = µm.URI.domainFromHostname(toHostname);
|
|
}
|
|
if ( toDomain === fromDomain ) {
|
|
continue;
|
|
}
|
|
// console.debug('foilRefererHeaders()> foiled referer "%s" for "%s"', fromDomain, toDomain);
|
|
// https://github.com/gorhill/httpswitchboard/issues/222#issuecomment-44828402
|
|
// Splicing instead of blanking: not sure how much it helps
|
|
headers.splice(i, 1);
|
|
µm.refererHeaderFoiledCounter++;
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
var foilUserAgent = function(µm, details) {
|
|
var headers = details.requestHeaders;
|
|
var header;
|
|
var i = 0;
|
|
while ( header = headers[i] ) {
|
|
if ( header.name.toLowerCase() === 'user-agent' ) {
|
|
header.value = µm.userAgentReplaceStr;
|
|
return true; // Assuming only one `user-agent` entry
|
|
}
|
|
i += 1;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// To prevent inline javascript from being executed.
|
|
|
|
// Prevent inline scripting using `Content-Security-Policy`:
|
|
// https://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html
|
|
|
|
// This fixes:
|
|
// https://github.com/gorhill/httpswitchboard/issues/35
|
|
|
|
var onHeadersReceived = function(details) {
|
|
|
|
// console.debug('onHeadersReceived()> "%s": %o', details.url, details);
|
|
|
|
// Ignore schemes other than 'http...'
|
|
if ( details.url.indexOf('http') !== 0 ) {
|
|
return;
|
|
}
|
|
|
|
var requestType = requestTypeNormalizer[details.type];
|
|
if ( requestType === 'frame' ) {
|
|
return onSubDocHeadersReceived(details);
|
|
}
|
|
if ( requestType === 'doc' ) {
|
|
return onMainDocHeadersReceived(details);
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
var onMainDocHeadersReceived = function(details) {
|
|
|
|
// console.debug('onMainDocHeadersReceived()> "%s": %o', details.url, details);
|
|
|
|
var µm = µMatrix;
|
|
|
|
// Do not ignore traffic outside tabs.
|
|
// https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
|
|
var tabId = details.tabId;
|
|
if ( tabId < 0 ) {
|
|
tabId = µm.behindTheSceneTabId;
|
|
}
|
|
|
|
var µmuri = µm.URI.set(details.url);
|
|
var requestURL = µmuri.normalizedURI();
|
|
var requestScheme = µmuri.scheme;
|
|
var requestHostname = µmuri.hostname;
|
|
|
|
// rhill 2013-12-07:
|
|
// Apparently in Opera, onBeforeRequest() is triggered while the
|
|
// URL is not yet bound to a tab (-1), which caused the code here
|
|
// to not be able to lookup the pageStats. So let the code here bind
|
|
// the page to a tab if not done yet.
|
|
// https://github.com/gorhill/httpswitchboard/issues/75
|
|
µm.bindTabToPageStats(tabId, requestURL);
|
|
|
|
// Re-classify orphan HTTP requests as behind-the-scene requests. There is
|
|
// not much else which can be done, because there are URLs
|
|
// which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`,
|
|
// as this would lead to complications with no obvious solution, like how
|
|
// to scope on unknown scheme? Etc.
|
|
// https://github.com/gorhill/httpswitchboard/issues/191
|
|
// https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
|
|
var pageStats = µm.pageStatsFromTabId(tabId);
|
|
if ( !pageStats ) {
|
|
tabId = µm.behindTheSceneTabId;
|
|
pageStats = µm.pageStatsFromTabId(tabId);
|
|
}
|
|
|
|
var headers = details.responseHeaders;
|
|
|
|
// Simplify code paths by splitting func in two different handlers, one
|
|
// for main docs, one for sub docs.
|
|
// rhill 2014-01-15: Report redirects.
|
|
// https://github.com/gorhill/httpswitchboard/issues/112
|
|
// rhill 2014-02-10: Handle all redirects.
|
|
// https://github.com/gorhill/httpswitchboard/issues/188
|
|
if ( /\s+30[12378]\s+/.test(details.statusLine) ) {
|
|
var i = headerIndexFromName('location', headers);
|
|
if ( i >= 0 ) {
|
|
// rhill 2014-01-20: Be ready to handle relative URLs.
|
|
// https://github.com/gorhill/httpswitchboard/issues/162
|
|
var locationURL = µmuri.set(headers[i].value.trim()).normalizedURI();
|
|
if ( µmuri.authority === '' ) {
|
|
locationURL = requestScheme + '://' + requestHostname + µmuri.path;
|
|
}
|
|
µm.redirectRequests[locationURL] = requestURL;
|
|
}
|
|
// console.debug('onMainDocHeadersReceived()> redirect "%s" to "%s"', requestURL, headers[i].value);
|
|
}
|
|
|
|
// rhill 2014-01-15: Report redirects if any.
|
|
// https://github.com/gorhill/httpswitchboard/issues/112
|
|
if ( details.statusLine.indexOf(' 200') > 0 ) {
|
|
var mainFrameStack = [requestURL];
|
|
var destinationURL = requestURL;
|
|
var sourceURL;
|
|
while ( sourceURL = µm.redirectRequests[destinationURL] ) {
|
|
mainFrameStack.push(sourceURL);
|
|
delete µm.redirectRequests[destinationURL];
|
|
destinationURL = sourceURL;
|
|
}
|
|
|
|
while ( destinationURL = mainFrameStack.pop() ) {
|
|
pageStats.recordRequest('doc', destinationURL, false);
|
|
}
|
|
}
|
|
|
|
// Evaluate
|
|
if ( µm.mustAllow(pageStats.pageHostname, requestHostname, 'script') ) {
|
|
// https://github.com/gorhill/httpswitchboard/issues/181
|
|
pageStats.pageScriptBlocked = false;
|
|
return;
|
|
}
|
|
|
|
// https://github.com/gorhill/httpswitchboard/issues/181
|
|
pageStats.pageScriptBlocked = true;
|
|
|
|
// If javascript not allowed, say so through a `Content-Security-Policy`
|
|
// directive.
|
|
// console.debug('onMainDocHeadersReceived()> PAGE CSP "%s": %o', details.url, details);
|
|
headers.push({
|
|
'name': 'Content-Security-Policy',
|
|
'value': "script-src 'none'"
|
|
});
|
|
|
|
return { responseHeaders: headers };
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
var onSubDocHeadersReceived = function(details) {
|
|
|
|
// console.debug('onSubDocHeadersReceived()> "%s": %o', details.url, details);
|
|
|
|
var µm = µMatrix;
|
|
|
|
// Do not ignore traffic outside tabs.
|
|
// https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
|
|
var tabId = details.tabId;
|
|
if ( tabId < 0 ) {
|
|
tabId = µm.behindTheSceneTabId;
|
|
}
|
|
|
|
// Re-classify orphan HTTP requests as behind-the-scene requests. There is
|
|
// not much else which can be done, because there are URLs
|
|
// which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`,
|
|
// as this would lead to complications with no obvious solution, like how
|
|
// to scope on unknown scheme? Etc.
|
|
// https://github.com/gorhill/httpswitchboard/issues/191
|
|
// https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
|
|
var pageStats = µm.pageStatsFromTabId(tabId);
|
|
if ( !pageStats ) {
|
|
tabId = µm.behindTheSceneTabId;
|
|
pageStats = µm.pageStatsFromTabId(tabId);
|
|
}
|
|
|
|
// Evaluate
|
|
if ( µm.mustAllow(pageStats.pageHostname, µm.hostnameFromURL(details.url), 'script') ) {
|
|
return;
|
|
}
|
|
|
|
// If javascript not allowed, say so through a `Content-Security-Policy`
|
|
// directive.
|
|
|
|
// For inline javascript within iframes, we need to sandbox.
|
|
|
|
// https://github.com/gorhill/httpswitchboard/issues/73
|
|
// Now because sandbox cancels all permissions, this means
|
|
// not just javascript is disabled. To avoid negative side
|
|
// effects, I allow some other permissions, but...
|
|
|
|
// https://github.com/gorhill/uMatrix/issues/27
|
|
// Need to add `allow-popups` to prevent completely breaking links on
|
|
// some sites old style sites.
|
|
|
|
// TODO: Reuse CSP `sandbox` directive if it's already in the
|
|
// headers (strip out `allow-scripts` if present),
|
|
// and find out if the `sandbox` in the header interfere with a
|
|
// `sandbox` attribute which might be present on the iframe.
|
|
|
|
// console.debug('onSubDocHeadersReceived()> FRAME CSP "%s": %o, scope="%s"', details.url, details, pageURL);
|
|
|
|
details.responseHeaders.push({
|
|
'name': 'Content-Security-Policy',
|
|
'value': 'sandbox allow-forms allow-same-origin allow-popups allow-top-navigation'
|
|
});
|
|
|
|
return { responseHeaders: details.responseHeaders };
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// As per Chrome API doc, webRequest.onErrorOccurred event is the last
|
|
// one called in the sequence of webRequest events.
|
|
// http://developer.chrome.com/extensions/webRequest.html
|
|
|
|
var onErrorOccurredHandler = function(details) {
|
|
// console.debug('onErrorOccurred()> "%s": %o', details.url, details);
|
|
var requestType = requestTypeNormalizer[details.type];
|
|
|
|
// Ignore all that is not a main document
|
|
if ( requestType !== 'doc'|| details.parentFrameId >= 0 ) {
|
|
return;
|
|
}
|
|
|
|
var µm = µMatrix;
|
|
var pageStats = µm.pageStatsFromPageUrl(details.url);
|
|
if ( !pageStats ) {
|
|
return;
|
|
}
|
|
|
|
// rhill 2014-01-28: Unwind the stack of redirects if any. Chromium will
|
|
// emit an error when a web page redirects apparently endlessly, so
|
|
// we need to unravel and report all these redirects upon error.
|
|
// https://github.com/gorhill/httpswitchboard/issues/171
|
|
var requestURL = µm.URI.set(details.url).normalizedURI();
|
|
var mainFrameStack = [requestURL];
|
|
var destinationURL = requestURL;
|
|
var sourceURL;
|
|
while ( sourceURL = µm.redirectRequests[destinationURL] ) {
|
|
mainFrameStack.push(sourceURL);
|
|
delete µm.redirectRequests[destinationURL];
|
|
destinationURL = sourceURL;
|
|
}
|
|
|
|
while ( destinationURL = mainFrameStack.pop() ) {
|
|
pageStats.recordRequest('doc', destinationURL, false);
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// Caller must ensure headerName is normalized to lower case.
|
|
|
|
var headerIndexFromName = function(headerName, headers) {
|
|
var i = headers.length;
|
|
while ( i-- ) {
|
|
if ( headers[i].name.toLowerCase() === headerName ) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
var requestTypeNormalizer = {
|
|
'main_frame' : 'doc',
|
|
'sub_frame' : 'frame',
|
|
'stylesheet' : 'css',
|
|
'script' : 'script',
|
|
'image' : 'image',
|
|
'object' : 'plugin',
|
|
'xmlhttprequest': 'xhr',
|
|
'other' : 'other'
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
var start = function() {
|
|
chrome.webRequest.onBeforeRequest.addListener(
|
|
//function(details) {
|
|
// quickProfiler.start('onBeforeRequest');
|
|
// var r = onBeforeRequestHandler(details);
|
|
// quickProfiler.stop();
|
|
// return r;
|
|
//},
|
|
onBeforeRequestHandler,
|
|
{
|
|
"urls": [
|
|
"http://*/*",
|
|
"https://*/*",
|
|
"chrome-extension://*/*"
|
|
],
|
|
"types": [
|
|
"main_frame",
|
|
"sub_frame",
|
|
'stylesheet',
|
|
"script",
|
|
"image",
|
|
"object",
|
|
"xmlhttprequest",
|
|
"other"
|
|
]
|
|
},
|
|
[ "blocking" ]
|
|
);
|
|
|
|
//console.log('µMatrix > Beginning to intercept net requests at %s', (new Date()).toISOString());
|
|
|
|
chrome.webRequest.onBeforeSendHeaders.addListener(
|
|
onBeforeSendHeadersHandler,
|
|
{
|
|
'urls': [
|
|
"http://*/*",
|
|
"https://*/*"
|
|
]
|
|
},
|
|
['blocking', 'requestHeaders']
|
|
);
|
|
|
|
chrome.webRequest.onHeadersReceived.addListener(
|
|
onHeadersReceived,
|
|
{
|
|
'urls': [
|
|
"http://*/*",
|
|
"https://*/*"
|
|
]
|
|
},
|
|
['blocking', 'responseHeaders']
|
|
);
|
|
|
|
chrome.webRequest.onErrorOccurred.addListener(
|
|
onErrorOccurredHandler,
|
|
{
|
|
'urls': [
|
|
"http://*/*",
|
|
"https://*/*"
|
|
]
|
|
}
|
|
);
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
return {
|
|
blockedRootFramePrefix: 'data:text/html;base64,' + btoa(rootFrameReplacement).slice(0, 80),
|
|
start: start
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
})();
|
|
|
|
/******************************************************************************/
|
|
|