From 099b9852cd1630c7536800a5f25b8b6be0636e7b Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Wed, 11 Sep 2024 09:56:44 -0400 Subject: [PATCH] Code review for `ipaddress=` filter option If an IP address can be extracted from the hostname portion of a URL, the IP address matching will be performed at onBeforeRequest() time. Regardless, IP address matching will subsequently always be performed at onHeadersReceived() time as the request details at that point contain a reliable IP address value on supported platforms (Firefox- only as of now). The `cap_ipaddress` now evaluates to `true` in Chromium-based browsers. Even though these browsers are unable to provide reliable IP address value at onHeadersReceived() time, they can still perform IP address matching for IP address extracted from hostname portion of a URL. --- platform/common/vapi-common.js | 4 +- src/js/background.js | 2 +- src/js/filtering-context.js | 29 +++++++++++- src/js/static-filtering-parser.js | 1 + src/js/static-net-filtering.js | 75 ++++++++++++++++--------------- 5 files changed, 71 insertions(+), 40 deletions(-) diff --git a/platform/common/vapi-common.js b/platform/common/vapi-common.js index 367def90a..1cf98242f 100644 --- a/platform/common/vapi-common.js +++ b/platform/common/vapi-common.js @@ -163,6 +163,7 @@ vAPI.webextFlavor = { // This is always true. soup.add('ublock').add('webext'); + soup.add('ipaddress'); // Whether this is a dev build. if ( /^\d+\.\d+\.\d+\D/.test(browser.runtime.getManifest().version) ) { @@ -181,8 +182,7 @@ vAPI.webextFlavor = { if ( browser.runtime.getURL('').startsWith('moz-extension://') ) { soup.add('firefox') .add('user_stylesheet') - .add('html_filtering') - .add('ipaddress'); + .add('html_filtering'); const match = /Firefox\/(\d+)/.exec(ua); flavor.major = match && parseInt(match[1], 10) || 115; } else { diff --git a/src/js/background.js b/src/js/background.js index a59671091..a68487f6c 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -305,8 +305,8 @@ const µBlock = { // jshint ignore:line this.realm = ''; this.setMethod(details.method); this.setURL(details.url); + this.setIPAddress(details.ip); this.aliasURL = details.aliasURL || undefined; - this.ipaddress = details.ip || undefined; this.redirectURL = undefined; this.filter = undefined; if ( this.itype !== this.SUB_FRAME ) { diff --git a/src/js/filtering-context.js b/src/js/filtering-context.js index b1d7d7033..0bbd7decf 100644 --- a/src/js/filtering-context.js +++ b/src/js/filtering-context.js @@ -122,6 +122,8 @@ const methodBitToStrMap = new Map([ [ METHOD_PUT, 'put' ], ]); +const reIPv4 = /^\d+\.\d+\.\d+\.\d+$/; + /******************************************************************************/ export const FilteringContext = class { @@ -136,9 +138,9 @@ export const FilteringContext = class { this.stype = undefined; this.url = undefined; this.aliasURL = undefined; - this.ipaddress = undefined; this.hostname = undefined; this.domain = undefined; + this.ipaddress = undefined; this.docId = -1; this.frameId = -1; this.docOrigin = undefined; @@ -176,6 +178,7 @@ export const FilteringContext = class { this.url = other.url; this.hostname = other.hostname; this.domain = other.domain; + this.ipaddress = other.ipaddress; this.docId = other.docId; this.frameId = other.frameId; this.docOrigin = other.docOrigin; @@ -213,7 +216,7 @@ export const FilteringContext = class { setURL(a) { if ( a !== this.url ) { - this.hostname = this.domain = undefined; + this.hostname = this.domain = this.ipaddress = undefined; this.url = a; } return this; @@ -246,6 +249,28 @@ export const FilteringContext = class { return this; } + getIPAddress() { + if ( this.ipaddress !== undefined ) { + return this.ipaddress; + } + const ipaddr = this.getHostname(); + const c0 = ipaddr.charCodeAt(0); + if ( c0 === 0x5B /* [ */ ) { + return (this.ipaddress = ipaddr.slice(1, -1)); + } else if ( c0 >= 0x30 && c0 <= 0x39 ) { + if ( reIPv4.test(ipaddr) ) { + return (this.ipaddress = ipaddr); + } + } + return (this.ipaddress = ''); + } + + // Must always be called *after* setURL() + setIPAddress(ipaddr) { + this.ipaddress = ipaddr || undefined; + return this; + } + getDocOrigin() { if ( this.docOrigin === undefined ) { this.docOrigin = this.tabOrigin; diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index d3520b6e5..3d8e82fd6 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -578,6 +578,7 @@ export const preparserIfTokens = new Set([ 'env_mv3', 'env_safari', 'cap_html_filtering', + 'cap_ipaddress', 'cap_user_stylesheet', 'false', 'ext_abp', diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 052321777..780015209 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -2973,6 +2973,9 @@ registerFilterClass(FilterOnHeaders); /******************************************************************************/ class FilterIPAddress { + static reIPv6IPv4lan = /^::ffff:(7f\w{2}|a\w{2}|a9fe|c0a8):\w+$/; + static reIPv6local = /^f[cd]\w{2}:/; + static match(idata) { const ipaddr = $requestAddress; const details = filterRefs[filterData[idata+1]]; @@ -3011,26 +3014,26 @@ class FilterIPAddress { } return ipaddr.startsWith('192.168.'); } - if ( c0 !== 0x5B /* [ */ ) { return false; } // ipv6 - const c1 = ipaddr.charCodeAt(1); - if ( c1 === 0x3A /* : */ ) { - if ( ipaddr.startsWith('[::') === false ) { return false; } - if ( ipaddr === '[::]' || ipaddr === '[::1]' ) { return true; } - if ( ipaddr.startsWith('[::ffff:') === false ) { return false; } - return /^\[::ffff:(7f\w{2}|a\w{2}|a9fe|c0a8):\w+\]$/.test(ipaddr); + if ( c0 === 0x3A /* : */ ) { + if ( ipaddr.startsWith('::') === false ) { return false; } + if ( ipaddr === '::' || ipaddr === '::1' ) { return true; } + if ( ipaddr.startsWith('::ffff:') === false ) { return false; } + return this.reIPv6IPv4lan.test(ipaddr); } - if ( c1 === 0x36 /* 6 */ ) { - return ipaddr.startsWith('[64:ff9b:'); - } - if ( c1 === 0x66 /* f */ ) { - return /^\[f[cd]\w{2}:/.test(ipaddr); + if ( ipaddr.includes(':') ) { + if ( c0 === 0x36 /* 6 */ ) { + return ipaddr.startsWith('64:ff9b:'); + } + if ( c0 === 0x66 /* f */ ) { + return this.reIPv6local.test(ipaddr); + } } return false; } static isLoopback(ipaddr) { - return ipaddr === '127.0.0.1' || ipaddr === '[::1]'; + return ipaddr === '127.0.0.1' || ipaddr === '::1'; } static compile(details) { @@ -3412,7 +3415,6 @@ class FilterCompiler { this.notTypeBits = 0; this.methodBits = 0; this.notMethodBits = 0; - this.responseHeadersRealm = false; return this; } @@ -3532,13 +3534,11 @@ class FilterCompiler { case sfp.NODE_TYPE_NET_OPTION_NAME_HEADER: { this.optionValues.set('header', parser.getNetOptionValue(id) || ''); this.optionUnitBits |= HEADER_BIT; - this.responseHeadersRealm = true; break; } case sfp.NODE_TYPE_NET_OPTION_NAME_IPADDRESS: this.optionValues.set('ipaddress', parser.getNetOptionValue(id) || ''); this.optionUnitBits |= IPADDRESS_BIT; - this.responseHeadersRealm = true; break; case sfp.NODE_TYPE_NET_OPTION_NAME_METHOD: this.processMethodOption(parser.getNetOptionValue(id)); @@ -4008,7 +4008,7 @@ class FilterCompiler { } // Origin - if ( this.optionValues.has('from') ) { + if ( (this.optionUnitBits & FROM_BIT) !== 0 ) { compileFromDomainOpt( this.optionValues.get('fromList'), units.length !== 0 && patternClass.isSlow === true, @@ -4017,7 +4017,7 @@ class FilterCompiler { } // Destination - if ( this.optionValues.has('to') ) { + if ( (this.optionUnitBits & TO_BIT) !== 0 ) { compileToDomainOpt( this.optionValues.get('toList'), units.length !== 0 && patternClass.isSlow === true, @@ -4026,18 +4026,18 @@ class FilterCompiler { } // Deny-allow - if ( this.optionValues.has('denyallow') ) { + if ( (this.optionUnitBits & DENYALLOW_BIT) !== 0 ) { units.push(FilterDenyAllow.compile(this)); } + // IP address + if ( (this.optionUnitBits & IPADDRESS_BIT) !== 0 ) { + units.push(FilterIPAddress.compile(this)); + } + // Header - if ( this.responseHeadersRealm ) { - if ( this.optionValues.has('ipaddress') ) { - units.push(FilterIPAddress.compile(this)); - } - if ( this.optionValues.has('header') ) { - units.push(FilterOnHeaders.compile(this)); - } + if ( (this.optionUnitBits & HEADER_BIT) !== 0 ) { + units.push(FilterOnHeaders.compile(this)); this.action |= HEADERS_REALM; } @@ -4058,12 +4058,17 @@ class FilterCompiler { modifierBitsFromType.get(this.modifyType); } - this.compileToAtomicFilter( - units.length === 1 - ? units[0] - : FilterCompositeAll.compile(units), - writer - ); + const fdata = units.length === 1 + ? units[0] + : FilterCompositeAll.compile(units); + + this.compileToAtomicFilter(fdata, writer); + + if ( (this.optionUnitBits & IPADDRESS_BIT) !== 0 ) { + if ( (this.action & HEADERS_REALM) !== 0 ) { return; } + this.action |= HEADERS_REALM; + this.compileToAtomicFilter(fdata, writer); + } } compilePattern(units) { @@ -4856,7 +4861,7 @@ StaticNetFilteringEngine.prototype.matchAndFetchModifiers = function( $requestHostname = fctxt.getHostname(); $requestMethodBit = fctxt.method || 0; $requestTypeValue = (typeBits & TypeBitsMask) >>> TypeBitsOffset; - $requestAddress = fctxt.ipaddress || ''; + $requestAddress = fctxt.getIPAddress(); const modifierType = modifierTypeFromName.get(modifierName); const modifierBits = modifierBitsFromType.get(modifierType); @@ -5223,7 +5228,7 @@ StaticNetFilteringEngine.prototype.matchRequest = function(fctxt, modifiers = 0) $requestHostname = fctxt.getHostname(); $requestMethodBit = fctxt.method || 0; $requestTypeValue = (typeBits & TypeBitsMask) >>> TypeBitsOffset; - $requestAddress = fctxt.ipaddress || ''; + $requestAddress = fctxt.getIPAddress(); $isBlockImportant = false; // Evaluate block realm before allow realm, and allow realm before @@ -5259,7 +5264,7 @@ StaticNetFilteringEngine.prototype.matchHeaders = function(fctxt, headers) { $requestHostname = fctxt.getHostname(); $requestMethodBit = fctxt.method || 0; $requestTypeValue = (typeBits & TypeBitsMask) >>> TypeBitsOffset; - $requestAddress = fctxt.ipaddress || ''; + $requestAddress = fctxt.getIPAddress(); $httpHeaders.init(headers); let r = 0;