2015-11-23 13:52:50 +01:00
|
|
|
/*******************************************************************************
|
|
|
|
|
|
|
|
uBlock Origin - a browser extension to block requests.
|
|
|
|
Copyright (C) 2015 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/uBlock
|
|
|
|
*/
|
|
|
|
|
|
|
|
/* global µBlock */
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µBlock.redirectEngine = (function(){
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
var toBroaderHostname = function(hostname) {
|
|
|
|
var pos = hostname.indexOf('.');
|
2015-11-23 15:49:50 +01:00
|
|
|
if ( pos !== -1 ) {
|
|
|
|
return hostname.slice(pos + 1);
|
2015-11-23 13:52:50 +01:00
|
|
|
}
|
2015-11-23 15:49:50 +01:00
|
|
|
return hostname !== '*' && hostname !== '' ? '*' : '';
|
2015-11-23 13:52:50 +01:00
|
|
|
};
|
|
|
|
|
2015-11-26 23:56:30 +01:00
|
|
|
/******************************************************************************/
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2015-11-29 17:04:42 +01:00
|
|
|
var RedirectEntry = function() {
|
|
|
|
this.mime = '';
|
|
|
|
this.encoded = false;
|
|
|
|
this.ph = false;
|
|
|
|
this.data = '';
|
2015-11-26 23:56:30 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2015-11-29 17:04:42 +01:00
|
|
|
RedirectEntry.rePlaceHolders = /\{\{.+?\}\}/;
|
|
|
|
RedirectEntry.reRequestURL = /\{\{requestURL\}\}/g;
|
2015-11-26 23:56:30 +01:00
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
RedirectEntry.prototype.toURL = function(requestURL) {
|
|
|
|
if ( this.ph === false ) {
|
|
|
|
return this.data;
|
|
|
|
}
|
|
|
|
return 'data:' +
|
|
|
|
this.mime + ';base64,' +
|
2015-11-29 17:04:42 +01:00
|
|
|
btoa(this.data.replace(RedirectEntry.reRequestURL, requestURL));
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
RedirectEntry.fromFields = function(mime, lines) {
|
|
|
|
var r = new RedirectEntry();
|
|
|
|
|
|
|
|
r.mime = mime;
|
|
|
|
r.encoded = mime.indexOf(';') !== -1;
|
|
|
|
var data = lines.join(r.encoded ? '' : '\n');
|
|
|
|
// check for placeholders.
|
|
|
|
r.ph = r.encoded === false && RedirectEntry.rePlaceHolders.test(data);
|
|
|
|
if ( r.ph ) {
|
|
|
|
r.data = data;
|
|
|
|
} else {
|
|
|
|
r.data =
|
|
|
|
'data:' +
|
|
|
|
mime +
|
|
|
|
(r.encoded ? '' : ';base64') +
|
|
|
|
',' +
|
|
|
|
(r.encoded ? data : btoa(data));
|
|
|
|
}
|
|
|
|
|
|
|
|
return r;
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
RedirectEntry.fromSelfie = function(selfie) {
|
|
|
|
var r = new RedirectEntry();
|
|
|
|
|
|
|
|
r.mime = selfie.mime;
|
|
|
|
r.encoded = selfie.encoded;
|
|
|
|
r.ph = selfie.ph;
|
|
|
|
r.data = selfie.data;
|
|
|
|
|
|
|
|
return r;
|
2015-11-26 23:56:30 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
2015-11-23 13:52:50 +01:00
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
var RedirectEngine = function() {
|
2015-11-24 05:34:03 +01:00
|
|
|
this.resources = Object.create(null);
|
2015-11-23 13:52:50 +01:00
|
|
|
this.reset();
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
RedirectEngine.prototype.reset = function() {
|
|
|
|
this.rules = Object.create(null);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2015-11-24 01:18:25 +01:00
|
|
|
RedirectEngine.prototype.freeze = function() {
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2015-11-23 13:52:50 +01:00
|
|
|
RedirectEngine.prototype.lookup = function(context) {
|
|
|
|
var typeEntry = this.rules[context.requestType];
|
|
|
|
if ( typeEntry === undefined ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var src, des = context.requestHostname,
|
|
|
|
srcHostname = context.pageHostname,
|
|
|
|
reqURL = context.requestURL,
|
|
|
|
desEntry, entries, i, entry;
|
|
|
|
for (;;) {
|
|
|
|
desEntry = typeEntry[des];
|
|
|
|
if ( desEntry !== undefined ) {
|
|
|
|
src = srcHostname;
|
|
|
|
for (;;) {
|
|
|
|
entries = desEntry[src];
|
|
|
|
if ( entries !== undefined ) {
|
|
|
|
i = entries.length;
|
|
|
|
while ( i-- ) {
|
|
|
|
entry = entries[i];
|
|
|
|
if ( entry.c.test(reqURL) ) {
|
2015-11-26 23:56:30 +01:00
|
|
|
return entry.r;
|
2015-11-23 13:52:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-11-23 15:49:50 +01:00
|
|
|
src = toBroaderHostname(src);
|
|
|
|
if ( src === '' ) {
|
|
|
|
break;
|
|
|
|
}
|
2015-11-23 13:52:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
des = toBroaderHostname(des);
|
|
|
|
if ( des === '' ) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2015-11-26 23:56:30 +01:00
|
|
|
RedirectEngine.prototype.toURL = function(context) {
|
|
|
|
var token = this.lookup(context);
|
|
|
|
if ( token === undefined ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var entry = this.resources[token];
|
|
|
|
if ( entry !== undefined ) {
|
|
|
|
return entry.toURL(context.requestURL);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
RedirectEngine.prototype.matches = function(context) {
|
|
|
|
var token = this.lookup(context);
|
|
|
|
return token !== undefined && this.resources[token] !== undefined;
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2015-11-24 01:18:25 +01:00
|
|
|
RedirectEngine.prototype.addRule = function(src, des, type, pattern, redirect) {
|
|
|
|
var typeEntry = this.rules[type];
|
|
|
|
if ( typeEntry === undefined ) {
|
|
|
|
typeEntry = this.rules[type] = Object.create(null);
|
|
|
|
}
|
|
|
|
var desEntry = typeEntry[des];
|
|
|
|
if ( desEntry === undefined ) {
|
|
|
|
desEntry = typeEntry[des] = Object.create(null);
|
|
|
|
}
|
|
|
|
var ruleEntries = desEntry[src];
|
|
|
|
if ( ruleEntries === undefined ) {
|
|
|
|
ruleEntries = desEntry[src] = [];
|
|
|
|
}
|
|
|
|
ruleEntries.push({
|
|
|
|
c: new RegExp(pattern),
|
|
|
|
r: redirect
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
RedirectEngine.prototype.fromCompiledRule = function(line) {
|
|
|
|
var fields = line.split('\t');
|
|
|
|
if ( fields.length !== 5 ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.addRule(fields[0], fields[1], fields[2], fields[3], fields[4]);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
RedirectEngine.prototype.compileRuleFromStaticFilter = function(line) {
|
|
|
|
var matches = this.reFilterParser.exec(line);
|
|
|
|
if ( matches === null || matches.length !== 4 ) {
|
2015-11-25 16:05:23 +01:00
|
|
|
return;
|
2015-11-24 01:18:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
var pattern = (matches[1] + matches[2]).replace(/[.+?{}()|[\]\\]/g, '\\$&')
|
|
|
|
.replace(/\^/g, '[^\\w\\d%-]')
|
|
|
|
.replace(/\*/g, '.*?');
|
|
|
|
|
|
|
|
var des = matches[1];
|
2015-11-25 16:05:23 +01:00
|
|
|
var type;
|
2015-11-24 01:18:25 +01:00
|
|
|
var redirect = '';
|
|
|
|
var srcs = [];
|
|
|
|
var options = matches[3].split(','), option;
|
|
|
|
while ( (option = options.pop()) ) {
|
2015-12-15 16:40:40 +01:00
|
|
|
if ( option.startsWith('redirect=') ) {
|
2015-11-24 01:18:25 +01:00
|
|
|
redirect = option.slice(9);
|
|
|
|
continue;
|
|
|
|
}
|
2015-12-15 16:40:40 +01:00
|
|
|
if ( option.startsWith('domain=') ) {
|
2015-11-24 01:18:25 +01:00
|
|
|
srcs = option.slice(7).split('|');
|
|
|
|
continue;
|
|
|
|
}
|
2015-12-16 03:34:36 +01:00
|
|
|
if ( option === 'first-party' ) {
|
|
|
|
srcs.push(des);
|
|
|
|
continue;
|
|
|
|
}
|
2015-11-25 16:05:23 +01:00
|
|
|
// One and only one type must be specified.
|
2015-11-24 01:18:25 +01:00
|
|
|
if ( option in this.supportedTypes ) {
|
2015-11-25 16:05:23 +01:00
|
|
|
if ( type !== undefined ) {
|
|
|
|
return;
|
|
|
|
}
|
2015-11-26 17:13:33 +01:00
|
|
|
type = this.supportedTypes[option];
|
2015-11-24 01:18:25 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-11-24 05:34:03 +01:00
|
|
|
// Need a resource token.
|
|
|
|
if ( redirect === '' ) {
|
2015-11-25 16:05:23 +01:00
|
|
|
return;
|
2015-11-24 05:34:03 +01:00
|
|
|
}
|
|
|
|
|
2015-11-25 16:05:23 +01:00
|
|
|
// Need one single type -- not negated.
|
2015-12-15 16:40:40 +01:00
|
|
|
if ( type === undefined || type.startsWith('~') ) {
|
2015-11-25 16:05:23 +01:00
|
|
|
return;
|
2015-11-24 01:18:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if ( des === '' ) {
|
|
|
|
des = '*';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( srcs.length === 0 ) {
|
|
|
|
srcs.push('*');
|
|
|
|
}
|
|
|
|
|
|
|
|
var out = [];
|
2015-11-24 05:34:03 +01:00
|
|
|
var i = srcs.length, src;
|
2015-11-24 01:18:25 +01:00
|
|
|
while ( i-- ) {
|
2015-11-24 05:34:03 +01:00
|
|
|
src = srcs[i];
|
2015-11-25 16:05:23 +01:00
|
|
|
if ( src === '' ) {
|
|
|
|
continue;
|
|
|
|
}
|
2015-12-15 16:40:40 +01:00
|
|
|
if ( src.startsWith('~') ) {
|
2015-11-24 05:34:03 +01:00
|
|
|
continue;
|
2015-11-24 01:18:25 +01:00
|
|
|
}
|
2015-11-25 16:05:23 +01:00
|
|
|
// Need at least one specific src or des.
|
|
|
|
if ( src === '*' && des === '*' ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
out.push(src + '\t' + des + '\t' + type + '\t' + pattern + '\t' + redirect);
|
2015-11-24 01:18:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return out;
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2015-11-26 17:13:33 +01:00
|
|
|
RedirectEngine.prototype.reFilterParser = /^\|\|([^\/?#^*]+)([^$]+)\$([^$]+)$/;
|
2015-11-24 01:18:25 +01:00
|
|
|
|
|
|
|
RedirectEngine.prototype.supportedTypes = (function() {
|
|
|
|
var types = Object.create(null);
|
|
|
|
types.stylesheet = 'stylesheet';
|
|
|
|
types.image = 'image';
|
|
|
|
types.object = 'object';
|
|
|
|
types.script = 'script';
|
|
|
|
types.xmlhttprequest = 'xmlhttprequest';
|
|
|
|
types.subdocument = 'sub_frame';
|
|
|
|
types.font = 'font';
|
|
|
|
return types;
|
|
|
|
})();
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2015-11-29 17:04:42 +01:00
|
|
|
RedirectEngine.prototype.toSelfie = function() {
|
2015-11-29 23:06:58 +01:00
|
|
|
var r = {
|
2015-11-29 17:04:42 +01:00
|
|
|
resources: this.resources,
|
2015-11-29 23:06:58 +01:00
|
|
|
rules: []
|
2015-11-29 17:04:42 +01:00
|
|
|
};
|
2015-11-29 23:06:58 +01:00
|
|
|
|
|
|
|
var typeEntry, desEntry, entries, entry;
|
|
|
|
for ( var type in this.rules ) {
|
|
|
|
typeEntry = this.rules[type];
|
|
|
|
for ( var des in typeEntry ) {
|
|
|
|
desEntry = typeEntry[des];
|
|
|
|
for ( var src in desEntry ) {
|
|
|
|
entries = desEntry[src];
|
|
|
|
for ( var i = 0; i < entries.length; i++ ) {
|
|
|
|
entry = entries[i];
|
|
|
|
r.rules.push(
|
|
|
|
src + '\t' +
|
|
|
|
des + '\t' +
|
|
|
|
type + '\t' +
|
|
|
|
entry.c.source + '\t' +
|
|
|
|
entry.r
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return r;
|
2015-11-29 17:04:42 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
RedirectEngine.prototype.fromSelfie = function(selfie) {
|
|
|
|
// Resources.
|
|
|
|
var resources = selfie.resources;
|
|
|
|
for ( var token in resources ) {
|
|
|
|
if ( resources.hasOwnProperty(token) === false ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
this.resources[token] = RedirectEntry.fromSelfie(resources[token]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rules.
|
|
|
|
var rules = selfie.rules;
|
2015-11-29 23:06:58 +01:00
|
|
|
var i = rules.length;
|
|
|
|
while ( i-- ) {
|
|
|
|
this.fromCompiledRule(rules[i]);
|
2015-11-29 17:04:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2015-12-22 22:32:09 +01:00
|
|
|
RedirectEngine.prototype.resourceFromName = function(name, mime) {
|
|
|
|
var entry = this.resources[name];
|
|
|
|
if ( entry && (mime === undefined || entry.mime.startsWith(mime)) ) {
|
|
|
|
return entry.toURL();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
2015-11-24 01:18:25 +01:00
|
|
|
// TODO: combine same key-redirect pairs into a single regex.
|
|
|
|
|
2015-11-24 05:34:03 +01:00
|
|
|
RedirectEngine.prototype.resourcesFromString = function(text) {
|
2015-11-23 13:52:50 +01:00
|
|
|
var textEnd = text.length;
|
|
|
|
var lineBeg = 0, lineEnd;
|
2015-11-26 01:38:05 +01:00
|
|
|
var line, fields, encoded;
|
|
|
|
var reNonEmptyLine = /\S/;
|
2015-11-24 19:21:14 +01:00
|
|
|
|
2015-11-24 05:34:03 +01:00
|
|
|
this.resources = Object.create(null);
|
2015-11-23 13:52:50 +01:00
|
|
|
|
|
|
|
while ( lineBeg < textEnd ) {
|
|
|
|
lineEnd = text.indexOf('\n', lineBeg);
|
|
|
|
if ( lineEnd < 0 ) {
|
|
|
|
lineEnd = text.indexOf('\r', lineBeg);
|
|
|
|
if ( lineEnd < 0 ) {
|
|
|
|
lineEnd = textEnd;
|
|
|
|
}
|
|
|
|
}
|
2015-11-26 01:38:05 +01:00
|
|
|
line = text.slice(lineBeg, lineEnd);
|
2015-11-23 13:52:50 +01:00
|
|
|
lineBeg = lineEnd + 1;
|
|
|
|
|
2015-12-15 16:40:40 +01:00
|
|
|
if ( line.startsWith('#') ) {
|
2015-11-23 13:52:50 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2015-11-24 19:21:14 +01:00
|
|
|
if ( fields === undefined ) {
|
2015-11-26 01:38:05 +01:00
|
|
|
fields = line.trim().split(/\s+/);
|
|
|
|
if ( fields.length === 2 ) {
|
|
|
|
encoded = fields[1].indexOf(';') !== -1;
|
|
|
|
} else {
|
2015-11-24 19:21:14 +01:00
|
|
|
fields = undefined;
|
2015-11-23 13:52:50 +01:00
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2015-11-26 01:38:05 +01:00
|
|
|
if ( reNonEmptyLine.test(line) ) {
|
|
|
|
fields.push(encoded ? line.trim() : line);
|
2015-11-23 13:52:50 +01:00
|
|
|
continue;
|
|
|
|
}
|
2015-11-24 05:34:03 +01:00
|
|
|
|
2015-11-24 19:21:14 +01:00
|
|
|
// No more data, add the resource.
|
2015-11-29 17:04:42 +01:00
|
|
|
this.resources[fields[0]] = RedirectEntry.fromFields(fields[1], fields.slice(2));
|
2015-11-24 19:21:14 +01:00
|
|
|
|
|
|
|
fields = undefined;
|
|
|
|
}
|
2015-11-24 05:34:03 +01:00
|
|
|
|
2015-11-24 19:21:14 +01:00
|
|
|
// Process pending resource data.
|
|
|
|
if ( fields !== undefined ) {
|
2015-11-29 17:04:42 +01:00
|
|
|
this.resources[fields[0]] = RedirectEntry.fromFields(fields[1], fields.slice(2));
|
2015-11-23 13:52:50 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-11-26 23:56:30 +01:00
|
|
|
/******************************************************************************/
|
2015-11-23 13:52:50 +01:00
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
return new RedirectEngine();
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
})();
|