From aa73f292eced0d34a2a2989b1b27ace1214a2809 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sat, 3 Aug 2019 10:18:47 -0400 Subject: [PATCH] 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. --- src/js/redirect-engine.js | 18 +++-- src/js/static-net-filtering.js | 118 ++++++++++++++++++--------------- 2 files changed, 78 insertions(+), 58 deletions(-) diff --git a/src/js/redirect-engine.js b/src/js/redirect-engine.js index b6c4b6e2b..da9ed4999 100644 --- a/src/js/redirect-engine.js +++ b/src/js/redirect-engine.js @@ -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' ], diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 365c38039..d0ac63283 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -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; }; /******************************************************************************/