/******************************************************************************* ηMatrix - a browser extension to black/white list requests. Copyright (C) 2014-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.org/heckyel/ematrix uMatrix Home: https://github.com/gorhill/uMatrix */ 'use strict'; /******************************************************************************/ ηMatrix.webRequest = (function() { Cu.import('chrome://ematrix/content/lib/UriTools.jsm'); // Intercept and filter web requests according to white and black lists. var onBeforeRootFrameRequestHandler = function (details) { let ηm = ηMatrix; let requestURL = details.url; let requestHostname = UriTools.hostnameFromURI(requestURL); let tabId = details.tabId; ηm.tabContextManager.push(tabId, requestURL); let tabContext = ηm.tabContextManager.mustLookup(tabId); let rootHostname = tabContext.rootHostname; // Disallow request as per matrix? let block = ηm.mustBlock(rootHostname, requestHostname, 'doc'); let pageStore = ηm.pageStoreFromTabId(tabId); pageStore.recordRequest('doc', requestURL, block); ηm.logger.writeOne(tabId, 'net', rootHostname, requestURL, 'doc', block); // Not blocked if (!block) { // rhill 2013-11-07: Senseless to do this for // behind-the-scene requests. ηm.cookieHunter.recordPageCookies(pageStore); return; } // Blocked var query = btoa(JSON.stringify({ url: requestURL, hn: requestHostname, why: '?' })); vAPI.tabs.replace(tabId, vAPI.getURL('main-blocked.html?details=') + query); return { cancel: true }; }; // Intercept and filter web requests according to white and black lists. var onBeforeRequestHandler = function (details) { let ηm = ηMatrix; let requestURL = details.url; let requestScheme = UriTools.schemeFromURI(requestURL); if (UriTools.isNetworkScheme(requestScheme) === false) { return; } var requestType = requestTypeNormalizer[details.type] || 'other'; // 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 === -1) { return onBeforeRootFrameRequestHandler(details); } // 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 ηMatrix, // 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 let tabContext = ηm.tabContextManager.mustLookup(details.tabId); let tabId = tabContext.tabId; let rootHostname = tabContext.rootHostname; let specificity = 0; // Filter through matrix let block = ηm.tMatrix.mustBlock(rootHostname, UriTools.hostnameFromURI(requestURL), requestType); if (block) { specificity = ηm.tMatrix.specificityRegister; } // 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. let pageStore = ηm.mustPageStoreFromTabId(tabId); // Enforce strict secure connection? if (tabContext.secure && UriTools.isSecureScheme(requestScheme) === false) { pageStore.hasMixedContent = true; if (block === false) { block = ηm.tMatrix.evaluateSwitchZ('https-strict', rootHostname); } } pageStore.recordRequest(requestType, requestURL, block); ηm.logger.writeOne(tabId, 'net', rootHostname, requestURL, details.type, block); if (block) { pageStore.cacheBlockedCollapsible(requestType, requestURL, specificity); return { 'cancel': true }; } }; // Sanitize outgoing headers as per user settings. var onBeforeSendHeadersHandler = function (details) { let ηm = ηMatrix; let requestURL = details.url; let requestScheme = UriTools.schemeFromURI(requestURL); // Ignore non-network schemes if (UriTools.isNetworkScheme(requestScheme) === false) { return; } // 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 let tabId = details.tabId; let pageStore = ηm.mustPageStoreFromTabId(tabId); let requestType = requestTypeNormalizer[details.type] || 'other'; let requestHeaders = details.requestHeaders; let headerIndex, headerValue; // https://github.com/gorhill/httpswitchboard/issues/342 // Is this hyperlink auditing? If yes, create a synthetic URL // for reporting hyperlink auditing in request log. This way // the user is better informed of what went on. // https://html.spec.whatwg.org/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. headerIndex = headerIndexFromName('ping-to', requestHeaders); if (headerIndex !== -1) { headerValue = requestHeaders[headerIndex].value; if (headerValue !== '') { let block = ηm.userSettings.processHyperlinkAuditing; pageStore.recordRequest('other', requestURL + '{Ping-To:' + headerValue + '}', block); ηm.logger.writeOne(tabId, 'net', '', requestURL, 'ping', block); if (block) { ηm.hyperlinkAuditingFoiledCounter += 1; return { 'cancel': true }; } } } // If we reach this point, request is not blocked, so what is // left to do is to sanitize headers. let rootHostname = pageStore.pageHostname; let requestHostname = UriTools.hostnameFromURI(requestURL); let modified = false; // Process `Cookie` header. headerIndex = headerIndexFromName('cookie', requestHeaders); if (headerIndex !== -1 && ηm.mustBlock(rootHostname, requestHostname, 'cookie')) { modified = true; headerValue = requestHeaders[headerIndex].value; requestHeaders.splice(headerIndex, 1); ηm.cookieHeaderFoiledCounter++; if (requestType === 'doc') { ηm.logger.writeOne(tabId, 'net', '', headerValue, 'COOKIE', true); } } // Process `Referer` header. // https://github.com/gorhill/httpswitchboard/issues/222#issuecomment-44828402 // https://github.com/gorhill/uMatrix/issues/320 // http://tools.ietf.org/html/rfc6454#section-7.3 // "The user agent MAY include an Origin header field in any HTTP // "request. // "The user agent MUST NOT include more than one Origin header field in // "any HTTP request. // "Whenever a user agent issues an HTTP request from a "privacy- // "sensitive" context, the user agent MUST send the value "null" in the // "Origin header field." // https://github.com/gorhill/uMatrix/issues/358 // Do not spoof `Origin` header for the time being. // https://github.com/gorhill/uMatrix/issues/773 // For non-GET requests, remove `Referer` header instead of spoofing it. headerIndex = headerIndexFromName('referer', requestHeaders); if (headerIndex !== -1) { headerValue = requestHeaders[headerIndex].value; if (headerValue !== '') { let toDomain = UriTools.domainFromHostname(requestHostname); if (toDomain !== '' && toDomain !== UriTools.domainFromURI(headerValue)) { pageStore.has3pReferrer = true; if (ηm.tMatrix.evaluateSwitchZ('referrer-spoof', rootHostname)) { modified = true; let newValue; if (details.method === 'GET') { newValue = requestHeaders[headerIndex].value = requestScheme + '://' + requestHostname + '/'; } else { requestHeaders.splice(headerIndex, 1); } ηm.refererHeaderFoiledCounter++; if (requestType === 'doc') { ηm.logger.writeOne(tabId, 'net', '', headerValue, 'REFERER', true); if (newValue !== undefined) { ηm.logger.writeOne(tabId, 'net', '', newValue, 'REFERER', false); } } } } } } if (modified) { return { requestHeaders: requestHeaders }; } }; // 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) { // Ignore schemes other than 'http...' let ηm = ηMatrix; let tabId = details.tabId; let requestURL = details.url; let requestType = requestTypeNormalizer[details.type] || 'other'; // https://github.com/gorhill/uMatrix/issues/145 // Check if the main_frame is a download if (requestType === 'doc') { ηm.tabContextManager.push(tabId, requestURL); } let tabContext = ηm.tabContextManager.lookup(tabId); if (tabContext === null) { return; } let csp = []; let cspReport = []; let rootHostname = tabContext.rootHostname; let requestHostname = UriTools.hostnameFromURI(requestURL); // Inline script tags. if (ηm.mustAllow(rootHostname, requestHostname, 'script') !== true) { csp.push(ηm.cspNoInlineScript); } // Inline style tags. if (ηm.mustAllow(rootHostname, requestHostname, 'css') !== true) { csp.push(ηm.cspNoInlineStyle); } // https://bugzilla.mozilla.org/show_bug.cgi?id=1302667 let cspNoWorker = ηm.cspNoWorker; if (cspNoWorker === undefined) { cspNoWorker = cspNoWorkerInit(); } if (ηm.tMatrix.evaluateSwitchZ('no-workers', rootHostname)) { csp.push(cspNoWorker); } else if (ηm.rawSettings.disableCSPReportInjection === false) { cspReport.push(cspNoWorker); } let headers = details.responseHeaders; let cspDirectives, i; if (csp.length !== 0) { cspDirectives = csp.join(','); i = headerIndexFromName('content-security-policy', headers); if (i !== -1) { headers[i].value += ',' + cspDirectives; } else { headers.push({ name: 'Content-Security-Policy', value: cspDirectives }); } if (requestType === 'doc') { ηm.logger.writeOne(tabId, 'net', '', cspDirectives, 'CSP', false); } } if (cspReport.length !== 0) { cspDirectives = cspReport.join(','); i = headerIndexFromName('content-security-policy-report-only', headers); if (i !== -1) { headers[i].value += ',' + cspDirectives; } else { headers.push({ name: 'Content-Security-Policy-Report-Only', value: cspDirectives }); } } return { responseHeaders: headers }; }; let cspNoWorkerInit = function () { if (ηMatrix.cspNoWorker === undefined) { ηMatrix.cspNoWorker = "worker-src 'none'; " +"frame-src data: blob: *; " +"report-uri about:blank"; } return ηMatrix.cspNoWorker; }; // Caller must ensure headerName is normalized to lower case. let headerIndexFromName = function (headerName, headers) { for (let i=0; i