sanitizing outgoing headers (drafty)

pull/2/head
gorhill 10 years ago
parent 5106c4bc44
commit 595de33e83

@ -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 || ['<all_urls>']
}
);
// <<<<<<<<
// End of: Network event handlers
};
/******************************************************************************/

@ -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) {

@ -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
};

@ -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

Loading…
Cancel
Save