diff options
Diffstat (limited to 'data/extensions/uBlock0@raymondhill.net/js/dynamic-net-filtering.js')
-rw-r--r-- | data/extensions/uBlock0@raymondhill.net/js/dynamic-net-filtering.js | 482 |
1 files changed, 482 insertions, 0 deletions
diff --git a/data/extensions/uBlock0@raymondhill.net/js/dynamic-net-filtering.js b/data/extensions/uBlock0@raymondhill.net/js/dynamic-net-filtering.js new file mode 100644 index 0000000..f921d93 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/dynamic-net-filtering.js @@ -0,0 +1,482 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2014-2018 Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +import { + decomposeHostname, + domainFromHostname, +} from './uri-utils.js'; +import { LineIterator } from './text-utils.js'; +import punycode from '../lib/punycode.js'; + +/******************************************************************************/ + +// Object.create(null) is used below to eliminate worries about unexpected +// property names in prototype chain -- and this way we don't have to use +// Object.hasOwn() to avoid this. + +const supportedDynamicTypes = Object.create(null); +Object.assign(supportedDynamicTypes, { + '3p': true, + 'image': true, +'inline-script': true, + '1p-script': true, + '3p-script': true, + '3p-frame': true +}); + +const typeBitOffsets = Object.create(null); +Object.assign(typeBitOffsets, { + '*': 0, +'inline-script': 2, + '1p-script': 4, + '3p-script': 6, + '3p-frame': 8, + 'image': 10, + '3p': 12 +}); + +const nameToActionMap = Object.create(null); +Object.assign(nameToActionMap, { + 'block': 1, + 'allow': 2, + 'noop': 3 +}); + +const intToActionMap = new Map([ + [ 1, 'block' ], + [ 2, 'allow' ], + [ 3, 'noop' ] +]); + +// For performance purpose, as simple tests as possible +const reBadHostname = /[^0-9a-z_.[\]:%-]/; +const reNotASCII = /[^\x20-\x7F]/; +const decomposedSource = []; +const decomposedDestination = []; + +/******************************************************************************/ + +function is3rdParty(srcHostname, desHostname) { + // If at least one is party-less, the relation can't be labelled + // "3rd-party" + if ( desHostname === '*' || srcHostname === '*' || srcHostname === '' ) { + return false; + } + + // No domain can very well occurs, for examples: + // - localhost + // - file-scheme + // etc. + const srcDomain = domainFromHostname(srcHostname) || srcHostname; + + if ( desHostname.endsWith(srcDomain) === false ) { + return true; + } + // Do not confuse 'example.com' with 'anotherexample.com' + return desHostname.length !== srcDomain.length && + desHostname.charAt(desHostname.length - srcDomain.length - 1) !== '.'; +} + +/******************************************************************************/ + +class DynamicHostRuleFiltering { + + constructor() { + this.reset(); + } + + reset() { + this.r = 0; + this.type = ''; + this.y = ''; + this.z = ''; + this.rules = new Map(); + this.changed = false; + } + + assign(other) { + // Remove rules not in other + for ( const k of this.rules.keys() ) { + if ( other.rules.has(k) === false ) { + this.rules.delete(k); + this.changed = true; + } + } + // Add/change rules in other + for ( const entry of other.rules ) { + if ( this.rules.get(entry[0]) !== entry[1] ) { + this.rules.set(entry[0], entry[1]); + this.changed = true; + } + } + } + + copyRules(from, srcHostname, desHostnames) { + // Specific types + let thisBits = this.rules.get('* *'); + let fromBits = from.rules.get('* *'); + if ( fromBits !== thisBits ) { + if ( fromBits !== undefined ) { + this.rules.set('* *', fromBits); + } else { + this.rules.delete('* *'); + } + this.changed = true; + } + + let key = `${srcHostname} *`; + thisBits = this.rules.get(key); + fromBits = from.rules.get(key); + if ( fromBits !== thisBits ) { + if ( fromBits !== undefined ) { + this.rules.set(key, fromBits); + } else { + this.rules.delete(key); + } + this.changed = true; + } + + // Specific destinations + for ( const desHostname in desHostnames ) { + key = `* ${desHostname}`; + thisBits = this.rules.get(key); + fromBits = from.rules.get(key); + if ( fromBits !== thisBits ) { + if ( fromBits !== undefined ) { + this.rules.set(key, fromBits); + } else { + this.rules.delete(key); + } + this.changed = true; + } + key = `${srcHostname} ${desHostname}` ; + thisBits = this.rules.get(key); + fromBits = from.rules.get(key); + if ( fromBits !== thisBits ) { + if ( fromBits !== undefined ) { + this.rules.set(key, fromBits); + } else { + this.rules.delete(key); + } + this.changed = true; + } + } + + return this.changed; + } + + // - * * type + // - from * type + // - * to * + // - from to * + + hasSameRules(other, srcHostname, desHostnames) { + // Specific types + let key = '* *'; + if ( this.rules.get(key) !== other.rules.get(key) ) { return false; } + key = `${srcHostname} *`; + if ( this.rules.get(key) !== other.rules.get(key) ) { return false; } + // Specific destinations + for ( const desHostname in desHostnames ) { + key = `* ${desHostname}`; + if ( this.rules.get(key) !== other.rules.get(key) ) { + return false; + } + key = `${srcHostname} ${desHostname}`; + if ( this.rules.get(key) !== other.rules.get(key) ) { + return false; + } + } + return true; + } + + setCell(srcHostname, desHostname, type, state) { + const bitOffset = typeBitOffsets[type]; + const k = `${srcHostname} ${desHostname}`; + const oldBitmap = this.rules.get(k) || 0; + const newBitmap = oldBitmap & ~(3 << bitOffset) | (state << bitOffset); + if ( newBitmap === oldBitmap ) { return false; } + if ( newBitmap === 0 ) { + this.rules.delete(k); + } else { + this.rules.set(k, newBitmap); + } + this.changed = true; + return true; + } + + unsetCell(srcHostname, desHostname, type) { + this.evaluateCellZY(srcHostname, desHostname, type); + if ( this.r === 0 ) { return false; } + this.setCell(srcHostname, desHostname, type, 0); + this.changed = true; + return true; + } + + evaluateCell(srcHostname, desHostname, type) { + const key = `${srcHostname} ${desHostname}`; + const bitmap = this.rules.get(key); + if ( bitmap === undefined ) { return 0; } + return bitmap >> typeBitOffsets[type] & 3; + } + + clearRegisters() { + this.r = 0; + this.type = this.y = this.z = ''; + return this; + } + + evaluateCellZ(srcHostname, desHostname, type) { + decomposeHostname(srcHostname, decomposedSource); + this.type = type; + const bitOffset = typeBitOffsets[type]; + for ( const srchn of decomposedSource ) { + this.z = srchn; + let v = this.rules.get(`${srchn} ${desHostname}`); + if ( v === undefined ) { continue; } + v = v >>> bitOffset & 3; + if ( v === 0 ) { continue; } + return (this.r = v); + } + // srcHostname is '*' at this point + this.r = 0; + return 0; + } + + evaluateCellZY(srcHostname, desHostname, type) { + // Pathological cases. + if ( desHostname === '' ) { + this.r = 0; + return 0; + } + + // Precedence: from most specific to least specific + + // Specific-destination, any party, any type + decomposeHostname(desHostname, decomposedDestination); + for ( const deshn of decomposedDestination ) { + if ( deshn === '*' ) { break; } + this.y = deshn; + if ( this.evaluateCellZ(srcHostname, deshn, '*') !== 0 ) { + return this.r; + } + } + + const thirdParty = is3rdParty(srcHostname, desHostname); + + // Any destination + this.y = '*'; + + // Specific party + if ( thirdParty ) { + // 3rd-party, specific type + if ( type === 'script' ) { + if ( this.evaluateCellZ(srcHostname, '*', '3p-script') !== 0 ) { + return this.r; + } + } else if ( type === 'sub_frame' || type === 'object' ) { + if ( this.evaluateCellZ(srcHostname, '*', '3p-frame') !== 0 ) { + return this.r; + } + } + // 3rd-party, any type + if ( this.evaluateCellZ(srcHostname, '*', '3p') !== 0 ) { + return this.r; + } + } else if ( type === 'script' ) { + // 1st party, specific type + if ( this.evaluateCellZ(srcHostname, '*', '1p-script') !== 0 ) { + return this.r; + } + } + + // Any destination, any party, specific type + if ( supportedDynamicTypes[type] !== undefined ) { + if ( this.evaluateCellZ(srcHostname, '*', type) !== 0 ) { + return this.r; + } + if ( type.startsWith('3p-') ) { + if ( this.evaluateCellZ(srcHostname, '*', '3p') !== 0 ) { + return this.r; + } + } + } + + // Any destination, any party, any type + if ( this.evaluateCellZ(srcHostname, '*', '*') !== 0 ) { + return this.r; + } + + this.type = ''; + return 0; + } + + mustAllowCellZY(srcHostname, desHostname, type) { + return this.evaluateCellZY(srcHostname, desHostname, type) === 2; + } + + mustBlockOrAllow() { + return this.r === 1 || this.r === 2; + } + + mustBlock() { + return this.r === 1; + } + + mustAbort() { + return this.r === 3; + } + + lookupRuleData(src, des, type) { + const r = this.evaluateCellZY(src, des, type); + if ( r === 0 ) { return; } + return `${this.z} ${this.y} ${this.type} ${r}`; + } + + toLogData() { + if ( this.r === 0 || this.type === '' ) { return; } + return { + source: 'dynamicHost', + result: this.r, + raw: `${this.z} ${this.y} ${this.type} ${intToActionMap.get(this.r)}` + }; + } + + srcHostnameFromRule(rule) { + return rule.slice(0, rule.indexOf(' ')); + } + + desHostnameFromRule(rule) { + return rule.slice(rule.indexOf(' ') + 1); + } + + toArray() { + const out = []; + for ( const key of this.rules.keys() ) { + const srchn = this.srcHostnameFromRule(key); + const deshn = this.desHostnameFromRule(key); + const srchnPretty = srchn.includes('xn--') && punycode + ? punycode.toUnicode(srchn) + : srchn; + const deshnPretty = deshn.includes('xn--') && punycode + ? punycode.toUnicode(deshn) + : deshn; + for ( const type in typeBitOffsets ) { + if ( typeBitOffsets[type] === undefined ) { continue; } + const val = this.evaluateCell(srchn, deshn, type); + if ( val === 0 ) { continue; } + const action = intToActionMap.get(val); + if ( action === undefined ) { continue; } + out.push(`${srchnPretty} ${deshnPretty} ${type} ${action}`); + } + } + return out; + } + + toString() { + return this.toArray().join('\n'); + } + + fromString(text, append) { + const lineIter = new LineIterator(text); + if ( append !== true ) { this.reset(); } + while ( lineIter.eot() === false ) { + this.addFromRuleParts(lineIter.next().trim().split(/\s+/)); + } + } + + validateRuleParts(parts) { + if ( parts.length < 4 ) { return; } + + // Ignore hostname-based switch rules + if ( parts[0].endsWith(':') ) { return; } + + // Ignore URL-based rules + if ( parts[1].includes('/') ) { return; } + + if ( typeBitOffsets[parts[2]] === undefined ) { return; } + + if ( nameToActionMap[parts[3]] === undefined ) { return; } + + // https://github.com/chrisaljoudi/uBlock/issues/840 + // Discard invalid rules + if ( parts[1] !== '*' && parts[2] !== '*' ) { return; } + + // Performance: avoid punycoding when only ASCII chars + if ( punycode !== undefined ) { + if ( reNotASCII.test(parts[0]) ) { + parts[0] = punycode.toASCII(parts[0]); + } + if ( reNotASCII.test(parts[1]) ) { + parts[1] = punycode.toASCII(parts[1]); + } + } + + // https://github.com/chrisaljoudi/uBlock/issues/1082 + // Discard rules with invalid hostnames + if ( + (parts[0] !== '*' && reBadHostname.test(parts[0])) || + (parts[1] !== '*' && reBadHostname.test(parts[1])) + ) { + return; + } + + return parts; + } + + addFromRuleParts(parts) { + if ( this.validateRuleParts(parts) !== undefined ) { + this.setCell(parts[0], parts[1], parts[2], nameToActionMap[parts[3]]); + return true; + } + return false; + } + + removeFromRuleParts(parts) { + if ( this.validateRuleParts(parts) !== undefined ) { + this.setCell(parts[0], parts[1], parts[2], 0); + return true; + } + return false; + } + + toSelfie() { + return { + magicId: this.magicId, + rules: Array.from(this.rules) + }; + } + + fromSelfie(selfie) { + if ( selfie.magicId !== this.magicId ) { return false; } + this.rules = new Map(selfie.rules); + this.changed = true; + return true; + } +} + +DynamicHostRuleFiltering.prototype.magicId = 1; + +/******************************************************************************/ + +export default DynamicHostRuleFiltering; + +/******************************************************************************/ |