Add new static network filter option: `redirect-rule=`

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

The purpose of this new option is to add the ability
to create standalone redirect rule without being forced
to create a block filter (a corresponding block filter
is always created when using the `redirect=`).

Additionally:

The syntax `*$redirect=token,...` is now supported, there
is no need to "trick" the filter parser with
`*/*$redirect=token,...` in order to create redirect rules
which are meant to match all paths.

Filters of the form `|http*://` will be normalized into
two corresponding filters `|https://` and `|http://` so as
to reduce the number of filters in the buckets of
untokenizable filters.
This commit is contained in:
Raymond Hill 2019-08-03 10:18:47 -04:00
parent 663fd5fab0
commit aa73f292ec
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
2 changed files with 78 additions and 58 deletions

View File

@ -303,7 +303,7 @@ RedirectEngine.prototype.lookup = function(fctxt) {
for (;;) {
if ( this.ruleSources.has(src) ) {
for ( let i = 0; i < n; i++ ) {
const entries = this.rules.get(src + ' ' + desAll[i] + ' ' + type);
const entries = this.rules.get(`${src} ${desAll[i]} ${type}`);
if ( entries && this.lookupToken(entries, reqURL) ) {
return this.resourceNameRegister;
}
@ -414,14 +414,18 @@ RedirectEngine.prototype.compileRuleFromStaticFilter = function(line) {
}
}
const pattern =
const path = matches[2] || '';
let pattern =
des
.replace(/\*/g, '[\\w.%-]*')
.replace(/\./g, '\\.') +
matches[2]
path
.replace(/[.+?{}()|[\]\/\\]/g, '\\$&')
.replace(/\^/g, '[^\\w.%-]')
.replace(/\*/g, '.*?');
if ( pattern === '' ) {
pattern = '^';
}
let type,
redirect = '',
@ -431,6 +435,10 @@ RedirectEngine.prototype.compileRuleFromStaticFilter = function(line) {
redirect = option.slice(9);
continue;
}
if ( option.startsWith('redirect-rule=') ) {
redirect = option.slice(14);
continue;
}
if ( option.startsWith('domain=') ) {
srchns = option.slice(7).split('|');
continue;
@ -468,12 +476,14 @@ RedirectEngine.prototype.compileRuleFromStaticFilter = function(line) {
out.push(`${srchn}\t${deshn}\t${type}\t${pattern}\t${redirect}`);
}
if ( out.length === 0 ) { return; }
return out;
};
/******************************************************************************/
RedirectEngine.prototype.reFilterParser = /^(?:\|\|([^\/:?#^]+)|\*)([^$]+)\$([^$]+)$/;
RedirectEngine.prototype.reFilterParser = /^(?:\|\|([^\/:?#^]+)|\*)([^$]+)?\$([^$]+)$/;
RedirectEngine.prototype.supportedTypes = new Map([
[ 'css', 'stylesheet' ],

View File

@ -1883,7 +1883,7 @@ FilterParser.prototype.reset = function() {
this.isPureHostname = false;
this.isRegex = false;
this.raw = '';
this.redirect = false;
this.redirect = 0;
this.token = '*';
this.tokenHash = this.noTokenHash;
this.tokenBeg = 0;
@ -1963,25 +1963,9 @@ FilterParser.prototype.parseOptions = function(s) {
this.parsePartyOption(false, not);
continue;
}
// https://issues.adblockplus.org/ticket/616
// `generichide` concept already supported, just a matter of
// adding support for the new keyword.
if ( opt === 'elemhide' || opt === 'generichide' ) {
if ( not === false ) {
this.parseTypeOption('generichide', false);
continue;
}
this.unsupported = true;
break;
}
// Test before handling all other types.
if ( opt.startsWith('redirect=') ) {
if ( this.action === BlockAction ) {
this.redirect = true;
continue;
}
this.unsupported = true;
break;
if ( opt === 'first-party' || opt === '1p' ) {
this.parsePartyOption(true, not);
continue;
}
if ( this.toNormalizedType.hasOwnProperty(opt) ) {
this.parseTypeOption(opt, not);
@ -2002,8 +1986,12 @@ FilterParser.prototype.parseOptions = function(s) {
this.important = Important;
continue;
}
if ( opt === 'first-party' || opt === '1p' ) {
this.parsePartyOption(true, not);
if ( /^redirect(?:-rule)?=/.test(opt) ) {
if ( this.redirect !== 0 ) {
this.unsupported = true;
break;
}
this.redirect = opt.charCodeAt(8) === 0x3D /* '=' */ ? 1 : 2;
continue;
}
if (
@ -2036,6 +2024,11 @@ FilterParser.prototype.parseOptions = function(s) {
break;
}
// Redirect rules can't be exception filters.
if ( this.redirect !== 0 && this.action !== BlockAction ) {
this.unsupported = true;
}
// Negated network types? Toggle on all network type bits.
// Negated non-network types can only toggle themselves.
if ( (this.notTypes & allNetworkTypesBits) !== 0 ) {
@ -2216,6 +2209,10 @@ FilterParser.prototype.parse = function(raw) {
if ( s === '' ) {
s = '*';
}
// TODO: remove once redirect rules with `*/*` pattern are no longer used.
else if ( this.redirect !== 0 && s === '/' ) {
s = '*';
}
// https://github.com/gorhill/uBlock/issues/1047
// Hostname-anchored makes no sense if matching all requests.
@ -2332,9 +2329,8 @@ FilterParser.prototype.makeToken = function() {
FilterParser.prototype.isJustOrigin = function() {
return this.dataType === undefined &&
this.redirect === false &&
this.domainOpt !== '' &&
/^(?:\*|https?:(?:\/\/)?)$/.test(this.f) &&
/^(?:\*|http[s*]?:(?:\/\/)?)$/.test(this.f) &&
this.domainOpt.indexOf('~') === -1;
};
@ -2654,6 +2650,23 @@ FilterContainer.prototype.compile = function(raw, writer) {
return false;
}
// Redirect rule
if ( parsed.redirect !== 0 ) {
const result = this.compileRedirectRule(parsed, writer);
if ( result === false ) {
const who = writer.properties.get('assetKey') || '?';
µb.logger.writeOne({
realm: 'message',
type: 'error',
text: `Invalid redirect rule in ${who}: ${raw}`
});
return false;
}
if ( parsed.redirect === 2 ) {
return true;
}
}
// Pure hostnames, use more efficient dictionary lookup
// https://github.com/chrisaljoudi/uBlock/issues/665
// Create a dict keyed on request type etc.
@ -2694,25 +2707,24 @@ FilterContainer.prototype.compile = function(raw, writer) {
} else {
fdata = FilterGenericHnAnchored.compile(parsed);
}
} else if ( parsed.anchor === 0x2 && parsed.isJustOrigin() ) {
const hostnames = parsed.domainOpt.split('|');
const isHTTPS = parsed.f === 'https://' || parsed.f === 'http*://';
const isHTTP = parsed.f === 'http://' || parsed.f === 'http*://';
for ( const hn of hostnames ) {
if ( isHTTPS ) {
parsed.tokenHash = this.anyHTTPSTokenHash;
this.compileToAtomicFilter(parsed, hn, writer);
}
if ( isHTTP ) {
parsed.tokenHash = this.anyHTTPTokenHash;
this.compileToAtomicFilter(parsed, hn, writer);
}
}
return true;
} else if ( parsed.wildcarded || parsed.tokenHash === parsed.noTokenHash ) {
fdata = FilterGeneric.compile(parsed);
} else if ( parsed.anchor === 0x2 ) {
if ( parsed.isJustOrigin() ) {
if ( parsed.f === 'https://' ) {
parsed.tokenHash = this.anyHTTPSTokenHash;
for ( const hn of parsed.domainOpt.split('|') ) {
this.compileToAtomicFilter(parsed, hn, writer);
}
return true;
}
if ( parsed.f === 'http://' ) {
parsed.tokenHash = this.anyHTTPTokenHash;
for ( const hn of parsed.domainOpt.split('|') ) {
this.compileToAtomicFilter(parsed, hn, writer);
}
return true;
}
}
fdata = FilterPlainLeftAnchored.compile(parsed);
} else if ( parsed.anchor === 0x1 ) {
fdata = FilterPlainRightAnchored.compile(parsed);
@ -2747,11 +2759,7 @@ FilterContainer.prototype.compileToAtomicFilter = function(
// 0 = network filters
// 1 = network filters: bad filters
if ( parsed.badFilter ) {
writer.select(1);
} else {
writer.select(0);
}
writer.select(parsed.badFilter ? 1 : 0);
const descBits = parsed.action | parsed.important | parsed.party;
let typeBits = parsed.types;
@ -2777,17 +2785,19 @@ FilterContainer.prototype.compileToAtomicFilter = function(
bitOffset += 1;
typeBits >>>= 1;
} while ( typeBits !== 0 );
};
// Only static filter with an explicit type can be redirected. If we reach
// this point, it's because there is one or more explicit type.
if ( parsed.redirect ) {
const redirects = µb.redirectEngine.compileRuleFromStaticFilter(parsed.raw);
if ( Array.isArray(redirects) ) {
for ( const redirect of redirects ) {
writer.push([ typeNameToTypeValue.redirect, redirect ]);
}
}
/******************************************************************************/
FilterContainer.prototype.compileRedirectRule = function(parsed, writer) {
const redirects = µb.redirectEngine.compileRuleFromStaticFilter(parsed.raw);
if ( Array.isArray(redirects) === false ) { return false; }
writer.select(parsed.badFilter ? 1 : 0);
const type = typeNameToTypeValue.redirect;
for ( const redirect of redirects ) {
writer.push([ type, redirect ]);
}
return true;
};
/******************************************************************************/