Add support for AdGuard's `empty` option

Related issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/701

The filter option `empty` is converted to `redirect=empty`
by uBO internally; however unlike when the `redirect=`
option is used expressly, the `empty` option does not
require a resource type.

When `empty` is used, only network requests which are meant
to return a text response will be redirected to an empty
response body by uBO -- so `empty` will not work for
resources such as images, media, or other binary resources.
This commit is contained in:
Raymond Hill 2019-08-13 08:16:21 -04:00
parent 2c39a1af02
commit 3e5c9e00ab
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
3 changed files with 138 additions and 83 deletions

View File

@ -56,15 +56,15 @@ const redirectableResources = new Map([
[ 'addthis_widget.js', {
alias: 'addthis.com/addthis_widget.js',
} ],
[ 'amazon_ads.js', {
alias: 'amazon-adsystem.com/aax2/amzn_ads.js',
} ],
[ 'ampproject_v0.js', {
alias: 'ampproject.org/v0.js',
} ],
[ 'chartbeat.js', {
alias: 'static.chartbeat.com/chartbeat.js',
} ],
[ 'amazon_ads.js', {
alias: 'amazon-adsystem.com/aax2/amzn_ads.js',
} ],
[ 'disqus_embed.js', {
alias: 'disqus.com/embed.js',
} ],
@ -74,6 +74,9 @@ const redirectableResources = new Map([
[ 'doubleclick_instream_ad_status.js', {
alias: 'doubleclick.net/instream/ad_status.js',
} ],
[ 'empty', {
data: 'text', // Important!
} ],
[ 'google-analytics_analytics.js', {
alias: 'google-analytics.com/analytics.js',
} ],
@ -165,6 +168,15 @@ const extToMimeMap = new Map([
[ 'txt', 'text/plain' ],
]);
const typeToMimeMap = new Map([
[ 'main_frame', 'text/html' ],
[ 'other', 'text/plain' ],
[ 'script', 'application/javascript' ],
[ 'stylesheet', 'text/css' ],
[ 'sub_frame', 'text/html' ],
[ 'xmlhttprequest', 'text/plain' ],
]);
const validMimes = new Set(extToMimeMap.values());
const mimeFromName = function(name) {
@ -177,13 +189,12 @@ const mimeFromName = function(name) {
/******************************************************************************/
/******************************************************************************/
const RedirectEntry = function() {
const RedirectEntry = class {
constructor() {
this.mime = '';
this.data = '';
this.warURL = undefined;
};
/******************************************************************************/
}
// Prevent redirection to web accessible resources when the request is
// of type 'xmlhttprequest', because XMLHttpRequest.responseURL would
@ -191,7 +202,7 @@ const RedirectEntry = function() {
// - https://stackoverflow.com/a/8056313
// - https://bugzilla.mozilla.org/show_bug.cgi?id=998076
RedirectEntry.prototype.toURL = function(fctxt, asDataURI = false) {
toURL(fctxt, asDataURI = false) {
if (
this.warURL !== undefined &&
asDataURI !== true &&
@ -201,15 +212,19 @@ RedirectEntry.prototype.toURL = function(fctxt, asDataURI = false) {
return `${this.warURL}${vAPI.warSecret()}`;
}
if ( this.data === undefined ) { return; }
// https://github.com/uBlockOrigin/uBlock-issues/issues/701
if ( this.data === '' ) {
const mime = typeToMimeMap.get(fctxt.type);
if ( mime === undefined ) { return; }
return `data:${mime},`;
}
if ( this.data.startsWith('data:') === false ) {
this.data = `data:${this.mime};base64,${btoa(this.data)}`;
}
return this.data;
};
}
/******************************************************************************/
RedirectEntry.prototype.toContent = function() {
toContent() {
if ( this.data.startsWith('data:') ) {
const pos = this.data.indexOf(',');
const base64 = this.data.endsWith(';base64', pos);
@ -219,25 +234,22 @@ RedirectEntry.prototype.toContent = function() {
}
}
return this.data;
};
}
/******************************************************************************/
RedirectEntry.fromContent = function(mime, content) {
static fromContent(mime, content) {
const r = new RedirectEntry();
r.mime = mime;
r.data = content;
return r;
};
}
/******************************************************************************/
RedirectEntry.fromSelfie = function(selfie) {
static fromSelfie(selfie) {
const r = new RedirectEntry();
r.mime = selfie.mime;
r.data = selfie.data;
r.warURL = selfie.warURL;
return r;
}
};
/******************************************************************************/
@ -248,7 +260,13 @@ const RedirectEngine = function() {
this.resources = new Map();
this.reset();
this.resourceNameRegister = '';
this._desAll = []; // re-use better than re-allocate
// Internal use
this._missedQueryHash = '';
this._src = '';
this._srcAll = [ '*' ];
this._des = '';
this._desAll = [ '*' ];
};
/******************************************************************************/
@ -278,35 +296,60 @@ RedirectEngine.prototype.toBroaderHostname = function(hostname) {
/******************************************************************************/
RedirectEngine.prototype.decomposeHostname = function(hn, dict, out) {
let i = 0;
for (;;) {
if ( dict.has(hn) ) {
out[i] = hn; i += 1;
}
hn = this.toBroaderHostname(hn);
if ( hn === '' ) { break; }
}
out.length = i;
};
/******************************************************************************/
RedirectEngine.prototype.lookup = function(fctxt) {
const src = fctxt.getDocHostname();
const des = fctxt.getHostname();
const type = fctxt.type;
if ( this.ruleTypes.has(type) === false ) { return; }
const desAll = this._desAll;
const queryHash = `${src} ${des} ${type}`;
if ( queryHash === this._missedQueryHash ) {
return;
}
if ( src !== this._src ) {
this._src = src;
this.decomposeHostname(src, this.ruleSources, this._srcAll);
}
if ( this._srcAll.length === 0 ) {
this._missedQueryHash = queryHash;
return;
}
if ( des !== this._des ) {
this._des = des;
this.decomposeHostname(des, this.ruleDestinations, this._desAll);
}
if ( this._desAll.length === 0 ) {
this._missedQueryHash = queryHash;
return;
}
const reqURL = fctxt.url;
let src = fctxt.getDocHostname();
let des = fctxt.getHostname();
let n = 0;
for (;;) {
if ( this.ruleDestinations.has(des) ) {
desAll[n] = des; n += 1;
}
des = this.toBroaderHostname(des);
if ( des === '' ) { break; }
}
if ( n === 0 ) { return; }
for (;;) {
if ( this.ruleSources.has(src) ) {
for ( let i = 0; i < n; i++ ) {
const entries = this.rules.get(`${src} ${desAll[i]} ${type}`);
if ( entries === undefined ) { continue; }
for ( const src of this._srcAll ) {
for ( const des of this._desAll ) {
let entries = this.rules.get(`${src} ${des} ${type}`);
if ( entries !== undefined ) {
const rule = this.lookupRule(entries, reqURL);
if ( rule === undefined ) { continue; }
return rule;
if ( rule !== undefined ) { return rule; }
}
entries = this.rules.get(`${src} ${des} *`);
if ( entries !== undefined ) {
const rule = this.lookupRule(entries, reqURL);
if ( rule !== undefined ) { return rule; }
}
}
src = this.toBroaderHostname(src);
if ( src === '' ) { break; }
}
this._missedQueryHash = queryHash;
};
RedirectEngine.prototype.lookupRule = function(entries, reqURL) {
@ -428,6 +471,10 @@ RedirectEngine.prototype.compileRuleFromStaticFilter = function(line) {
redirect = option.slice(14);
continue;
}
if ( option === 'empty' ) {
redirect = 'empty';
continue;
}
if ( option.startsWith('domain=') ) {
srchns = option.slice(7).split('|');
continue;
@ -448,7 +495,10 @@ RedirectEngine.prototype.compileRuleFromStaticFilter = function(line) {
if ( redirect === '' ) { return; }
// Need one single type -- not negated.
if ( type === undefined ) { return; }
if ( type === undefined ) {
if ( redirect !== 'empty' ) { return; }
type = '*';
}
if ( deshn === '' ) {
deshn = '*';
@ -649,12 +699,12 @@ const removeTopCommentBlock = function(text) {
/******************************************************************************/
RedirectEngine.prototype.loadBuiltinResources = function() {
this.resources = new Map();
this.aliases = new Map();
// TODO: remove once usage of uBO 1.20.4 is widespread.
µBlock.assets.remove('ublock-resources');
this.resources = new Map();
this.aliases = new Map();
const fetches = [
µBlock.assets.fetchText(
'/assets/resources/scriptlets.js'

View File

@ -2012,6 +2012,11 @@ FilterParser.prototype.parseOptions = function(s) {
}
// Used by Adguard, purpose is unclear -- just ignore for now.
if ( opt === 'empty' ) {
if ( this.redirect !== 0 ) {
this.unsupported = true;
break;
}
this.redirect = 1;
continue;
}
// https://github.com/uBlockOrigin/uAssets/issues/192

View File