From 1e2eb037e5b4754feb4a40519951b3e7a73d545d Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sat, 31 Oct 2020 10:42:53 -0400 Subject: [PATCH] Add new filter option `queryprune=` Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/760 The purpose of this new network filter option is to remove query parameters form the URL of network requests. The name `queryprune` has been picked over `querystrip` since the purpose of the option is to remove some parameters from the URL rather than all parameters. `queryprune` is a modifier option (like `csp`) in that it does not cause a network request to be blocked but rather modified before being emitted. `queryprune` must be assigned a value, which value will determine which parameters from a query string will be removed. The syntax for the value is that of regular expression *except* for the following rules: - do not wrap the regex directive between `/` - do not use regex special values `^` and `$` - do not use literal comma character in the value, though you can use hex-encoded version, `\x2c` - to match the start of a query parameter, prepend `|` - to match the end of a query parameter, append `|` `queryprune` regex-like values will be tested against each key-value parameter pair as `[key]=[value]` string. This way you can prune according to either the key, the value, or both. This commit introduces the concept of modifier filter options, which as of now are: - `csp=` - `queryprune=` They both work in similar way when used with `important` option or when used in exception filters. Modifier options can apply to any network requests, hence the logger reports the type of the network requests, and no longer use the modifier as the type, i.e. `csp` filters are no longer reported as requests of type `csp`. Though modifier options can apply to any network requests, for the time being the `csp=` modifier option still apply only to top or embedded (frame) documents, just as before. In some future we may want to apply `csp=` directives to network requests of type script, to control the behavior of service workers for example. A new built-in filter expression has been added to the logger: "modified", which allow to see all the network requests which were modified before being emitted. The translation work for this new option will be available in a future commit. --- platform/chromium/vapi-background.js | 2 +- src/js/about.js | 4 +- src/js/background.js | 4 +- src/js/codemirror/ubo-static-filtering.js | 2 +- src/js/filtering-context.js | 385 +++++++++++----- src/js/logger-ui.js | 1 + src/js/logger.js | 5 +- src/js/messaging.js | 10 +- src/js/pagestore.js | 71 ++- src/js/static-filtering-parser.js | 216 ++++++--- src/js/static-net-filtering.js | 534 ++++++++++++++-------- src/js/traffic.js | 137 +++--- src/js/utils.js | 72 ++- src/logger-ui.html | 4 +- 14 files changed, 958 insertions(+), 489 deletions(-) diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 569dd9e5a..a6ae9a288 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -1145,7 +1145,7 @@ vAPI.warSecret = (( ) => { url.lastIndexOf(`?secret=${secret}`) !== -1 ); if ( pos === -1 ) { - return { redirectUrl: root }; + return { cancel: true }; } secrets.splice(pos, 1); }; diff --git a/src/js/about.js b/src/js/about.js index 212abae8c..a7befaa39 100644 --- a/src/js/about.js +++ b/src/js/about.js @@ -49,7 +49,9 @@ vAPI.messaging.send('dashboard', { what: 'sfneBenchmark', }).then(result => { - document.getElementById('sfneBenchmarkResult').textContent = result; + document.getElementById('sfneBenchmarkResult').prepend( + document.createTextNode(result.trim() + '\n') + ); button.removeAttribute('disabled'); }); }); diff --git a/src/js/background.js b/src/js/background.js index a9990b90f..6aa6df696 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -139,8 +139,8 @@ const µBlock = (( ) => { // jshint ignore:line // Read-only systemSettings: { - compiledMagic: 29, // Increase when compiled format changes - selfieMagic: 29, // Increase when selfie format changes + compiledMagic: 30, // Increase when compiled format changes + selfieMagic: 30, // Increase when selfie format changes }, // https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501 diff --git a/src/js/codemirror/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js index 25ecd3187..6f7ac7088 100644 --- a/src/js/codemirror/ubo-static-filtering.js +++ b/src/js/codemirror/ubo-static-filtering.js @@ -420,7 +420,7 @@ const initHints = function() { if ( assignPos !== -1 ) { seedRight = seedRight.slice(0, assignPos); } const isException = parser.isException(); const hints = []; - for ( let [ text, bits ] of parser.netOptionTokens ) { + for ( let [ text, bits ] of parser.netOptionTokenDescriptors ) { if ( isNegated && (bits & parser.OPTCanNegate) === 0 ) { continue; } if ( isException ) { if ( (bits & parser.OPTBlockOnly) !== 0 ) { continue; } diff --git a/src/js/filtering-context.js b/src/js/filtering-context.js index cad4e1684..20b66ccb6 100644 --- a/src/js/filtering-context.js +++ b/src/js/filtering-context.js @@ -23,55 +23,140 @@ /******************************************************************************/ -µBlock.FilteringContext = function(other) { - if ( other instanceof µBlock.FilteringContext ) { - return this.fromFilteringContext(other); - } - this.tstamp = 0; - this.realm = ''; - this.id = undefined; - this.type = undefined; - this.url = undefined; - this.aliasURL = undefined; - this.hostname = undefined; - this.domain = undefined; - this.docId = -1; - this.frameId = -1; - this.docOrigin = undefined; - this.docHostname = undefined; - this.docDomain = undefined; - this.tabId = undefined; - this.tabOrigin = undefined; - this.tabHostname = undefined; - this.tabDomain = undefined; - this.redirectURL = undefined; - this.filter = undefined; +{ +// >>>>> start of local scope + +/******************************************************************************/ + +const originFromURI = µBlock.URI.originFromURI; +const hostnameFromURI = vAPI.hostnameFromURI; +const domainFromHostname = vAPI.domainFromHostname; + +/******************************************************************************/ + +// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType + +// Long term, convert code wherever possible to work with integer-based type +// values -- the assumption being that integer operations are faster than +// string operations. + +const NO_TYPE = 0; +const BEACON = 1 << 0; +const CSP_REPORT = 1 << 1; +const FONT = 1 << 2; +const IMAGE = 1 << 4; +const IMAGESET = 1 << 4; +const MAIN_FRAME = 1 << 5; +const MEDIA = 1 << 6; +const OBJECT = 1 << 7; +const OBJECT_SUBREQUEST = 1 << 7; +const PING = 1 << 8; +const SCRIPT = 1 << 9; +const STYLESHEET = 1 << 10; +const SUB_FRAME = 1 << 11; +const WEBSOCKET = 1 << 12; +const XMLHTTPREQUEST = 1 << 13; +const INLINE_FONT = 1 << 14; +const INLINE_SCRIPT = 1 << 15; +const OTHER = 1 << 16; +const FRAME_ANY = MAIN_FRAME | SUB_FRAME; +const FONT_ANY = FONT | INLINE_FONT; +const INLINE_ANY = INLINE_FONT | INLINE_SCRIPT; +const PING_ANY = BEACON | CSP_REPORT | PING; +const SCRIPT_ANY = SCRIPT | INLINE_SCRIPT; + +const typeStrToIntMap = { + 'no_type': NO_TYPE, + 'beacon': BEACON, + 'csp_report': CSP_REPORT, + 'font': FONT, + 'image': IMAGE, + 'imageset': IMAGESET, + 'main_frame': MAIN_FRAME, + 'media': MEDIA, + 'object': OBJECT, + 'object_subrequest': OBJECT_SUBREQUEST, + 'ping': PING, + 'script': SCRIPT, + 'stylesheet': STYLESHEET, + 'sub_frame': SUB_FRAME, + 'websocket': WEBSOCKET, + 'xmlhttprequest': XMLHTTPREQUEST, + 'inline-font': INLINE_FONT, + 'inline-script': INLINE_SCRIPT, + 'other': OTHER, }; -µBlock.FilteringContext.prototype = { - fromTabId: function(tabId) { +/******************************************************************************/ + +const FilteringContext = class { + constructor(other) { + if ( other instanceof FilteringContext ) { + return this.fromFilteringContext(other); + } + this.tstamp = 0; + this.realm = ''; + this.id = undefined; + this.itype = 0; + this.stype = undefined; + this.url = undefined; + this.aliasURL = undefined; + this.hostname = undefined; + this.domain = undefined; + this.docId = -1; + this.frameId = -1; + this.docOrigin = undefined; + this.docHostname = undefined; + this.docDomain = undefined; + this.tabId = undefined; + this.tabOrigin = undefined; + this.tabHostname = undefined; + this.tabDomain = undefined; + this.redirectURL = undefined; + this.filter = undefined; + } + + get type() { + return this.stype; + } + + set type(a) { + this.itype = typeStrToIntMap[a] || NO_TYPE; + this.stype = a; + } + + isDocument() { + return (this.itype & FRAME_ANY) !== 0; + } + + isFont() { + return (this.itype & FONT_ANY) !== 0; + } + + fromTabId(tabId) { const tabContext = µBlock.tabContextManager.mustLookup(tabId); this.tabOrigin = tabContext.origin; this.tabHostname = tabContext.rootHostname; this.tabDomain = tabContext.rootDomain; this.tabId = tabContext.tabId; return this; - }, + } + // https://github.com/uBlockOrigin/uBlock-issues/issues/459 // In case of a request for frame and if ever no context is specified, // assume the origin of the context is the same as the request itself. - fromWebrequestDetails: function(details) { + fromWebrequestDetails(details) { const tabId = details.tabId; - if ( tabId > 0 && details.type === 'main_frame' ) { + this.fromTabId(tabId); + this.type = details.type; + if ( this.itype === MAIN_FRAME && tabId > 0 ) { µBlock.tabContextManager.push(tabId, details.url); } - this.fromTabId(tabId); this.realm = ''; this.id = details.requestId; - this.type = details.type; this.setURL(details.url); this.aliasURL = details.aliasURL || undefined; - if ( details.type !== 'sub_frame' ) { + if ( this.itype !== SUB_FRAME ) { this.docId = details.frameId; this.frameId = -1; } else { @@ -95,12 +180,12 @@ } } } else if ( details.documentUrl !== undefined ) { - const origin = this.originFromURI( + const origin = originFromURI( µBlock.normalizePageURL(0, details.documentUrl) ); this.setDocOrigin(origin).setTabOrigin(origin); - } else if ( this.docId === -1 || this.type.endsWith('_frame') ) { - const origin = this.originFromURI(this.url); + } else if ( this.docId === -1 || (this.itype & FRAME_ANY) !== 0 ) { + const origin = originFromURI(this.url); this.setDocOrigin(origin).setTabOrigin(origin); } else { this.setDocOrigin(this.tabOrigin); @@ -108,8 +193,9 @@ this.redirectURL = undefined; this.filter = undefined; return this; - }, - fromFilteringContext: function(other) { + } + + fromFilteringContext(other) { this.realm = other.realm; this.type = other.type; this.url = other.url; @@ -127,90 +213,106 @@ this.redirectURL = other.redirectURL; this.filter = undefined; return this; - }, - duplicate: function() { - return (new µBlock.FilteringContext(this)); - }, - setRealm: function(a) { + } + + duplicate() { + return (new FilteringContext(this)); + } + + setRealm(a) { this.realm = a; return this; - }, - setType: function(a) { + } + + setType(a) { this.type = a; return this; - }, - setURL: function(a) { + } + + setURL(a) { if ( a !== this.url ) { this.hostname = this.domain = undefined; this.url = a; } return this; - }, - getHostname: function() { + } + + getHostname() { if ( this.hostname === undefined ) { - this.hostname = this.hostnameFromURI(this.url); + this.hostname = hostnameFromURI(this.url); } return this.hostname; - }, - setHostname: function(a) { + } + + setHostname(a) { if ( a !== this.hostname ) { this.domain = undefined; this.hostname = a; } return this; - }, - getDomain: function() { + } + + getDomain() { if ( this.domain === undefined ) { - this.domain = this.domainFromHostname(this.getHostname()); + this.domain = domainFromHostname(this.getHostname()); } return this.domain; - }, - setDomain: function(a) { + } + + setDomain(a) { this.domain = a; return this; - }, - getDocOrigin: function() { + } + + getDocOrigin() { if ( this.docOrigin === undefined ) { this.docOrigin = this.tabOrigin; } return this.docOrigin; - }, - setDocOrigin: function(a) { + } + + setDocOrigin(a) { if ( a !== this.docOrigin ) { this.docHostname = this.docDomain = undefined; this.docOrigin = a; } return this; - }, - setDocOriginFromURL: function(a) { - return this.setDocOrigin(this.originFromURI(a)); - }, - getDocHostname: function() { + } + + setDocOriginFromURL(a) { + return this.setDocOrigin(originFromURI(a)); + } + + getDocHostname() { if ( this.docHostname === undefined ) { - this.docHostname = this.hostnameFromURI(this.getDocOrigin()); + this.docHostname = hostnameFromURI(this.getDocOrigin()); } return this.docHostname; - }, - setDocHostname: function(a) { + } + + setDocHostname(a) { if ( a !== this.docHostname ) { this.docDomain = undefined; this.docHostname = a; } return this; - }, - getDocDomain: function() { + } + + getDocDomain() { if ( this.docDomain === undefined ) { - this.docDomain = this.domainFromHostname(this.getDocHostname()); + this.docDomain = domainFromHostname(this.getDocHostname()); } return this.docDomain; - }, - setDocDomain: function(a) { + } + + setDocDomain(a) { this.docDomain = a; return this; - }, + } + // The idea is to minimize the amout of work done to figure out whether // the resource is 3rd-party to the document. - is3rdPartyToDoc: function() { + is3rdPartyToDoc() { let docDomain = this.getDocDomain(); if ( docDomain === '' ) { docDomain = this.docHostname; } if ( this.domain !== undefined && this.domain !== '' ) { @@ -221,12 +323,14 @@ const i = hostname.length - docDomain.length; if ( i === 0 ) { return false; } return hostname.charCodeAt(i - 1) !== 0x2E /* '.' */; - }, - setTabId: function(a) { + } + + setTabId(a) { this.tabId = a; return this; - }, - getTabOrigin: function() { + } + + getTabOrigin() { if ( this.tabOrigin === undefined ) { const tabContext = µBlock.tabContextManager.mustLookup(this.tabId); this.tabOrigin = tabContext.origin; @@ -234,43 +338,50 @@ this.tabDomain = tabContext.rootDomain; } return this.tabOrigin; - }, - setTabOrigin: function(a) { + } + + setTabOrigin(a) { if ( a !== this.tabOrigin ) { this.tabHostname = this.tabDomain = undefined; this.tabOrigin = a; } return this; - }, - setTabOriginFromURL: function(a) { - return this.setTabOrigin(this.originFromURI(a)); - }, - getTabHostname: function() { + } + + setTabOriginFromURL(a) { + return this.setTabOrigin(originFromURI(a)); + } + + getTabHostname() { if ( this.tabHostname === undefined ) { - this.tabHostname = this.hostnameFromURI(this.getTabOrigin()); + this.tabHostname = hostnameFromURI(this.getTabOrigin()); } return this.tabHostname; - }, - setTabHostname: function(a) { + } + + setTabHostname(a) { if ( a !== this.tabHostname ) { this.tabDomain = undefined; this.tabHostname = a; } return this; - }, - getTabDomain: function() { + } + + getTabDomain() { if ( this.tabDomain === undefined ) { - this.tabDomain = this.domainFromHostname(this.getTabHostname()); + this.tabDomain = domainFromHostname(this.getTabHostname()); } return this.tabDomain; - }, - setTabDomain: function(a) { + } + + setTabDomain(a) { this.docDomain = a; return this; - }, + } + // The idea is to minimize the amout of work done to figure out whether // the resource is 3rd-party to the top document. - is3rdPartyToTab: function() { + is3rdPartyToTab() { let tabDomain = this.getTabDomain(); if ( tabDomain === '' ) { tabDomain = this.tabHostname; } if ( this.domain !== undefined && this.domain !== '' ) { @@ -281,12 +392,38 @@ const i = hostname.length - tabDomain.length; if ( i === 0 ) { return false; } return hostname.charCodeAt(i - 1) !== 0x2E /* '.' */; - }, - setFilter: function(a) { + } + + setFilter(a) { this.filter = a; return this; - }, - toLogger: function() { + } + + pushFilter(a) { + if ( this.filter === undefined ) { + return this.setFilter(a); + } + if ( Array.isArray(this.filter) ) { + this.filter.push(a); + } else { + this.filter = [ this.filter, a ]; + } + return this; + } + + pushFilters(a) { + if ( this.filter === undefined ) { + return this.setFilter(a); + } + if ( Array.isArray(this.filter) ) { + this.filter.push(...a); + } else { + this.filter = [ this.filter, ...a ]; + } + return this; + } + + toLogger() { this.tstamp = Date.now(); if ( this.domain === undefined ) { void this.getDomain(); @@ -297,11 +434,49 @@ if ( this.tabDomain === undefined ) { void this.getTabDomain(); } - µBlock.logger.writeOne(this); - }, - originFromURI: µBlock.URI.originFromURI, - hostnameFromURI: µBlock.URI.hostnameFromURI, - domainFromHostname: µBlock.URI.domainFromHostname, + const logger = µBlock.logger; + const filters = this.filter; + // Many filters may have been applied to the current context + if ( Array.isArray(filters) === false ) { + return logger.writeOne(this); + } + for ( const filter of filters ) { + this.filter = filter; + logger.writeOne(this); + } + } }; -µBlock.filteringContext = new µBlock.FilteringContext(); +/******************************************************************************/ + +FilteringContext.prototype.BEACON = FilteringContext.BEACON = BEACON; +FilteringContext.prototype.CSP_REPORT = FilteringContext.CSP_REPORT = CSP_REPORT; +FilteringContext.prototype.FONT = FilteringContext.FONT = FONT; +FilteringContext.prototype.IMAGE = FilteringContext.IMAGE = IMAGE; +FilteringContext.prototype.IMAGESET = FilteringContext.IMAGESET = IMAGESET; +FilteringContext.prototype.MAIN_FRAME = FilteringContext.MAIN_FRAME = MAIN_FRAME; +FilteringContext.prototype.MEDIA = FilteringContext.MEDIA = MEDIA; +FilteringContext.prototype.OBJECT = FilteringContext.OBJECT = OBJECT; +FilteringContext.prototype.OBJECT_SUBREQUEST = FilteringContext.OBJECT_SUBREQUEST = OBJECT_SUBREQUEST; +FilteringContext.prototype.PING = FilteringContext.PING = PING; +FilteringContext.prototype.SCRIPT = FilteringContext.SCRIPT = SCRIPT; +FilteringContext.prototype.STYLESHEET = FilteringContext.STYLESHEET = STYLESHEET; +FilteringContext.prototype.SUB_FRAME = FilteringContext.SUB_FRAME = SUB_FRAME; +FilteringContext.prototype.WEBSOCKET = FilteringContext.WEBSOCKET = WEBSOCKET; +FilteringContext.prototype.XMLHTTPREQUEST = FilteringContext.XMLHTTPREQUEST = XMLHTTPREQUEST; +FilteringContext.prototype.INLINE_FONT = FilteringContext.INLINE_FONT = INLINE_FONT; +FilteringContext.prototype.INLINE_SCRIPT = FilteringContext.INLINE_SCRIPT = INLINE_SCRIPT; +FilteringContext.prototype.OTHER = FilteringContext.OTHER = OTHER; +FilteringContext.prototype.FRAME_ANY = FilteringContext.FRAME_ANY = FRAME_ANY; +FilteringContext.prototype.FONT_ANY = FilteringContext.FONT_ANY = FONT_ANY; +FilteringContext.prototype.INLINE_ANY = FilteringContext.INLINE_ANY = INLINE_ANY; +FilteringContext.prototype.PING_ANY = FilteringContext.PING_ANY = PING_ANY; +FilteringContext.prototype.SCRIPT_ANY = FilteringContext.SCRIPT_ANY = SCRIPT_ANY; + +/******************************************************************************/ + +µBlock.FilteringContext = FilteringContext; +µBlock.filteringContext = new FilteringContext(); + +// <<<<< end of local scope +} diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index cfd9ed4be..7a13b4d14 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -214,6 +214,7 @@ const LogEntry = function(details) { this[prop] = details[prop]; } } + this.type = details.stype; if ( details.aliasURL !== undefined ) { this.aliased = true; } diff --git a/src/js/logger.js b/src/js/logger.js index 8b5c05c31..6e3741402 100644 --- a/src/js/logger.js +++ b/src/js/logger.js @@ -62,10 +62,11 @@ ownerId: undefined, writeOne: function(details) { if ( buffer === null ) { return; } + const box = boxEntry(details); if ( writePtr === buffer.length ) { - buffer.push(boxEntry(details)); + buffer.push(box); } else { - buffer[writePtr] = boxEntry(details); + buffer[writePtr] = box; } writePtr += 1; }, diff --git a/src/js/messaging.js b/src/js/messaging.js index f3c57fdd4..253692f33 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -1546,10 +1546,12 @@ const logCSPViolations = function(pageStore, request) { cspData = new Map(); const staticDirectives = - µb.staticNetFilteringEngine.matchAndFetchData(fctxt, 'csp'); - for ( const directive of staticDirectives ) { - if ( directive.result !== 1 ) { continue; } - cspData.set(directive.getData('csp'), directive.logData()); + µb.staticNetFilteringEngine.matchAndFetchModifiers(fctxt, 'csp'); + if ( staticDirectives !== undefined ) { + for ( const directive of staticDirectives ) { + if ( directive.result !== 1 ) { continue; } + cspData.set(directive.value, directive.logData()); + } } fctxt.type = 'inline-script'; diff --git a/src/js/pagestore.js b/src/js/pagestore.js index ad80d1ae0..f5e7e7acd 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -545,27 +545,28 @@ const PageStore = class { return 0; } - const requestType = fctxt.type; - if ( - requestType === 'csp_report' && + fctxt.itype === fctxt.CSP_REPORT && this.filterCSPReport(fctxt) === 1 ) { return 1; } - if ( requestType.endsWith('font') && this.filterFont(fctxt) === 1 ) { + if ( + (fctxt.itype & fctxt.FONT_ANY) !== 0 && + this.filterFont(fctxt) === 1 ) + { return 1; } if ( - requestType === 'script' && + fctxt.itype === fctxt.SCRIPT && this.filterScripting(fctxt, true) === 1 ) { return 1; } - const cacheableResult = this.cacheableResults.has(requestType); + const cacheableResult = this.cacheableResults.has(fctxt.itype); if ( cacheableResult ) { const entry = this.netFilteringCache.lookupResult(fctxt); @@ -576,13 +577,16 @@ const PageStore = class { } } + const requestType = fctxt.type; + const loggerEnabled = µb.logger.enabled; + // Dynamic URL filtering. let result = µb.sessionURLFiltering.evaluateZ( fctxt.getTabHostname(), fctxt.url, requestType ); - if ( result !== 0 && µb.logger.enabled ) { + if ( result !== 0 && loggerEnabled ) { fctxt.filter = µb.sessionURLFiltering.toLogData(); } @@ -593,17 +597,17 @@ const PageStore = class { fctxt.getHostname(), requestType ); - if ( result !== 0 && result !== 3 && µb.logger.enabled ) { + if ( result !== 0 && result !== 3 && loggerEnabled ) { fctxt.filter = µb.sessionFirewall.toLogData(); } } // Static filtering has lowest precedence. + const snfe = µb.staticNetFilteringEngine; if ( result === 0 || result === 3 ) { - const snfe = µb.staticNetFilteringEngine; result = snfe.matchString(fctxt); if ( result !== 0 ) { - if ( µb.logger.enabled ) { + if ( loggerEnabled ) { fctxt.filter = snfe.toLogData(); } // https://github.com/uBlockOrigin/uBlock-issues/issues/943 @@ -625,8 +629,8 @@ const PageStore = class { const frameStore = this.getFrameStore(fctxt.frameId); if ( frameStore !== null && frameStore.clickToLoad ) { result = 2; - if ( µb.logger.enabled ) { - fctxt.setFilter({ + if ( loggerEnabled ) { + fctxt.pushFilter({ result, source: 'network', raw: 'click-to-load', @@ -635,19 +639,36 @@ const PageStore = class { } } + // Modifier(s)? + // A modifier is an action which transform the original network request. // https://github.com/gorhill/uBlock/issues/949 // Redirect blocked request? - if ( result === 1 && µb.hiddenSettings.ignoreRedirectFilters !== true ) { - const redirectURL = µb.redirectEngine.toURL(fctxt); - if ( redirectURL !== undefined ) { - fctxt.redirectURL = redirectURL; - this.internalRedirectionCount += 1; + // https://github.com/uBlockOrigin/uBlock-issues/issues/760 + // Redirect non-blocked request? + if ( (fctxt.itype & fctxt.INLINE_ANY) === 0 ) { + if ( result === 1 ) { + if ( µb.hiddenSettings.ignoreRedirectFilters !== true ) { + const redirectURL = µb.redirectEngine.toURL(fctxt); + if ( redirectURL !== undefined ) { + fctxt.redirectURL = redirectURL; + this.internalRedirectionCount += 1; + fctxt.pushFilter({ + source: 'redirect', + raw: µb.redirectEngine.resourceNameRegister + }); + } + } + } else if ( snfe.hasQuery(fctxt) ) { + const directives = snfe.filterQuery(fctxt); + if ( directives !== undefined && loggerEnabled ) { + fctxt.pushFilters(directives.map(a => a.logData())); + } } } if ( cacheableResult ) { this.netFilteringCache.rememberResult(fctxt, result); - } else if ( result === 1 && this.collapsibleResources.has(requestType) ) { + } else if ( result === 1 && this.collapsibleResources.has(fctxt.itype) ) { this.netFilteringCache.rememberBlock(fctxt); } @@ -670,7 +691,7 @@ const PageStore = class { } filterFont(fctxt) { - if ( fctxt.type === 'font' ) { + if ( fctxt.itype === fctxt.FONT ) { this.remoteFontCount += 1; } if ( @@ -800,7 +821,7 @@ const PageStore = class { } if ( exceptCname === false ) { return false; } if ( exceptCname instanceof Object ) { - fctxt.setFilter(exceptCname); + fctxt.pushFilter(exceptCname); } return true; } @@ -830,14 +851,14 @@ const PageStore = class { }; PageStore.prototype.cacheableResults = new Set([ - 'sub_frame', + µb.FilteringContext.SUB_FRAME, ]); PageStore.prototype.collapsibleResources = new Set([ - 'image', - 'media', - 'object', - 'sub_frame', + µb.FilteringContext.IMAGE, + µb.FilteringContext.MEDIA, + µb.FilteringContext.OBJECT, + µb.FilteringContext.SUB_FRAME, ]); µb.PageStore = PageStore; diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index 4df8f7c6f..098a99e5b 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -1922,11 +1922,12 @@ const OPTTokenPopunder = 26; const OPTTokenPopup = 27; const OPTTokenRedirect = 28; const OPTTokenRedirectRule = 29; -const OPTTokenScript = 30; -const OPTTokenShide = 31; -const OPTTokenXhr = 32; -const OPTTokenWebrtc = 33; -const OPTTokenWebsocket = 34; +const OPTTokenQueryprune = 30; +const OPTTokenScript = 31; +const OPTTokenShide = 32; +const OPTTokenXhr = 33; +const OPTTokenWebrtc = 34; +const OPTTokenWebsocket = 35; const OPTCanNegate = 1 << 8; const OPTBlockOnly = 1 << 9; @@ -1937,8 +1938,9 @@ const OPTDomainList = 1 << 13; const OPTType = 1 << 14; const OPTNetworkType = 1 << 15; const OPTRedirectType = 1 << 16; -const OPTRedirectableType = 1 << 17; -const OPTNotSupported = 1 << 18; +const OPTModifiableType = 1 << 17; +const OPTModifierType = 1 << 18; +const OPTNotSupported = 1 << 19; /******************************************************************************/ @@ -2003,6 +2005,7 @@ Parser.prototype.OPTTokenOther = OPTTokenOther; Parser.prototype.OPTTokenPing = OPTTokenPing; Parser.prototype.OPTTokenPopunder = OPTTokenPopunder; Parser.prototype.OPTTokenPopup = OPTTokenPopup; +Parser.prototype.OPTTokenQueryprune = OPTTokenQueryprune; Parser.prototype.OPTTokenRedirect = OPTTokenRedirect; Parser.prototype.OPTTokenRedirectRule = OPTTokenRedirectRule; Parser.prototype.OPTTokenScript = OPTTokenScript; @@ -2023,12 +2026,12 @@ Parser.prototype.OPTDomainList = OPTDomainList; Parser.prototype.OPTType = OPTType; Parser.prototype.OPTNetworkType = OPTNetworkType; Parser.prototype.OPTRedirectType = OPTRedirectType; -Parser.prototype.OPTRedirectableType = OPTRedirectableType; +Parser.prototype.OPTModifiableType = OPTModifiableType; Parser.prototype.OPTNotSupported = OPTNotSupported; /******************************************************************************/ -const netOptionTokens = new Map([ +const netOptionTokenDescriptors = new Map([ [ '1p', OPTToken1p | OPTCanNegate ], [ 'first-party', OPTToken1p | OPTCanNegate ], [ '3p', OPTToken3p | OPTCanNegate ], @@ -2036,47 +2039,136 @@ const netOptionTokens = new Map([ [ 'all', OPTTokenAll | OPTType | OPTNetworkType ], [ 'badfilter', OPTTokenBadfilter ], [ 'cname', OPTTokenCname | OPTAllowOnly | OPTType ], - [ 'csp', OPTTokenCsp | OPTMustAssign | OPTAllowMayAssign ], - [ 'css', OPTTokenCss | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ], - [ 'stylesheet', OPTTokenCss | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ], + [ 'csp', OPTTokenCsp | OPTMustAssign | OPTAllowMayAssign | OPTModifierType ], + [ 'css', OPTTokenCss | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], + [ 'stylesheet', OPTTokenCss | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], [ 'denyallow', OPTTokenDenyAllow | OPTMustAssign | OPTDomainList ], - [ 'doc', OPTTokenDoc | OPTType | OPTNetworkType | OPTCanNegate ], - [ 'document', OPTTokenDoc | OPTType | OPTNetworkType | OPTCanNegate ], + [ 'doc', OPTTokenDoc | OPTType | OPTNetworkType | OPTCanNegate | OPTModifiableType ], + [ 'document', OPTTokenDoc | OPTType | OPTNetworkType | OPTCanNegate | OPTModifiableType ], [ 'domain', OPTTokenDomain | OPTMustAssign | OPTDomainList ], [ 'ehide', OPTTokenEhide | OPTType ], [ 'elemhide', OPTTokenEhide | OPTType ], - [ 'empty', OPTTokenEmpty | OPTBlockOnly | OPTRedirectType ], - [ 'frame', OPTTokenFrame | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ], - [ 'subdocument', OPTTokenFrame | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ], - [ 'font', OPTTokenFont | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ], + [ 'empty', OPTTokenEmpty | OPTBlockOnly | OPTRedirectType | OPTModifierType ], + [ 'frame', OPTTokenFrame | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], + [ 'subdocument', OPTTokenFrame | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], + [ 'font', OPTTokenFont | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], [ 'genericblock', OPTTokenGenericblock | OPTNotSupported ], [ 'ghide', OPTTokenGhide | OPTType ], [ 'generichide', OPTTokenGhide | OPTType ], - [ 'image', OPTTokenImage | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ], + [ 'image', OPTTokenImage | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], [ 'important', OPTTokenImportant | OPTBlockOnly ], [ 'inline-font', OPTTokenInlineFont | OPTType | OPTCanNegate ], [ 'inline-script', OPTTokenInlineScript | OPTType | OPTCanNegate ], - [ 'media', OPTTokenMedia | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ], - [ 'mp4', OPTTokenMp4 | OPTType | OPTNetworkType | OPTBlockOnly | OPTRedirectType | OPTRedirectableType ], - [ 'object', OPTTokenObject | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ], - [ 'object-subrequest', OPTTokenObject | OPTCanNegate | OPTType | OPTNetworkType ], - [ 'other', OPTTokenOther | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ], - [ 'ping', OPTTokenPing | OPTCanNegate | OPTType | OPTNetworkType ], - [ 'beacon', OPTTokenPing | OPTCanNegate | OPTType | OPTNetworkType ], + [ 'media', OPTTokenMedia | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], + [ 'mp4', OPTTokenMp4 | OPTType | OPTNetworkType | OPTBlockOnly | OPTRedirectType | OPTModifierType ], + [ 'object', OPTTokenObject | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], + [ 'object-subrequest', OPTTokenObject | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], + [ 'other', OPTTokenOther | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], + [ 'ping', OPTTokenPing | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], + [ 'beacon', OPTTokenPing | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], [ 'popunder', OPTTokenPopunder | OPTType ], [ 'popup', OPTTokenPopup | OPTType | OPTCanNegate ], - [ 'redirect', OPTTokenRedirect | OPTMustAssign | OPTBlockOnly | OPTRedirectType ], - [ 'redirect-rule', OPTTokenRedirectRule | OPTMustAssign | OPTBlockOnly | OPTRedirectType ], - [ 'script', OPTTokenScript | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ], + [ 'queryprune', OPTTokenQueryprune | OPTMustAssign | OPTAllowMayAssign | OPTModifierType ], + [ 'redirect', OPTTokenRedirect | OPTMustAssign | OPTBlockOnly | OPTRedirectType | OPTModifierType ], + [ 'redirect-rule', OPTTokenRedirectRule | OPTMustAssign | OPTBlockOnly | OPTRedirectType | OPTModifierType ], + [ 'script', OPTTokenScript | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], [ 'shide', OPTTokenShide | OPTType ], [ 'specifichide', OPTTokenShide | OPTType ], - [ 'xhr', OPTTokenXhr | OPTCanNegate| OPTType | OPTNetworkType | OPTRedirectableType ], - [ 'xmlhttprequest', OPTTokenXhr | OPTCanNegate | OPTType | OPTNetworkType | OPTRedirectableType ], + [ 'xhr', OPTTokenXhr | OPTCanNegate| OPTType | OPTNetworkType | OPTModifiableType ], + [ 'xmlhttprequest', OPTTokenXhr | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], [ 'webrtc', OPTTokenWebrtc | OPTNotSupported ], - [ 'websocket', OPTTokenWebsocket | OPTCanNegate | OPTType | OPTNetworkType ], + [ 'websocket', OPTTokenWebsocket | OPTCanNegate | OPTType | OPTNetworkType | OPTModifiableType ], ]); -Parser.prototype.netOptionTokens = netOptionTokens; +Parser.prototype.netOptionTokenDescriptors = + Parser.netOptionTokenDescriptors = netOptionTokenDescriptors; + +Parser.netOptionTokenIds = new Map([ + [ '1p', OPTToken1p ], + [ 'first-party', OPTToken1p ], + [ '3p', OPTToken3p ], + [ 'third-party', OPTToken3p ], + [ 'all', OPTTokenAll ], + [ 'badfilter', OPTTokenBadfilter ], + [ 'cname', OPTTokenCname ], + [ 'csp', OPTTokenCsp ], + [ 'css', OPTTokenCss ], + [ 'stylesheet', OPTTokenCss ], + [ 'denyallow', OPTTokenDenyAllow ], + [ 'doc', OPTTokenDoc ], + [ 'document', OPTTokenDoc ], + [ 'domain', OPTTokenDomain ], + [ 'ehide', OPTTokenEhide ], + [ 'elemhide', OPTTokenEhide ], + [ 'empty', OPTTokenEmpty ], + [ 'frame', OPTTokenFrame ], + [ 'subdocument', OPTTokenFrame ], + [ 'font', OPTTokenFont ], + [ 'genericblock', OPTTokenGenericblock ], + [ 'ghide', OPTTokenGhide ], + [ 'generichide', OPTTokenGhide ], + [ 'image', OPTTokenImage ], + [ 'important', OPTTokenImportant ], + [ 'inline-font', OPTTokenInlineFont ], + [ 'inline-script', OPTTokenInlineScript ], + [ 'media', OPTTokenMedia ], + [ 'mp4', OPTTokenMp4 ], + [ 'object', OPTTokenObject ], + [ 'object-subrequest', OPTTokenObject ], + [ 'other', OPTTokenOther ], + [ 'ping', OPTTokenPing ], + [ 'beacon', OPTTokenPing ], + [ 'popunder', OPTTokenPopunder ], + [ 'popup', OPTTokenPopup ], + [ 'queryprune', OPTTokenQueryprune ], + [ 'redirect', OPTTokenRedirect ], + [ 'redirect-rule', OPTTokenRedirectRule ], + [ 'script', OPTTokenScript ], + [ 'shide', OPTTokenShide ], + [ 'specifichide', OPTTokenShide ], + [ 'xhr', OPTTokenXhr ], + [ 'xmlhttprequest', OPTTokenXhr ], + [ 'webrtc', OPTTokenWebrtc ], + [ 'websocket', OPTTokenWebsocket ], +]); + +Parser.netOptionTokenNames = new Map([ + [ OPTToken1p, '1p' ], + [ OPTToken3p, '3p' ], + [ OPTTokenAll, 'all' ], + [ OPTTokenBadfilter, 'badfilter' ], + [ OPTTokenCname, 'cname' ], + [ OPTTokenCsp, 'csp' ], + [ OPTTokenCss, 'stylesheet' ], + [ OPTTokenDenyAllow, 'denyallow' ], + [ OPTTokenDoc, 'document' ], + [ OPTTokenDomain, 'domain' ], + [ OPTTokenEhide, 'elemhide' ], + [ OPTTokenEmpty, 'empty' ], + [ OPTTokenFrame, 'subdocument' ], + [ OPTTokenFont, 'font' ], + [ OPTTokenGenericblock, 'genericblock' ], + [ OPTTokenGhide, 'generichide' ], + [ OPTTokenImage, 'image' ], + [ OPTTokenImportant, 'important' ], + [ OPTTokenInlineFont, 'inline-font' ], + [ OPTTokenInlineScript, 'inline-script' ], + [ OPTTokenMedia, 'media' ], + [ OPTTokenMp4, 'mp4' ], + [ OPTTokenObject, 'object' ], + [ OPTTokenOther, 'other' ], + [ OPTTokenPing, 'ping' ], + [ OPTTokenPopunder, 'popunder' ], + [ OPTTokenPopup, 'popup' ], + [ OPTTokenQueryprune, 'queryprune' ], + [ OPTTokenRedirect, 'redirect' ], + [ OPTTokenRedirectRule, 'redirect-rule' ], + [ OPTTokenScript, 'script' ], + [ OPTTokenShide, 'specifichide' ], + [ OPTTokenXhr, 'xmlhttprequest' ], + [ OPTTokenWebrtc, 'webrtc' ], + [ OPTTokenWebsocket, 'websocket' ], +]); /******************************************************************************/ @@ -2134,9 +2226,8 @@ const NetOptionsIterator = class { const slices = this.parser.slices; const optSlices = this.optSlices; let typeCount = 0; - let redirectableTypeCount = 0; - let redirectIndex = -1; - let cspIndex = -1; + let modifiableTypeCount = 0; + let modifierIndex = -1; let writePtr = 0; let lopt = lopts; while ( lopt < ropts ) { @@ -2170,7 +2261,7 @@ const NetOptionsIterator = class { if ( good ) { const rtok = lval === 0 ? i : lval; const token = this.parser.raw.slice(slices[ltok+1], slices[rtok+1]); - descriptor = netOptionTokens.get(token); + descriptor = netOptionTokenDescriptors.get(token); } // Validate option according to context if ( @@ -2189,20 +2280,17 @@ const NetOptionsIterator = class { // Keep count of types if ( hasBits(descriptor, OPTType) ) { typeCount += 1; - if ( hasBits(descriptor, OPTRedirectableType) ) { - redirectableTypeCount += 1; - } - } - // Only one `redirect` or `csp` can be present - if ( hasBits(descriptor, OPTRedirectType) ) { - if ( redirectIndex === -1 ) { - redirectIndex = writePtr; - } else { + const modifiable = hasBits(descriptor, OPTModifiableType); + if ( modifiable ) { + modifiableTypeCount += 1; + } else if ( modifierIndex !== -1 ) { descriptor = OPTTokenInvalid; } - } else if ( (descriptor & 0xFF) === OPTTokenCsp ) { - if ( cspIndex === -1 ) { - cspIndex = writePtr; + } + // Only one modifier can be present + if ( hasBits(descriptor, OPTModifierType) ) { + if ( modifierIndex === -1 ) { + modifierIndex = writePtr; } else { descriptor = OPTTokenInvalid; } @@ -2244,38 +2332,26 @@ const NetOptionsIterator = class { if ( this.interactive && hasBits(this.parser.slices[ropts-3], BITComma) ) { this.parser.slices[ropts-3] |= BITError; } - // Invalid combinations of options - // - // `csp` can't be used with any other types or redirection - if ( cspIndex !== -1 && ( typeCount !== 0 || redirectIndex !== -1 ) ) { - optSlices[cspIndex] = OPTTokenInvalid; - if ( this.interactive ) { - this.parser.markSlices( - optSlices[cspIndex+1], - optSlices[cspIndex+5], - BITError - ); - } - } // `redirect` requires one single redirectable type, EXCEPT for when we // redirect to `empty`, in which case it is allowed to not have any // network type specified. if ( - redirectIndex !== -1 && - redirectableTypeCount !== 1 && ( - redirectableTypeCount !== 0 || + modifierIndex !== -1 && + hasBits(optSlices[modifierIndex+0], OPTRedirectType) && + modifiableTypeCount !== 1 && ( + modifiableTypeCount !== 0 || typeCount !== 0 || this.parser.raw.slice( - this.parser.slices[optSlices[redirectIndex+0]+1], - this.parser.slices[optSlices[redirectIndex+5]+1] + this.parser.slices[optSlices[modifierIndex+0]+1], + this.parser.slices[optSlices[modifierIndex+5]+1] ).endsWith('empty') === false ) ) { - optSlices[redirectIndex] = OPTTokenInvalid; + optSlices[modifierIndex] = OPTTokenInvalid; if ( this.interactive ) { this.parser.markSlices( - optSlices[redirectIndex+1], - optSlices[redirectIndex+5], + optSlices[modifierIndex+1], + optSlices[modifierIndex+5], BITError ); } diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 0b10b9161..daa36b2e5 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -33,103 +33,117 @@ const µb = µBlock; const urlTokenizer = µb.urlTokenizer; // fedcba9876543210 -// | | ||| -// | | ||| -// | | ||| -// | | ||| -// | | ||+---- bit 0: [BlockAction | AllowAction] -// | | |+----- bit 1: `important` -// | | +------ bit 2- 3: party [0 - 3] -// | +-------- bit 4- 8: type [0 - 31] -// +------------- bit 9-15: unused +// | | || | +// | | || | +// | | || | +// | | || | +// | | || +---- bit 0- 1: block=0, allow=1, modify=2 +// | | |+------ bit 2: important +// | | +------- bit 3- 4: party [0-3] +// | +--------- bit 5- 9: type [0-31] +// +-------------- bit 10-15: unused + +const ActionBitsMask = 0b0000000011; +const TypeBitsMask = 0b1111100000; +const TypeBitsOffset = 5; + +const BlockAction = 0 << 0; +const AllowAction = 1 << 0; +const ModifyAction = 2 << 0; +// Note: +// It's possible to increase granularity of ModifyAction realm with +// sub-realms if it helps performance, but so far I found it's not +// needed, there is no meaningful gains to be had. + +const Important = 1 << 2; -const BlockAction = 0 << 0; -const AllowAction = 1 << 0; -const Important = 1 << 1; -const AnyParty = 0 << 2; -const FirstParty = 1 << 2; -const ThirdParty = 2 << 2; const BlockImportant = BlockAction | Important; +const AnyParty = 0 << 3; +const FirstParty = 1 << 3; +const ThirdParty = 2 << 3; + + const typeNameToTypeValue = { - 'no_type': 0 << 4, - 'stylesheet': 1 << 4, - 'image': 2 << 4, - 'object': 3 << 4, - 'object_subrequest': 3 << 4, - 'script': 4 << 4, - 'fetch': 5 << 4, - 'xmlhttprequest': 5 << 4, - 'sub_frame': 6 << 4, - 'font': 7 << 4, - 'media': 8 << 4, - 'websocket': 9 << 4, - 'beacon': 10 << 4, - 'ping': 10 << 4, - 'other': 11 << 4, - 'popup': 12 << 4, // start of behavorial filtering - 'popunder': 13 << 4, - 'main_frame': 14 << 4, // start of 1st-party-only behavorial filtering - 'generichide': 15 << 4, - 'specifichide': 16 << 4, - 'inline-font': 17 << 4, - 'inline-script': 18 << 4, - 'cname': 19 << 4, - 'data': 20 << 4, // special: a generic data holder - 'redirect': 21 << 4, - 'webrtc': 22 << 4, - 'unsupported': 23 << 4, + 'no_type': 0 << TypeBitsOffset, + 'stylesheet': 1 << TypeBitsOffset, + 'image': 2 << TypeBitsOffset, + 'object': 3 << TypeBitsOffset, + 'object_subrequest': 3 << TypeBitsOffset, + 'script': 4 << TypeBitsOffset, + 'fetch': 5 << TypeBitsOffset, + 'xmlhttprequest': 5 << TypeBitsOffset, + 'sub_frame': 6 << TypeBitsOffset, + 'font': 7 << TypeBitsOffset, + 'media': 8 << TypeBitsOffset, + 'websocket': 9 << TypeBitsOffset, + 'beacon': 10 << TypeBitsOffset, + 'ping': 10 << TypeBitsOffset, + 'other': 11 << TypeBitsOffset, + 'popup': 12 << TypeBitsOffset, // start of behavorial filtering + 'popunder': 13 << TypeBitsOffset, + 'main_frame': 14 << TypeBitsOffset, // start of 1p behavorial filtering + 'generichide': 15 << TypeBitsOffset, + 'specifichide': 16 << TypeBitsOffset, + 'inline-font': 17 << TypeBitsOffset, + 'inline-script': 18 << TypeBitsOffset, + 'cname': 19 << TypeBitsOffset, + 'unused': 20 << TypeBitsOffset, + 'redirect': 21 << TypeBitsOffset, + 'webrtc': 22 << TypeBitsOffset, + 'unsupported': 23 << TypeBitsOffset, }; const otherTypeBitValue = typeNameToTypeValue.other; const bitFromType = type => - 1 << ((typeNameToTypeValue[type] >>> 4) - 1); + 1 << ((typeNameToTypeValue[type] >>> TypeBitsOffset) - 1); // All network request types to bitmap -// bring origin to 0 (from 4 -- see typeNameToTypeValue) +// bring origin to 0 (from TypeBitsOffset -- see typeNameToTypeValue) // left-shift 1 by the above-calculated value // subtract 1 to set all type bits const allNetworkTypesBits = - (1 << (otherTypeBitValue >>> 4)) - 1; + (1 << (otherTypeBitValue >>> TypeBitsOffset)) - 1; const allTypesBits = allNetworkTypesBits | - 1 << (typeNameToTypeValue['popup'] >>> 4) - 1 | - 1 << (typeNameToTypeValue['main_frame'] >>> 4) - 1 | - 1 << (typeNameToTypeValue['inline-font'] >>> 4) - 1 | - 1 << (typeNameToTypeValue['inline-script'] >>> 4) - 1; + 1 << (typeNameToTypeValue['popup'] >>> TypeBitsOffset) - 1 | + 1 << (typeNameToTypeValue['main_frame'] >>> TypeBitsOffset) - 1 | + 1 << (typeNameToTypeValue['inline-font'] >>> TypeBitsOffset) - 1 | + 1 << (typeNameToTypeValue['inline-script'] >>> TypeBitsOffset) - 1; const unsupportedTypeBit = - 1 << (typeNameToTypeValue['unsupported'] >>> 4) - 1; + 1 << (typeNameToTypeValue['unsupported'] >>> TypeBitsOffset) - 1; -const typeValueToTypeName = { - 1: 'stylesheet', - 2: 'image', - 3: 'object', - 4: 'script', - 5: 'xmlhttprequest', - 6: 'subdocument', - 7: 'font', - 8: 'media', - 9: 'websocket', - 10: 'ping', - 11: 'other', - 12: 'popup', - 13: 'popunder', - 14: 'document', - 15: 'generichide', - 16: 'specifichide', - 17: 'inline-font', - 18: 'inline-script', - 19: 'cname', - 20: 'data', - 21: 'redirect', - 22: 'webrtc', - 23: 'unsupported', -}; +const typeValueToTypeName = [ + '', + 'stylesheet', + 'image', + 'object', + 'script', + 'xmlhttprequest', + 'subdocument', + 'font', + 'media', + 'websocket', + 'ping', + 'other', + 'popup', + 'popunder', + 'document', + 'generichide', + 'specifichide', + 'inline-font', + 'inline-script', + 'cname', + 'unused', + 'redirect', + 'webrtc', + 'unsupported', +]; -const typeValueFromCatBits = catBits => (catBits >>> 4) & 0b11111; +//const typeValueFromCatBits = catBits => (catBits >>> TypeBitsOffset) & 0b11111; /******************************************************************************/ @@ -222,9 +236,9 @@ const toLogDataInternal = function(categoryBits, tokenHash, iunit) { } else if ( categoryBits & 0x004 ) { logData.options.unshift('1p'); } - const type = categoryBits & 0x1F0; - if ( type !== 0 && type !== typeNameToTypeValue.data ) { - logData.options.unshift(typeValueToTypeName[type >>> 4]); + const type = categoryBits & TypeBitsMask; + if ( type !== 0 ) { + logData.options.unshift(typeValueToTypeName[type >>> TypeBitsOffset]); } let raw = logData.pattern.join(''); if ( @@ -1444,80 +1458,96 @@ registerFilterClass(FilterOriginEntityMiss); /******************************************************************************/ -const FilterDataHolder = class { - constructor(dataType, data) { - this.dataType = dataType; - this.data = data; +const FilterModifier = class { + constructor(actionBits, modifier, value) { + this.actionBits = actionBits; + this.type = modifier; + this.value = value; + this.cache = undefined; } match() { return true; } - matchAndFetchData(type, callback) { - if ( this.dataType !== type ) { return; } - callback(this); + matchAndFetchModifiers(env) { + if ( this.type !== env.modifier ) { return; } + env.results.push( + new FilterModifierResult(env.bits, env.th, env.iunit) + ); } - getData(type) { - if ( type === this.dataType ) { - return this.data; - } + get modifier() { + return this; } logData(details) { - let opt = this.dataType; - if ( this.data !== '' ) { - opt += `=${this.data}`; + let opt = vAPI.StaticFilteringParser.netOptionTokenNames.get(this.type); + if ( this.value !== '' ) { + opt += `=${this.value}`; } details.options.push(opt); } toSelfie() { - return [ this.fid, this.dataType, this.data ]; + return [ this.fid, this.actionBits, this.type, this.value ]; } static compile(details) { - return [ FilterDataHolder.fid, details.dataType, details.data ]; + return [ + FilterModifier.fid, + details.action | details.important, + details.modifyType, + details.modifyValue + ]; } static fromCompiled(args) { - return new FilterDataHolder(args[1], args[2]); + return new FilterModifier(args[1], args[2], args[3]); } static fromSelfie(args) { - return new FilterDataHolder(args[1], args[2]); + return new FilterModifier(args[1], args[2], args[3]); } static keyFromArgs(args) { - return `${args[1]}\t${args[2]}`; + return `${args[1]}\t${args[2]}\t${args[3]}`; } }; -registerFilterClass(FilterDataHolder); +registerFilterClass(FilterModifier); -// Helper class for storing instances of FilterDataHolder which were found to +// Helper class for storing instances of FilterModifier which were found to // be a match. -const FilterDataHolderResult = class { +const FilterModifierResult = class { constructor(bits, th, iunit) { - this.bits = bits; - this.th = th; this.iunit = iunit; + this.th = th; + this.bits = (bits & ~ActionBitsMask) | this.modifier.actionBits; } - getData(type) { - return filterUnits[this.iunit].getData(type); + get filter() { + return filterUnits[this.iunit]; + } + + get modifier() { + return this.filter.modifier; } get result() { return (this.bits & AllowAction) === 0 ? 1 : 2; } + get value() { + return this.modifier.value; + } + logData() { const r = toLogDataInternal(this.bits, this.th, this.iunit); r.source = 'static'; r.result = this.result; + r.modifier = true; return r; } }; @@ -1636,20 +1666,21 @@ const FilterCompositeAll = class extends FilterCollection { return true; } - matchAndFetchData(type, callback) { + matchAndFetchModifiers(env) { if ( this.match() !== true ) { return false; } this.forEach(iunit => { const f = filterUnits[iunit]; - if ( f.matchAndFetchData instanceof Function === false ) { return; } - f.matchAndFetchData(type, ( ) => { callback(this); }); + if ( f.matchAndFetchModifiers instanceof Function ) { + f.matchAndFetchModifiers(env); + } }); } - getData(type) { + get modifier() { return this.forEach(iunit => { const f = filterUnits[iunit]; - if ( f.matchAndFetchData instanceof Function ) { - return f.getData(type); + if ( f.matchAndFetchModifiers instanceof Function ) { + return f.modifier; } }); } @@ -1902,7 +1933,7 @@ const FilterPlainTrie = class { return false; } - matchAndFetchData(/* type, out */) { + matchAndFetchModifiers(/* type, callback */) { // TODO } @@ -1953,12 +1984,11 @@ const FilterBucket = class extends FilterCollection { return false; } - matchAndFetchData(type, callback) { + matchAndFetchModifiers(env) { const units = filterUnits; this.forEach(iunit => { - units[iunit].matchAndFetchData(type, f => { - callback(f, iunit); - }); + env.iunit = iunit; + units[iunit].matchAndFetchModifiers(env); }); } @@ -2226,8 +2256,8 @@ const FilterParser = class { // 0101 (0x5): anchored to the hostname and end of the URL. this.anchor = 0; this.badFilter = false; - this.dataType = undefined; - this.data = undefined; + this.modifyType = undefined; + this.modifyValue = undefined; this.invalid = false; this.pattern = ''; this.firstParty = false; @@ -2325,18 +2355,17 @@ const FilterParser = class { this.badFilter = true; break; case parser.OPTTokenCsp: - this.typeBits = bitFromType('data'); - this.dataType = 'csp'; + this.modifyType = parser.OPTTokenCsp; if ( val !== undefined ) { if ( this.reBadCSP.test(val) ) { return false; } - this.data = val; + this.modifyValue = val; } else if ( this.action === AllowAction ) { - this.data = ''; + this.modifyValue = ''; } break; // https://github.com/gorhill/uBlock/issues/2294 - // Detect and discard filter if domain option contains nonsensical - // characters. + // Detect and discard filter if domain option contains + // nonsensical characters. case parser.OPTTokenDomain: this.domainOpt = this.parseHostnameList( parser, @@ -2368,6 +2397,14 @@ const FilterParser = class { if ( this.redirect !== 0 ) { return false; } this.redirect = id === parser.OPTTokenRedirectRule ? 2 : 1; break; + case parser.OPTTokenQueryprune: + this.modifyType = parser.OPTTokenQueryprune; + if ( val !== undefined ) { + this.modifyValue = val; + } else if ( this.action === AllowAction ) { + this.modifyValue = ''; + } + break; case parser.OPTTokenInvalid: return false; default: @@ -2576,7 +2613,7 @@ const FilterParser = class { isJustOrigin() { return this.isRegex === false && - this.dataType === undefined && + this.modifyType === undefined && this.denyallowOpt === '' && this.domainOpt !== '' && ( this.pattern === '*' || ( @@ -2728,7 +2765,7 @@ FilterContainer.prototype.freeze = function() { // Special cases: delegate to more specialized engines. // Redirect engine. - if ( (bits & 0x1F0) === redirectTypeValue ) { + if ( (bits & TypeBitsMask) === redirectTypeValue ) { µb.redirectEngine.fromCompiledRule(args[1]); continue; } @@ -2803,10 +2840,9 @@ FilterContainer.prototype.freeze = function() { this.badFilters.clear(); this.goodFilters.clear(); - // Skip 'data' type since bidi-trie does not (yet) support matchAll(). - const dataTypeValue = typeValueFromCatBits(typeNameToTypeValue['data']); + // Skip modify realm, since bidi-trie does not (yet) support matchAll(). for ( const [ catBits, bucket ] of this.categories ) { - if ( typeValueFromCatBits(catBits) === dataTypeValue ) { continue; } + if ( (catBits & ActionBitsMask) === ModifyAction ) { continue; } for ( const iunit of bucket.values() ) { const f = units[iunit]; if ( f instanceof FilterBucket === false ) { continue; } @@ -2980,7 +3016,7 @@ FilterContainer.prototype.compile = function(parser, writer) { parsed.isPureHostname && parsed.domainOpt === '' && parsed.denyallowOpt === '' && - parsed.dataType === undefined + parsed.modifyType === undefined ) { parsed.tokenHash = this.dotTokenHash; this.compileToAtomicFilter(parsed, parsed.pattern, writer); @@ -3064,8 +3100,10 @@ FilterContainer.prototype.compile = function(parser, writer) { } // Data - if ( parsed.dataType !== undefined ) { - units.push(FilterDataHolder.compile(parsed)); + if ( parsed.modifyType !== undefined ) { + units.push(FilterModifier.compile(parsed)); + parsed.action = ModifyAction; + parsed.important = 0; } const fdata = units.length === 1 @@ -3108,7 +3146,7 @@ FilterContainer.prototype.compileToAtomicFilter = function( do { if ( typeBits & 1 ) { writer.push([ - catBits | (bitOffset << 4), + catBits | (bitOffset << TypeBitsOffset), parsed.tokenHash, fdata ]); @@ -3154,70 +3192,113 @@ FilterContainer.prototype.fromCompiledContent = function(reader) { /******************************************************************************/ -FilterContainer.prototype.realmMatchAndFetchData = function( - realmBits, - partyBits, - type, - out +// TODO: +// Evaluate converting redirect directives in redirect engine into +// modifiers in static network filtering engine. +// +// Advantages: no more syntax quirks, gain all performance benefits, ability +// to reverse-lookup list of redirect directive in logger. +// +// Challenges: need to figure a way to calculate specificity so that the +// most specific redirect directive out of many can be +// identified (possibly based on total number of hostname labels +// seen at compile time). + +FilterContainer.prototype.matchAndFetchModifiers = function( + fctxt, + modifierType ) { - const bits01 = realmBits | typeNameToTypeValue.data; - const bits11 = realmBits | typeNameToTypeValue.data | partyBits; - - const bucket01 = this.categories.get(bits01); - const bucket11 = partyBits !== 0 - ? this.categories.get(bits11) - : undefined; - - if ( bucket01 === undefined && bucket11 === undefined ) { return false; } - - const t = type, o = out; // to avoid jshint warning - const fdhr = (a, b, c) => new FilterDataHolderResult(a, b, c); - const units = filterUnits; - const tokenHashes = urlTokenizer.getTokens(bidiTrie); - let i = 0; - for (;;) { - const th = tokenHashes[i]; - if ( th === 0 ) { return; } - $tokenBeg = tokenHashes[i+1]; - if ( bucket01 !== undefined ) bucket01: { - const iunit = bucket01.get(th); - if ( iunit === undefined ) { break bucket01; } - units[iunit].matchAndFetchData(type, (f, i) => { - o.set(f.getData(t), fdhr(bits01, th, i || iunit)); - }); - } - if ( bucket11 !== undefined ) bucket11: { - const iunit = bucket11.get(th); - if ( iunit === undefined ) { break bucket11; } - units[iunit].matchAndFetchData(t, (f, i) => { - o.set(f.getData(t), fdhr(bits11, th, i || iunit)); - }); - } - i += 2; - } -}; - -/******************************************************************************/ - -FilterContainer.prototype.matchAndFetchData = function(fctxt, type) { $requestURL = urlTokenizer.setURL(fctxt.url); $docHostname = fctxt.getDocHostname(); $docDomain = fctxt.getDocDomain(); $docEntity.reset(); $requestHostname = fctxt.getHostname(); + const typeBits = typeNameToTypeValue[fctxt.type] || otherTypeBitValue; const partyBits = fctxt.is3rdPartyToDoc() ? ThirdParty : FirstParty; + const catBits00 = ModifyAction; + const catBits01 = ModifyAction | typeBits; + const catBits10 = ModifyAction | partyBits; + const catBits11 = ModifyAction | typeBits | partyBits; + + const bucket00 = this.categories.get(catBits00); + const bucket01 = typeBits !== 0 + ? this.categories.get(catBits01) + : undefined; + const bucket10 = partyBits !== 0 + ? this.categories.get(catBits10) + : undefined; + const bucket11 = typeBits !== 0 && partyBits !== 0 + ? this.categories.get(catBits11) + : undefined; + + const modifierResults = []; + const env = { + modifier: vAPI.StaticFilteringParser.netOptionTokenIds.get(modifierType) || 0, + bits: 0, + th: 0, + iunit: 0, + results: modifierResults, + }; + + const units = filterUnits; + const tokenHashes = urlTokenizer.getTokens(bidiTrie); + let i = 0; + for (;;) { + const th = tokenHashes[i]; + if ( th === 0 ) { break; } + env.th = th; + $tokenBeg = tokenHashes[i+1]; + if ( bucket00 !== undefined ) { + const iunit = bucket00.get(th); + if ( iunit !== undefined ) { + env.bits = catBits00; env.iunit = iunit; + units[iunit].matchAndFetchModifiers(env); + } + } + if ( bucket01 !== undefined ) { + const iunit = bucket01.get(th); + if ( iunit !== undefined ) { + env.bits = catBits01; env.iunit = iunit; + units[iunit].matchAndFetchModifiers(env); + } + } + if ( bucket10 !== undefined ) { + const iunit = bucket10.get(th); + if ( iunit !== undefined ) { + env.bits = catBits10; env.iunit = iunit; + units[iunit].matchAndFetchModifiers(env); + } + } + if ( bucket11 !== undefined ) { + const iunit = bucket11.get(th); + if ( iunit !== undefined ) { + env.bits = catBits11; env.iunit = iunit; + units[iunit].matchAndFetchModifiers(env); + } + } + i += 2; + } + + if ( modifierResults.length === 0 ) { return; } + const toAddImportant = new Map(); - this.realmMatchAndFetchData(BlockImportant, partyBits, type, toAddImportant); - const toAdd = new Map(); - this.realmMatchAndFetchData(BlockAction, partyBits, type, toAdd); - - if ( toAddImportant.size === 0 && toAdd.size === 0 ) { return []; } - const toRemove = new Map(); - this.realmMatchAndFetchData(AllowAction, partyBits, type, toRemove); + + for ( const modifierResult of modifierResults ) { + const actionBits = modifierResult.bits & ActionBitsMask; + const modifyValue = modifierResult.modifier.value; + if ( actionBits === BlockImportant ) { + toAddImportant.set(modifyValue, modifierResult); + } else if ( actionBits === BlockAction ) { + toAdd.set(modifyValue, modifierResult); + } else { + toRemove.set(modifyValue, modifierResult); + } + } + if ( toAddImportant.size === 0 && toAdd.size === 0 ) { return; } // Remove entries overriden by important block filters. for ( const key of toAddImportant.keys() ) { @@ -3231,9 +3312,11 @@ FilterContainer.prototype.matchAndFetchData = function(fctxt, type) { if ( toRemove.has('') ) { if ( toAdd.size !== 0 ) { toAdd.clear(); - toRemove.forEach((v, k, m) => { - if ( k !== '' ) { m.delete(k); } - }); + if ( toRemove.size !== 1 ) { + const entry = toRemove.get(''); + toRemove.clear(); + toRemove.set('', entry); + } } else { toRemove.clear(); } @@ -3249,11 +3332,22 @@ FilterContainer.prototype.matchAndFetchData = function(fctxt, type) { } } - // Merge important and normal block filters - for ( const [ key, entry ] of toAddImportant ) { - toAdd.set(key, entry); + if ( + toAdd.size === 0 && + toAddImportant.size === 0 && + toRemove.size === 0 + ) { + return; } - return Array.from(toAdd.values()).concat(Array.from(toRemove.values())); + + const out = Array.from(toAdd.values()); + if ( toAddImportant.size !== 0 ) { + out.push(...toAddImportant.values()); + } + if ( toRemove.size !== 0 ) { + out.push(...toRemove.values()); + } + return out; }; /******************************************************************************/ @@ -3460,6 +3554,46 @@ FilterContainer.prototype.matchString = function(fctxt, modifiers = 0) { /******************************************************************************/ +FilterContainer.prototype.filterQuery = function(fctxt) { + const directives = this.matchAndFetchModifiers(fctxt, 'queryprune'); + if ( directives === undefined ) { return; } + const params = []; + const out = []; + const url = new URL(fctxt.url); + for ( const directive of directives ) { + const modifier = directive.modifier; + if ( modifier.cache === undefined ) { + let retext = modifier.value; + if ( retext.startsWith('|') ) { retext = `^${retext.slice(1)}`; } + if ( retext.endsWith('|') ) { retext = `${retext.slice(0,-1)}$`; } + modifier.cache = new RegExp(retext); + } + const re = modifier.cache; + let filtered = false; + for ( const [ key, value ] of url.searchParams ) { + if ( re.test(`${key}=${value}`) ) { + filtered = true; + } else { + params.push(`${key}=${encodeURIComponent(value)}`); + } + } + if ( filtered ) { + out.push(directive); + } + } + if ( out.length === 0 ) { return; } + url.search = params.length !== 0 ? `?${params.join('&')}` : ''; + fctxt.redirectURL = url.href; + return out; +}; + +FilterContainer.prototype.hasQuery = function(fctxt) { + urlTokenizer.setURL(fctxt.url); + return urlTokenizer.hasQuery(); +}; + +/******************************************************************************/ + FilterContainer.prototype.toLogData = function() { if ( this.$filterUnit === 0 ) { return; } const logData = toLogDataInternal( @@ -3471,7 +3605,7 @@ FilterContainer.prototype.toLogData = function() { logData.tokenHash = this.$tokenHash; logData.result = this.$filterUnit === 0 ? 0 - : ((this.$catBits & 1) !== 0 ? 2 : 1); + : ((this.$catBits & AllowAction) !== 0 ? 2 : 1); return logData; }; @@ -3505,8 +3639,9 @@ FilterContainer.prototype.benchmark = async function(action, target) { const requests = await µb.loadBenchmarkDataset(); if ( Array.isArray(requests) === false || requests.length === 0 ) { - console.info('No requests found to benchmark'); - return; + const text = 'No dataset found to benchmark'; + console.info(text); + return text; } const print = log.print; @@ -3560,8 +3695,11 @@ FilterContainer.prototype.benchmark = async function(action, target) { print(`\turl=${fctxt.url}`); print(`\tdocOrigin=${fctxt.getDocOrigin()}`); } - if ( fctxt.type === 'main_frame' || fctxt.type === 'sub_frame' ) { - this.matchAndFetchData(fctxt, 'csp'); + if ( r !== 1 && this.hasQuery(fctxt) ) { + this.filterQuery(fctxt, 'queryprune'); + } + if ( r !== 1 && fctxt.type === 'main_frame' || fctxt.type === 'sub_frame' ) { + this.matchAndFetchModifiers(fctxt, 'csp'); } } const t1 = self.performance.now(); @@ -3687,7 +3825,7 @@ FilterContainer.prototype.bucketHistogram = function() { "FilterAnchorHn" => 1453} "FilterOriginMiss" => 730} "FilterPatternGeneric" => 601} - "FilterDataHolder" => 404} + "FilterModifier" => 404} "FilterOriginMissSet" => 316} "FilterTrailingSeparator" => 235} "FilterAnchorRight" => 174} diff --git a/src/js/traffic.js b/src/js/traffic.js index b24d69971..4d2e53c4e 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -64,7 +64,7 @@ const onBeforeRequest = function(details) { // https://github.com/chrisaljoudi/uBlock/issues/1001 // This must be executed regardless of whether the request is // behind-the-scene - if ( details.type === 'main_frame' ) { + if ( fctxt.itype === fctxt.MAIN_FRAME ) { return onBeforeRootFrameRequest(fctxt); } @@ -94,33 +94,31 @@ const onBeforeRequest = function(details) { fctxt.setRealm('network').toLogger(); } - // Not blocked - if ( result !== 1 ) { - if ( - details.parentFrameId !== -1 && - details.type === 'sub_frame' && - details.aliasURL === undefined - ) { - pageStore.setFrameURL(details.frameId, details.url); - } - if ( result === 2 ) { - return { cancel: false }; - } - return; + // Redirected + + if ( fctxt.redirectURL !== undefined ) { + return { redirectUrl: fctxt.redirectURL }; } - // Blocked + // Not redirected - if ( fctxt.redirectURL === undefined ) { + // Blocked + if ( result === 1 ) { return { cancel: true }; } - if ( µb.logger.enabled ) { - fctxt.setRealm('redirect') - .setFilter({ source: 'redirect', raw: µb.redirectEngine.resourceNameRegister }) - .toLogger(); + // Not blocked + if ( + fctxt.itype === fctxt.SUB_FRAME && + details.parentFrameId !== -1 && + details.aliasURL === undefined + ) { + pageStore.setFrameURL(details.frameId, details.url); + } + + if ( result === 2 ) { + return { cancel: false }; } - return { redirectUrl: fctxt.redirectURL }; }; /******************************************************************************/ @@ -134,14 +132,14 @@ const onBeforeRootFrameRequest = function(fctxt) { // This must be executed regardless of whether the request is // behind-the-scene const requestHostname = fctxt.getHostname(); - const logEnabled = µb.logger.enabled; - let result = 0, - logData; + const loggerEnabled = µb.logger.enabled; + let result = 0; + let logData; // If the site is whitelisted, disregard strict blocking if ( µb.getNetFilteringSwitch(requestURL) === false ) { result = 2; - if ( logEnabled ) { + if ( loggerEnabled ) { logData = { engine: 'u', result: 2, raw: 'whitelisted' }; } } @@ -152,16 +150,24 @@ const onBeforeRootFrameRequest = function(fctxt) { µb.sessionSwitches.evaluateZ('no-strict-blocking', requestHostname) ) { result = 2; - if ( logEnabled ) { - logData = { engine: 'u', result: 2, raw: 'no-strict-blocking: ' + µb.sessionSwitches.z + ' true' }; + if ( loggerEnabled ) { + logData = { + engine: 'u', + result: 2, + raw: `no-strict-blocking: ${µb.sessionSwitches.z} true` + }; } } // Temporarily whitelisted? if ( result === 0 && strictBlockBypasser.isBypassed(requestHostname) ) { result = 2; - if ( logEnabled ) { - logData = { engine: 'u', result: 2, raw: 'no-strict-blocking: true (temporary)' }; + if ( loggerEnabled ) { + logData = { + engine: 'u', + result: 2, + raw: 'no-strict-blocking: true (temporary)' + }; } } @@ -170,9 +176,8 @@ const onBeforeRootFrameRequest = function(fctxt) { // Check for specific block if ( result === 0 ) { - fctxt.type = 'main_frame'; result = snfe.matchString(fctxt, 0b0001); - if ( result !== 0 || logEnabled ) { + if ( result !== 0 || loggerEnabled ) { logData = snfe.toLogData(); } } @@ -181,14 +186,14 @@ const onBeforeRootFrameRequest = function(fctxt) { if ( result === 0 ) { fctxt.type = 'no_type'; result = snfe.matchString(fctxt, 0b0001); - if ( result !== 0 || logEnabled ) { + if ( result !== 0 || loggerEnabled ) { logData = snfe.toLogData(); } // https://github.com/chrisaljoudi/uBlock/issues/1128 - // Do not block if the match begins after the hostname, except when - // the filter is specifically of type `other`. + // Do not block if the match begins after the hostname, except when + // the filter is specifically of type `other`. // https://github.com/gorhill/uBlock/issues/490 - // Removing this for the time being, will need a new, dedicated type. + // Removing this for the time being, will need a new, dedicated type. if ( result === 1 && toBlockDocResult(requestURL, requestHostname, logData) === false @@ -196,21 +201,43 @@ const onBeforeRootFrameRequest = function(fctxt) { result = 0; logData = undefined; } + fctxt.type = 'main_frame'; } - // Log - fctxt.type = 'main_frame'; const pageStore = µb.bindTabToPageStats(fctxt.tabId, 'beforeRequest'); if ( pageStore ) { pageStore.journalAddRootFrame('uncommitted', requestURL); pageStore.journalAddRequest(requestHostname, result); } - if ( logEnabled ) { - fctxt.setRealm('network').setFilter(logData).toLogger(); + // Log + if ( loggerEnabled ) { + fctxt.setRealm('network').setFilter(logData); + } + + // Modifier(s)? + // A modifier is an action which transform the original network request. + // https://github.com/uBlockOrigin/uBlock-issues/issues/760 + // Redirect non-blocked request? + if ( result === 0 && snfe.hasQuery(fctxt) ) { + const directives = snfe.filterQuery(fctxt); + if ( directives !== undefined && loggerEnabled ) { + fctxt.pushFilters(directives.map(a => a.logData())); + } + } + + if ( loggerEnabled ) { + fctxt.setRealm('network').toLogger(); + } + + // Redirected + + if ( fctxt.redirectURL !== undefined ) { + return { redirectUrl: fctxt.redirectURL }; } // Not blocked + if ( result !== 1 ) { return; } // No log data means no strict blocking (because we need to report why @@ -218,6 +245,7 @@ const onBeforeRootFrameRequest = function(fctxt) { if ( logData === undefined ) { return; } // Blocked + const query = encodeURIComponent(JSON.stringify({ url: requestURL, hn: requestHostname, @@ -307,7 +335,7 @@ const onBeforeBehindTheSceneRequest = function(fctxt) { fctxt.tabOrigin.endsWith('-scheme') === false && µb.URI.isNetworkURI(fctxt.tabOrigin) || µb.userSettings.advancedUserEnabled || - fctxt.type === 'csp_report' + fctxt.itype === fctxt.CSP_REPORT ) { result = pageStore.filterRequest(fctxt); @@ -440,9 +468,7 @@ const onHeadersReceived = function(details) { const µb = µBlock; const fctxt = µb.filteringContext.fromWebrequestDetails(details); - const requestType = fctxt.type; - const isRootDoc = requestType === 'main_frame'; - const isDoc = isRootDoc || requestType === 'sub_frame'; + const isRootDoc = fctxt.itype === fctxt.MAIN_FRAME; let pageStore = µb.pageStoreFromTabId(fctxt.tabId); if ( pageStore === null ) { @@ -451,11 +477,11 @@ const onHeadersReceived = function(details) { } if ( pageStore.getNetFilteringSwitch(fctxt) === false ) { return; } - if ( requestType === 'image' || requestType === 'media' ) { + if ( fctxt.itype === fctxt.IMAGE || fctxt.itype === fctxt.MEDIA ) { return foilLargeMediaElement(details, fctxt, pageStore); } - if ( isDoc === false ) { return; } + if ( isRootDoc === false && fctxt.itype !== fctxt.SUB_FRAME ) { return; } // Keep in mind response headers will be modified in-place if needed, so // `details.responseHeaders` will always point to the modified response @@ -801,6 +827,7 @@ const injectCSP = function(fctxt, pageStore, responseHeaders) { const µb = µBlock; const loggerEnabled = µb.logger.enabled; const cspSubsets = []; + const requestType = fctxt.type; // Start collecting policies >>>>>>>> @@ -849,11 +876,14 @@ const injectCSP = function(fctxt, pageStore, responseHeaders) { // Static filtering. + fctxt.type = requestType; const staticDirectives = - µb.staticNetFilteringEngine.matchAndFetchData(fctxt, 'csp'); - for ( const directive of staticDirectives ) { - if ( directive.result !== 1 ) { continue; } - cspSubsets.push(directive.getData('csp')); + µb.staticNetFilteringEngine.matchAndFetchModifiers(fctxt, 'csp'); + if ( staticDirectives !== undefined ) { + for ( const directive of staticDirectives ) { + if ( directive.result !== 1 ) { continue; } + cspSubsets.push(directive.modifier.value); + } } // URL filtering `allow` rules override static filtering. @@ -897,11 +927,10 @@ const injectCSP = function(fctxt, pageStore, responseHeaders) { // Static CSP policies will be applied. - if ( loggerEnabled && staticDirectives.length !== 0 ) { - fctxt.setRealm('network').setType('csp'); - for ( const directive of staticDirectives ) { - fctxt.setFilter(directive.logData()).toLogger(); - } + if ( loggerEnabled && staticDirectives !== undefined ) { + fctxt.setRealm('network') + .pushFilters(staticDirectives.map(a => a.logData())) + .toLogger(); } if ( cspSubsets.length === 0 ) { return; } diff --git a/src/js/utils.js b/src/js/utils.js index 83bc19e03..6ea1b85b6 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -61,6 +61,7 @@ this._urlIn = ''; this._urlOut = ''; this._tokenized = false; + this._hasQuery = 0; // https://www.reddit.com/r/uBlockOrigin/comments/dzw57l/ // Remember: 1 token needs two slots this._tokens = new Uint32Array(2064); @@ -74,6 +75,7 @@ if ( url !== this._urlIn ) { this._urlIn = url; this._urlOut = url.toLowerCase(); + this._hasQuery = 0; this._tokenized = false; } return this._urlOut; @@ -115,6 +117,14 @@ return this._tokens; } + hasQuery() { + if ( this._hasQuery === 0 ) { + const i = this._urlOut.indexOf('?'); + this._hasQuery = i !== -1 ? i + 1 : -1; + } + return this._hasQuery > 0; + } + tokenHashFromString(s) { const l = s.length; if ( l === 0 ) { return this.emptyTokenHash; } @@ -155,33 +165,47 @@ l = 2048; } encodeInto.haystackLen = l; - const knownTokens = this.knownTokens; - const vtc = this._validTokenChars; - const charCodes = encodeInto.haystack; - let i = 0, j = 0, n, ti, th; - for (;;) { + let j = 0; + let hasq = -1; + mainLoop: { + const knownTokens = this.knownTokens; + const vtc = this._validTokenChars; + const charCodes = encodeInto.haystack; + let i = 0, n = 0, ti = 0, th = 0; for (;;) { - if ( i === l ) { return j; } - th = vtc[(charCodes[i] = url.charCodeAt(i))]; - i += 1; - if ( th !== 0 ) { break; } - } - ti = i - 1; n = 1; - for (;;) { - if ( i === l ) { break; } - const v = vtc[(charCodes[i] = url.charCodeAt(i))]; - i += 1; - if ( v === 0 ) { break; } - if ( n === 7 ) { continue; } - th = th << 4 ^ v; - n += 1; - } - if ( knownTokens[th & 0xFFFF ^ th >>> 16] !== 0 ) { - tokens[j+0] = th; - tokens[j+1] = ti; - j += 2; + for (;;) { + if ( i === l ) { break mainLoop; } + const cc = url.charCodeAt(i); + charCodes[i] = cc; + i += 1; + th = vtc[cc]; + if ( th !== 0 ) { break; } + if ( cc === 0x3F /* '?' */ ) { hasq = i; } + } + ti = i - 1; n = 1; + for (;;) { + if ( i === l ) { break; } + const cc = url.charCodeAt(i); + charCodes[i] = cc; + i += 1; + const v = vtc[cc]; + if ( v === 0 ) { + if ( cc === 0x3F /* '?' */ ) { hasq = i; } + break; + } + if ( n === 7 ) { continue; } + th = th << 4 ^ v; + n += 1; + } + if ( knownTokens[th & 0xFFFF ^ th >>> 16] !== 0 ) { + tokens[j+0] = th; + tokens[j+1] = ti; + j += 2; + } } } + this._hasQuery = hasq; + return j; } })(); diff --git a/src/logger-ui.html b/src/logger-ui.html index c21ca5dcf..65b25b235 100644 --- a/src/logger-ui.html +++ b/src/logger-ui.html @@ -58,11 +58,11 @@ angle-up
-
+
modified
css/fontimagemediascript
-
xhrframedomother
+
xhrframedomother