From 595de33e830806141521b70f330dd8b5e051d9ff Mon Sep 17 00:00:00 2001 From: gorhill Date: Sun, 19 Apr 2015 16:19:14 -0400 Subject: [PATCH] sanitizing outgoing headers (drafty) --- platform/chromium/vapi-background.js | 89 ++++++++++++++- platform/firefox/vapi-background.js | 106 +++++++++++++----- src/js/traffic.js | 162 +++++++-------------------- tools/make-chromium.sh | 2 +- 4 files changed, 209 insertions(+), 150 deletions(-) diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 05795d5..987bc45 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -614,7 +614,79 @@ vAPI.net = {}; vAPI.net.registerListeners = function() { var µm = µMatrix; var µmuri = µm.URI; + var httpRequestHeadersJunkyard = []; + + // Abstraction layer to deal with request headers + // >>>>>>>> + var httpRequestHeadersFactory = function(headers) { + var entry = httpRequestHeadersJunkyard.pop(); + if ( entry ) { + return entry.init(headers); + } + return new HTTPRequestHeaders(headers); + }; + + var HTTPRequestHeaders = function(headers) { + this.init(headers); + }; + + HTTPRequestHeaders.prototype.init = function(headers) { + this.modified = false; + this.headers = headers; + return this; + }; + + HTTPRequestHeaders.prototype.dispose = function() { + var r = this.modified ? this.headers : null; + this.headers = null; + httpRequestHeadersJunkyard.push(this); + return r; + }; + + HTTPRequestHeaders.prototype.getHeader = function(target) { + var headers = this.headers; + var header, name; + var i = headers.length; + while ( i-- ) { + header = headers[i]; + name = header.name.toLowerCase(); + if ( name === target ) { + return header.value; + } + } + return ''; + }; + HTTPRequestHeaders.prototype.setHeader = function(target, value, create) { + var headers = this.headers; + var header, name; + var i = headers.length; + while ( i-- ) { + header = headers[i]; + name = header.name.toLowerCase(); + if ( name === target ) { + break; + } + } + if ( i < 0 && !create ) { // Header not found, don't add it + return false; + } + if ( i < 0 ) { // Header not found, add it + headers.push({ name: target, value: value }); + } else if ( value === '' ) { // Header found, remove it + headers.splice(i, 1); + } else { // Header found, modify it + header.value = value; + } + this.modified = true; + return true; + }; + // <<<<<<<< + // End of: Abstraction layer to deal with request headers + + + // Normalizing request types + // >>>>>>>> var normalizeRequestDetails = function(details) { µmuri.set(details.url); @@ -651,7 +723,12 @@ vAPI.net.registerListeners = function() { // https://code.google.com/p/chromium/issues/detail?id=410382 details.type = 'object'; }; + // <<<<<<<< + // End of: Normalizing request types + + // Network event handlers + // >>>>>>>> var onBeforeRequestClient = this.onBeforeRequest.callback; var onBeforeRequest = function(details) { normalizeRequestDetails(details); @@ -675,7 +752,15 @@ vAPI.net.registerListeners = function() { var onBeforeSendHeadersClient = this.onBeforeSendHeaders.callback; var onBeforeSendHeaders = function(details) { normalizeRequestDetails(details); - return onBeforeSendHeadersClient(details); + details.requestHeaders = httpRequestHeadersFactory(details.requestHeaders); + var result = onBeforeSendHeadersClient(details); + if ( typeof result === 'object' ) { + return result; + } + var modifiedHeaders = details.requestHeaders.dispose(); + if ( modifiedHeaders !== null ) { + return { requestHeaders: modifiedHeaders }; + } }; chrome.webRequest.onBeforeSendHeaders.addListener( onBeforeSendHeaders, @@ -706,6 +791,8 @@ vAPI.net.registerListeners = function() { 'urls': this.onErrorOccurred.urls || [''] } ); + // <<<<<<<< + // End of: Network event handlers }; /******************************************************************************/ diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index b0c2295..38350e4 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -948,6 +948,52 @@ CallbackWrapper.prototype.proxy = function(response) { /******************************************************************************/ +var httpRequestHeadersFactory = function(channel) { + var entry = httpRequestHeadersFactory.junkyard.pop(); + if ( entry ) { + return entry.init(channel); + } + return new HTTPRequestHeaders(channel); +}; + +httpRequestHeadersFactory.junkyard = []; + +var HTTPRequestHeaders = function(channel) { + this.init(channel); +}; + +HTTPRequestHeaders.prototype.init = function(channel) { + this.channel = channel; + return this; +}; + +HTTPRequestHeaders.prototype.dispose = function() { + this.channel = null; + httpRequestHeadersFactory.junkyard.push(this); +}; + +HTTPRequestHeaders.prototype.getHeader = function(name) { + try { + return this.channel.getRequestHeader(name); + } catch (e) { + } + return ''; +}; + +HTTPRequestHeaders.prototype.setHeader = function(name, newValue, create) { + var oldValue = this.getHeader(name); + if ( newValue === oldValue ) { + return false; + } + if ( oldValue === '' && create !== true ) { + return false; + } + this.channel.setRequestHeader(name, newValue, false); + return true; +}; + +/******************************************************************************/ + var httpObserver = { classDescription: 'net-channel-event-sinks for ' + location.host, classID: Components.ID('{dc8d6319-5f6e-4438-999e-53722db99e84}'), @@ -967,9 +1013,11 @@ var httpObserver = { 5: 'object', 6: 'main_frame', 7: 'sub_frame', + 10: 'ping', 11: 'xmlhttprequest', 12: 'object', 14: 'font', + 16: 'websocket', 21: 'image' }, lastRequest: [{}, {}], @@ -1061,41 +1109,47 @@ var httpObserver = { }, handleRequest: function(channel, URI, details) { - var onBeforeRequest = vAPI.net.onBeforeRequest; var type = this.typeMap[details.type] || 'other'; - - if ( - onBeforeRequest.types.size !== 0 && - onBeforeRequest.types.has(type) === false - ) { - return false; - } - - var result = onBeforeRequest.callback({ + var result; + var callbackDetails = { frameId: details.frameId, hostname: URI.asciiHost, parentFrameId: details.parentFrameId, tabId: details.tabId, type: type, url: URI.asciiSpec - }); + }; - if ( !result || typeof result !== 'object' ) { - return false; - } + var onBeforeRequest = vAPI.net.onBeforeRequest; + if ( onBeforeRequest.types.size === 0 || onBeforeRequest.types.has(type) ) { + result = onBeforeRequest.callback(callbackDetails); - if ( result.cancel === true ) { - channel.cancel(this.ABORT); - return true; + if ( typeof result === 'object' && result.cancel === true ) { + channel.cancel(this.ABORT); + return true; + } + + /*if ( result.redirectUrl ) { + channel.redirectionLimit = 1; + channel.redirectTo( + Services.io.newURI(result.redirectUrl, null, null) + ); + return true; + }*/ } - /*if ( result.redirectUrl ) { - channel.redirectionLimit = 1; - channel.redirectTo( - Services.io.newURI(result.redirectUrl, null, null) - ); - return true; - }*/ + var onBeforeSendHeaders = vAPI.net.onBeforeSendHeaders; + if ( onBeforeSendHeaders.types.size === 0 || onBeforeSendHeaders.types.has(type) ) { + callbackDetails.requestHeaders = httpRequestHeadersFactory(channel); + result = onBeforeSendHeaders.callback(callbackDetails); + callbackDetails.requestHeaders.dispose(); + + if ( typeof result === 'object' && result.cancel === true ) { + channel.cancel(this.ABORT); + return true; + } + + } return false; }, @@ -1263,10 +1317,8 @@ vAPI.net = {}; /******************************************************************************/ vAPI.net.registerListeners = function() { - // Since it's not used - this.onBeforeSendHeaders = null; - this.onBeforeRequest.types = new Set(this.onBeforeRequest.types); + this.onBeforeSendHeaders.types = new Set(this.onBeforeSendHeaders.types); var shouldLoadListenerMessageName = location.host + ':shouldLoad'; var shouldLoadListener = function(e) { diff --git a/src/js/traffic.js b/src/js/traffic.js index 20952e0..c4558fa 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -272,7 +272,7 @@ var onBeforeRequestHandler = function(details) { // console.debug('onBeforeRequestHandler()> "%s": %o', details.url, details); - var requestType = requestTypeNormalizer[details.type]; + 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 @@ -284,7 +284,7 @@ var onBeforeRequestHandler = function(details) { var requestURL = details.url; // Is it µMatrix's noop css file? - if ( requestType === 'css' && requestURL.slice(0, µm.noopCSSURL.length) === µm.noopCSSURL ) { + if ( requestType === 'css' && requestURL.lastIndexOf(µm.noopCSSURL, 0) === 0 ) { return onBeforeChromeExtensionRequestHandler(details); } @@ -295,19 +295,12 @@ var onBeforeRequestHandler = function(details) { // 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 ) { + if ( requestType === 'xhr' && requestURL.lastIndexOf(µm.projectServerRoot, 0) === 0 ) { return; } var requestHostname = µmuri.hostname; - // rhill 2013-12-15: - // Try to transpose generic `other` category into something more - // meaningful. - if ( requestType === 'other' ) { - requestType = µm.transposeType(requestType, µ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 µMatrix, i.e. `opera://startpage`, @@ -361,11 +354,7 @@ var onBeforeRequestHandler = function(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. +// Sanitize outgoing headers as per user settings. var onBeforeSendHeadersHandler = function(details) { @@ -392,11 +381,27 @@ var onBeforeSendHeadersHandler = function(details) { // If yes, create a synthetic URL for reporting hyperlink auditing // in request log. This way the user is better informed of what went // on. + + // 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 requestURL = details.url; - var requestType = requestTypeNormalizer[details.type]; + var requestType = requestTypeNormalizer[details.type] || 'other'; if ( requestType === 'other' ) { - var linkAuditor = hyperlinkAuditorFromHeaders(details.requestHeaders); - if ( linkAuditor ) { + var linkAuditor = details.requestHeaders.getHeader('ping-to'); + if ( linkAuditor !== '' ) { var block = µm.userSettings.processHyperlinkAuditing; pageStore.recordRequest('other', requestURL + '{Ping-To:' + linkAuditor + '}', block); µm.updateBadgeAsync(tabId); @@ -411,123 +416,41 @@ var onBeforeSendHeadersHandler = function(details) { // is to sanitize headers. var reqHostname = µm.hostnameFromURL(requestURL); - var changed = false; if ( µm.mustBlock(pageStore.pageHostname, reqHostname, 'cookie') ) { - changed = foilCookieHeaders(µm, details) || changed; + if ( details.requestHeaders.setHeader('cookie', '') ) { + µm.cookieHeaderFoiledCounter++; + } } if ( µm.tMatrix.evaluateSwitchZ('referrer-spoof', pageStore.pageHostname) ) { - changed = foilRefererHeaders(µm, reqHostname, details) || changed; + foilRefererHeaders(µm, reqHostname, details); } if ( µm.tMatrix.evaluateSwitchZ('ua-spoof', pageStore.pageHostname) ) { - 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 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; + details.requestHeaders.setHeader('user-agent', µm.userAgentReplaceStr); } - return changed; }; /******************************************************************************/ var foilRefererHeaders = function(µm, toHostname, details) { - var headers = details.requestHeaders; - var i = headers.length, header; - while ( i-- ) { - header = headers[i]; - if ( header.name.toLowerCase() === 'referer' ) { - break; - } - } - if ( i === -1 ) { - return false; + var referer = details.requestHeaders.getHeader('referer'); + if ( referer === '' ) { + return; } var µmuri = µm.URI; - var fromDomain = µmuri.domainFromURI(header.value); - var toDomain = µmuri.domainFromHostname(toHostname); - if ( toDomain === fromDomain ) { - return false; + if ( µmuri.domainFromHostname(toHostname) === µmuri.domainFromURI(referer) ) { + return; } //console.debug('foilRefererHeaders()> foiled referer for "%s"', details.url); //console.debug('\treferrer "%s"', header.value); // https://github.com/gorhill/httpswitchboard/issues/222#issuecomment-44828402 - header.value = µmuri.schemeFromURI(details.url) + '://' + toHostname + '/'; - //console.debug('\treplaced with "%s"', header.value); + details.requestHeaders.setHeader( + 'referer', + µmuri.schemeFromURI(details.url) + '://' + toHostname + '/' + ); µm.refererHeaderFoiledCounter++; - return true; -}; - -/******************************************************************************/ - -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; }; /******************************************************************************/ @@ -549,7 +472,7 @@ var onHeadersReceived = function(details) { return; } - var requestType = requestTypeNormalizer[details.type]; + var requestType = requestTypeNormalizer[details.type] || 'other'; if ( requestType === 'frame' ) { return onSubDocHeadersReceived(details); } @@ -731,7 +654,7 @@ var onSubDocHeadersReceived = function(details) { var onErrorOccurredHandler = function(details) { // console.debug('onErrorOccurred()> "%s": %o', details.url, details); - var requestType = requestTypeNormalizer[details.type]; + var requestType = requestTypeNormalizer[details.type] || 'other'; // Ignore all that is not a main document if ( requestType !== 'doc'|| details.parentFrameId >= 0 ) { @@ -788,7 +711,8 @@ var requestTypeNormalizer = { 'image' : 'image', 'object' : 'plugin', 'xmlhttprequest': 'xhr', - 'other' : 'other' + 'other' : 'other', + 'font' : 'css' }; /******************************************************************************/ @@ -808,10 +732,6 @@ vAPI.net.onBeforeSendHeaders = { "http://*/*", "https://*/*" ], - types: [ - "main_frame", - "sub_frame" - ], extra: [ 'blocking', 'requestHeaders' ], callback: onBeforeSendHeadersHandler }; diff --git a/tools/make-chromium.sh b/tools/make-chromium.sh index aacc5e5..0053544 100755 --- a/tools/make-chromium.sh +++ b/tools/make-chromium.sh @@ -5,7 +5,7 @@ echo "*** µMatrix(Chromium): Creating package" echo "*** µMatrix(Chromium): Copying files" -DES=./dist/uMatrix.chromium +DES=./dist/build/uMatrix.chromium rm -rf $DES mkdir -p $DES