Skip to content

Commit 3e5c9e0

Browse files
committed
Add support for AdGuard's empty option
Related issue: - uBlockOrigin/uBlock-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.
1 parent 2c39a1a commit 3e5c9e0

File tree

3 files changed

+139
-84
lines changed

3 files changed

+139
-84
lines changed

‎src/js/redirect-engine.js‎

Lines changed: 134 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,15 @@ const redirectableResources = new Map([
5656
[ 'addthis_widget.js', {
5757
alias: 'addthis.com/addthis_widget.js',
5858
} ],
59+
[ 'amazon_ads.js', {
60+
alias: 'amazon-adsystem.com/aax2/amzn_ads.js',
61+
} ],
5962
[ 'ampproject_v0.js', {
6063
alias: 'ampproject.org/v0.js',
6164
} ],
6265
[ 'chartbeat.js', {
6366
alias: 'static.chartbeat.com/chartbeat.js',
6467
} ],
65-
[ 'amazon_ads.js', {
66-
alias: 'amazon-adsystem.com/aax2/amzn_ads.js',
67-
} ],
6868
[ 'disqus_embed.js', {
6969
alias: 'disqus.com/embed.js',
7070
} ],
@@ -74,6 +74,9 @@ const redirectableResources = new Map([
7474
[ 'doubleclick_instream_ad_status.js', {
7575
alias: 'doubleclick.net/instream/ad_status.js',
7676
} ],
77+
[ 'empty', {
78+
data: 'text', // Important!
79+
} ],
7780
[ 'google-analytics_analytics.js', {
7881
alias: 'google-analytics.com/analytics.js',
7982
} ],
@@ -165,6 +168,15 @@ const extToMimeMap = new Map([
165168
[ 'txt', 'text/plain' ],
166169
]);
167170

171+
const typeToMimeMap = new Map([
172+
[ 'main_frame', 'text/html' ],
173+
[ 'other', 'text/plain' ],
174+
[ 'script', 'application/javascript' ],
175+
[ 'stylesheet', 'text/css' ],
176+
[ 'sub_frame', 'text/html' ],
177+
[ 'xmlhttprequest', 'text/plain' ],
178+
]);
179+
168180
const validMimes = new Set(extToMimeMap.values());
169181

170182
const mimeFromName = function(name) {
@@ -177,67 +189,67 @@ const mimeFromName = function(name) {
177189
/******************************************************************************/
178190
/******************************************************************************/
179191

180-
const RedirectEntry = function() {
181-
this.mime = '';
182-
this.data = '';
183-
this.warURL = undefined;
184-
};
185-
186-
/******************************************************************************/
187-
188-
// Prevent redirection to web accessible resources when the request is
189-
// of type 'xmlhttprequest', because XMLHttpRequest.responseURL would
190-
// cause leakage of extension id. See:
191-
// - https://stackoverflow.com/a/8056313
192-
// - https://bugzilla.mozilla.org/show_bug.cgi?id=998076
193-
194-
RedirectEntry.prototype.toURL = function(fctxt, asDataURI = false) {
195-
if (
196-
this.warURL !== undefined &&
197-
asDataURI !== true &&
198-
fctxt instanceof Object &&
199-
fctxt.type !== 'xmlhttprequest'
200-
) {
201-
return `${this.warURL}${vAPI.warSecret()}`;
202-
}
203-
if ( this.data === undefined ) { return; }
204-
if ( this.data.startsWith('data:') === false ) {
205-
this.data = `data:${this.mime};base64,${btoa(this.data)}`;
206-
}
207-
return this.data;
208-
};
192+
const RedirectEntry = class {
193+
constructor() {
194+
this.mime = '';
195+
this.data = '';
196+
this.warURL = undefined;
197+
}
209198

210-
/******************************************************************************/
199+
// Prevent redirection to web accessible resources when the request is
200+
// of type 'xmlhttprequest', because XMLHttpRequest.responseURL would
201+
// cause leakage of extension id. See:
202+
// - https://stackoverflow.com/a/8056313
203+
// - https://bugzilla.mozilla.org/show_bug.cgi?id=998076
211204

212-
RedirectEntry.prototype.toContent = function() {
213-
if ( this.data.startsWith('data:') ) {
214-
const pos = this.data.indexOf(',');
215-
const base64 = this.data.endsWith(';base64', pos);
216-
this.data = this.data.slice(pos + 1);
217-
if ( base64 ) {
218-
this.data = atob(this.data);
205+
toURL(fctxt, asDataURI = false) {
206+
if (
207+
this.warURL !== undefined &&
208+
asDataURI !== true &&
209+
fctxt instanceof Object &&
210+
fctxt.type !== 'xmlhttprequest'
211+
) {
212+
return `${this.warURL}${vAPI.warSecret()}`;
213+
}
214+
if ( this.data === undefined ) { return; }
215+
// https://github.com/uBlockOrigin/uBlock-issues/issues/701
216+
if ( this.data === '' ) {
217+
const mime = typeToMimeMap.get(fctxt.type);
218+
if ( mime === undefined ) { return; }
219+
return `data:${mime},`;
220+
}
221+
if ( this.data.startsWith('data:') === false ) {
222+
this.data = `data:${this.mime};base64,${btoa(this.data)}`;
219223
}
224+
return this.data;
220225
}
221-
return this.data;
222-
};
223-
224-
/******************************************************************************/
225226

226-
RedirectEntry.fromContent = function(mime, content) {
227-
const r = new RedirectEntry();
228-
r.mime = mime;
229-
r.data = content;
230-
return r;
231-
};
227+
toContent() {
228+
if ( this.data.startsWith('data:') ) {
229+
const pos = this.data.indexOf(',');
230+
const base64 = this.data.endsWith(';base64', pos);
231+
this.data = this.data.slice(pos + 1);
232+
if ( base64 ) {
233+
this.data = atob(this.data);
234+
}
235+
}
236+
return this.data;
237+
}
232238

233-
/******************************************************************************/
239+
static fromContent(mime, content) {
240+
const r = new RedirectEntry();
241+
r.mime = mime;
242+
r.data = content;
243+
return r;
244+
}
234245

235-
RedirectEntry.fromSelfie = function(selfie) {
236-
const r = new RedirectEntry();
237-
r.mime = selfie.mime;
238-
r.data = selfie.data;
239-
r.warURL = selfie.warURL;
240-
return r;
246+
static fromSelfie(selfie) {
247+
const r = new RedirectEntry();
248+
r.mime = selfie.mime;
249+
r.data = selfie.data;
250+
r.warURL = selfie.warURL;
251+
return r;
252+
}
241253
};
242254

243255
/******************************************************************************/
@@ -248,7 +260,13 @@ const RedirectEngine = function() {
248260
this.resources = new Map();
249261
this.reset();
250262
this.resourceNameRegister = '';
251-
this._desAll = []; // re-use better than re-allocate
263+
264+
// Internal use
265+
this._missedQueryHash = '';
266+
this._src = '';
267+
this._srcAll = [ '*' ];
268+
this._des = '';
269+
this._desAll = [ '*' ];
252270
};
253271

254272
/******************************************************************************/
@@ -278,35 +296,60 @@ RedirectEngine.prototype.toBroaderHostname = function(hostname) {
278296

279297
/******************************************************************************/
280298

281-
RedirectEngine.prototype.lookup = function(fctxt) {
282-
const type = fctxt.type;
283-
if ( this.ruleTypes.has(type) === false ) { return; }
284-
const desAll = this._desAll;
285-
const reqURL = fctxt.url;
286-
let src = fctxt.getDocHostname();
287-
let des = fctxt.getHostname();
288-
let n = 0;
299+
RedirectEngine.prototype.decomposeHostname = function(hn, dict, out) {
300+
let i = 0;
289301
for (;;) {
290-
if ( this.ruleDestinations.has(des) ) {
291-
desAll[n] = des; n += 1;
302+
if ( dict.has(hn) ) {
303+
out[i] = hn; i += 1;
292304
}
293-
des = this.toBroaderHostname(des);
294-
if ( des === '' ) { break; }
305+
hn = this.toBroaderHostname(hn);
306+
if ( hn === '' ) { break; }
295307
}
296-
if ( n === 0 ) { return; }
297-
for (;;) {
298-
if ( this.ruleSources.has(src) ) {
299-
for ( let i = 0; i < n; i++ ) {
300-
const entries = this.rules.get(`${src} ${desAll[i]} ${type}`);
301-
if ( entries === undefined ) { continue; }
308+
out.length = i;
309+
};
310+
311+
/******************************************************************************/
312+
313+
RedirectEngine.prototype.lookup = function(fctxt) {
314+
const src = fctxt.getDocHostname();
315+
const des = fctxt.getHostname();
316+
const type = fctxt.type;
317+
const queryHash = `${src} ${des} ${type}`;
318+
if ( queryHash === this._missedQueryHash ) {
319+
return;
320+
}
321+
if ( src !== this._src ) {
322+
this._src = src;
323+
this.decomposeHostname(src, this.ruleSources, this._srcAll);
324+
}
325+
if ( this._srcAll.length === 0 ) {
326+
this._missedQueryHash = queryHash;
327+
return;
328+
}
329+
if ( des !== this._des ) {
330+
this._des = des;
331+
this.decomposeHostname(des, this.ruleDestinations, this._desAll);
332+
}
333+
if ( this._desAll.length === 0 ) {
334+
this._missedQueryHash = queryHash;
335+
return;
336+
}
337+
const reqURL = fctxt.url;
338+
for ( const src of this._srcAll ) {
339+
for ( const des of this._desAll ) {
340+
let entries = this.rules.get(`${src} ${des} ${type}`);
341+
if ( entries !== undefined ) {
302342
const rule = this.lookupRule(entries, reqURL);
303-
if ( rule === undefined ) { continue; }
304-
return rule;
343+
if ( rule !== undefined ) { return rule; }
344+
}
345+
entries = this.rules.get(`${src} ${des} *`);
346+
if ( entries !== undefined ) {
347+
const rule = this.lookupRule(entries, reqURL);
348+
if ( rule !== undefined ) { return rule; }
305349
}
306350
}
307-
src = this.toBroaderHostname(src);
308-
if ( src === '' ) { break; }
309351
}
352+
this._missedQueryHash = queryHash;
310353
};
311354

312355
RedirectEngine.prototype.lookupRule = function(entries, reqURL) {
@@ -428,6 +471,10 @@ RedirectEngine.prototype.compileRuleFromStaticFilter = function(line) {
428471
redirect = option.slice(14);
429472
continue;
430473
}
474+
if ( option === 'empty' ) {
475+
redirect = 'empty';
476+
continue;
477+
}
431478
if ( option.startsWith('domain=') ) {
432479
srchns = option.slice(7).split('|');
433480
continue;
@@ -448,7 +495,10 @@ RedirectEngine.prototype.compileRuleFromStaticFilter = function(line) {
448495
if ( redirect === '' ) { return; }
449496

450497
// Need one single type -- not negated.
451-
if ( type === undefined ) { return; }
498+
if ( type === undefined ) {
499+
if ( redirect !== 'empty' ) { return; }
500+
type = '*';
501+
}
452502

453503
if ( deshn === '' ) {
454504
deshn = '*';
@@ -649,12 +699,12 @@ const removeTopCommentBlock = function(text) {
649699
/******************************************************************************/
650700

651701
RedirectEngine.prototype.loadBuiltinResources = function() {
652-
this.resources = new Map();
653-
this.aliases = new Map();
654-
655702
// TODO: remove once usage of uBO 1.20.4 is widespread.
656703
µBlock.assets.remove('ublock-resources');
657704

705+
this.resources = new Map();
706+
this.aliases = new Map();
707+
658708
const fetches = [
659709
µBlock.assets.fetchText(
660710
'/assets/resources/scriptlets.js'

‎src/js/static-net-filtering.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2012,6 +2012,11 @@ FilterParser.prototype.parseOptions = function(s) {
20122012
}
20132013
// Used by Adguard, purpose is unclear -- just ignore for now.
20142014
if ( opt === 'empty' ) {
2015+
if ( this.redirect !== 0 ) {
2016+
this.unsupported = true;
2017+
break;
2018+
}
2019+
this.redirect = 1;
20152020
continue;
20162021
}
20172022
// https://github.com/uBlockOrigin/uAssets/issues/192

‎src/web_accessible_resources/empty‎

Whitespace-only changes.

0 commit comments

Comments
 (0)