diff --git a/v2/firefox/common.js b/v2/firefox/common.js index 53ebc89..d7655e3 100644 --- a/v2/firefox/common.js +++ b/v2/firefox/common.js @@ -1,4 +1,4 @@ -/* globals UAParser */ +/* global UAParser */ 'use strict'; @@ -544,15 +544,15 @@ const onBeforeSendHeaders = d => { if (platform.toLowerCase().includes('mac')) { platform = 'macOS'; } + else if (platform.toLowerCase().includes('debian')) { + platform = 'Linux'; + } const version = o.userAgentDataBuilder.p?.browser?.major || 107; let name = o.userAgentDataBuilder.p?.browser?.name || 'Google Chrome'; if (name === 'Chrome') { name = 'Google Chrome'; } - else if (name.includes('debian')) { - platform = 'Linux'; - } requestHeaders.push({ name: 'sec-ch-ua-platform', diff --git a/v2/firefox/manifest.json b/v2/firefox/manifest.json index 5073fc0..7f133ca 100755 --- a/v2/firefox/manifest.json +++ b/v2/firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "__MSG_extensionName__", - "version": "0.4.8", + "version": "0.4.9", "default_locale": "en", "description": "__MSG_extensionDescription__", "permissions": [ diff --git a/v3/data/inject/isolated.js b/v3/data/inject/isolated.js index 6a69c4f..2607aba 100644 --- a/v3/data/inject/isolated.js +++ b/v3/data/inject/isolated.js @@ -15,3 +15,5 @@ self.prefs = self.prefs || { platformVersion: '10.0.0' }; Object.assign(port.dataset, self.prefs); + +port.dataset.enabled = self.ingored ? false : true; diff --git a/v3/data/inject/main.js b/v3/data/inject/main.js index b4eef84..93d1fde 100644 --- a/v3/data/inject/main.js +++ b/v3/data/inject/main.js @@ -1,6 +1,5 @@ const port = document.getElementById('ua-port-fgTd9n'); port.remove(); -console.log(12, port); // overwrite navigator.userAgent { @@ -8,8 +7,8 @@ console.log(12, port); Object.defineProperty(Navigator.prototype, 'userAgent', { get: new Proxy(get, { - apply() { - return port.dataset.ua; + apply(target, self, args) { + return port.dataset.enabled === 'true' ? port.dataset.ua : Reflect.apply(target, self, args); } }) }); @@ -31,8 +30,8 @@ if (port.dataset.uad) { Object.defineProperty(self.NavigatorUAData.prototype, 'brands', { get: new Proxy(Object.getOwnPropertyDescriptor(self.NavigatorUAData.prototype, 'brands').get, { - apply() { - return [{ + apply(target, self, args) { + return port.dataset.enabled === 'true' ? [{ brand: port.dataset.name, version: port.dataset.major }, { @@ -41,62 +40,65 @@ if (port.dataset.uad) { }, { brand: 'Not=A?Brand', version: '24' - }]; + }] : Reflect.apply(target, self, args); } }) }); Object.defineProperty(self.NavigatorUAData.prototype, 'mobile', { get: new Proxy(Object.getOwnPropertyDescriptor(self.NavigatorUAData.prototype, 'mobile').get, { - apply() { - return port.dataset.mobile === 'true'; + apply(target, self, args) { + return port.dataset.enabled === 'true' ? port.dataset.mobile === 'true' : Reflect.apply(target, self, args); } }) }); Object.defineProperty(self.NavigatorUAData.prototype, 'platform', { get: new Proxy(Object.getOwnPropertyDescriptor(self.NavigatorUAData.prototype, 'platform').get, { - apply() { - return port.dataset.platform; + apply(target, self, args) { + return port.dataset.enabled === 'true' ? port.dataset.platform : Reflect.apply(target, self, args); } }) }); self.NavigatorUAData.prototype.toJSON = new Proxy(self.NavigatorUAData.prototype.toJSON, { - apply(target, self) { - return { + apply(target, self, args) { + return port.dataset.enabled === 'true' ? { brands: self.brands, mobile: self.mobile, platform: self.platform - }; + } : Reflect.apply(target, self, args); } }); self.NavigatorUAData.prototype.getHighEntropyValues = new Proxy(self.NavigatorUAData.prototype.getHighEntropyValues, { apply(target, self, args) { - const hints = args[0]; + if (port.dataset.enabled === 'true') { + const hints = args[0]; - if (!hints || Array.isArray(hints) === false) { - return Promise.reject(Error(`Failed to execute 'getHighEntropyValues' on 'NavigatorUAData'`)); - } + if (!hints || Array.isArray(hints) === false) { + return Promise.reject(Error(`Failed to execute 'getHighEntropyValues' on 'NavigatorUAData'`)); + } - const r = self.toJSON(); + const r = self.toJSON(); - if (hints.includes('architecture')) { - r.architecture = port.dataset.architecture; + if (hints.includes('architecture')) { + r.architecture = port.dataset.architecture; + } + if (hints.includes('bitness')) { + r.bitness = port.dataset.bitness; + } + if (hints.includes('model')) { + r.model = ''; + } + if (hints.includes('platformVersion')) { + r.platformVersion = port.dataset.platformVersion; + } + if (hints.includes('uaFullVersion')) { + r.uaFullVersion = self.brands[0].version; + } + if (hints.includes('fullVersionList')) { + r.fullVersionList = this.brands; + } + return Promise.resolve(r); } - if (hints.includes('bitness')) { - r.bitness = port.dataset.bitness; - } - if (hints.includes('model')) { - r.model = ''; - } - if (hints.includes('platformVersion')) { - r.platformVersion = port.dataset.platformVersion; - } - if (hints.includes('uaFullVersion')) { - r.uaFullVersion = self.brands[0].version; - } - if (hints.includes('fullVersionList')) { - r.fullVersionList = this.brands; - } - return Promise.resolve(r); + return Reflect.apply(target, self, args); } }); } diff --git a/v3/helper/ReadMe.txt b/v3/helper/ReadMe.txt new file mode 100644 index 0000000..4937dd7 --- /dev/null +++ b/v3/helper/ReadMe.txt @@ -0,0 +1,2 @@ +ua-parser.min.js: + https://github.com/faisalman/ua-parser-js/releases/tag/1.0.32 diff --git a/v3/helper/ua-parser.min.js b/v3/helper/ua-parser.min.js new file mode 100644 index 0000000..8365377 --- /dev/null +++ b/v3/helper/ua-parser.min.js @@ -0,0 +1,4 @@ +/* UAParser.js v1.0.32 + Copyright © 2012-2021 Faisal Salman + MIT License */ +(function(window,undefined){"use strict";var LIBVERSION="1.0.32",EMPTY="",UNKNOWN="?",FUNC_TYPE="function",UNDEF_TYPE="undefined",OBJ_TYPE="object",STR_TYPE="string",MAJOR="major",MODEL="model",NAME="name",TYPE="type",VENDOR="vendor",VERSION="version",ARCHITECTURE="architecture",CONSOLE="console",MOBILE="mobile",TABLET="tablet",SMARTTV="smarttv",WEARABLE="wearable",EMBEDDED="embedded",UA_MAX_LENGTH=350;var AMAZON="Amazon",APPLE="Apple",ASUS="ASUS",BLACKBERRY="BlackBerry",BROWSER="Browser",CHROME="Chrome",EDGE="Edge",FIREFOX="Firefox",GOOGLE="Google",HUAWEI="Huawei",LG="LG",MICROSOFT="Microsoft",MOTOROLA="Motorola",OPERA="Opera",SAMSUNG="Samsung",SHARP="Sharp",SONY="Sony",XIAOMI="Xiaomi",ZEBRA="Zebra",FACEBOOK="Facebook";var extend=function(regexes,extensions){var mergedRegexes={};for(var i in regexes){if(extensions[i]&&extensions[i].length%2===0){mergedRegexes[i]=extensions[i].concat(regexes[i])}else{mergedRegexes[i]=regexes[i]}}return mergedRegexes},enumerize=function(arr){var enums={};for(var i=0;i0){if(q.length===2){if(typeof q[1]==FUNC_TYPE){this[q[0]]=q[1].call(this,match)}else{this[q[0]]=q[1]}}else if(q.length===3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){this[q[0]]=match?q[1].call(this,match,q[2]):undefined}else{this[q[0]]=match?match.replace(q[1],q[2]):undefined}}else if(q.length===4){this[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}}else{this[q]=match?match:undefined}}}}i+=2}},strMapper=function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;jUA_MAX_LENGTH?trim(ua,UA_MAX_LENGTH):ua;return this};this.setUA(_ua);return this};UAParser.VERSION=LIBVERSION;UAParser.BROWSER=enumerize([NAME,VERSION,MAJOR]);UAParser.CPU=enumerize([ARCHITECTURE]);UAParser.DEVICE=enumerize([MODEL,VENDOR,TYPE,CONSOLE,MOBILE,SMARTTV,TABLET,WEARABLE,EMBEDDED]);UAParser.ENGINE=UAParser.OS=enumerize([NAME,VERSION]);if(typeof exports!==UNDEF_TYPE){if(typeof module!==UNDEF_TYPE&&module.exports){exports=module.exports=UAParser}exports.UAParser=UAParser}else{if(typeof define===FUNC_TYPE&&define.amd){define(function(){return UAParser})}else if(typeof window!==UNDEF_TYPE){window.UAParser=UAParser}}var $=typeof window!==UNDEF_TYPE&&(window.jQuery||window.Zepto);if($&&!$.ua){var parser=new UAParser;$.ua=parser.getResult();$.ua.get=function(){return parser.getUA()};$.ua.set=function(ua){parser.setUA(ua);var result=parser.getResult();for(var prop in result){$.ua[prop]=result[prop]}}}})(typeof window==="object"?window:this); diff --git a/v3/manifest.json b/v3/manifest.json index d05f66f..bd162f2 100755 --- a/v3/manifest.json +++ b/v3/manifest.json @@ -10,6 +10,7 @@ "webNavigation", "webRequest", "declarativeNetRequest", + "declarativeNetRequestFeedback", "contextMenus" ], "host_permissions": [ diff --git a/v3/policy.js b/v3/policy.js new file mode 100644 index 0000000..24027f6 --- /dev/null +++ b/v3/policy.js @@ -0,0 +1,74 @@ +/* global UAParser */ +self.importScripts('./helper/ua-parser.min.js'); + +const PREFS = { + 'enabled': true, + 'mode': 'blacklist', + 'blacklist-exception-hosts': [], + 'whitelist-hosts': [], + 'custom-routing': { + 'whatismybrowser.com': 'ff' + } +}; + +const policy = {}; + +{ + const cache = new Map(); + + policy.parse = d => { + const ua = 'Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36'; + + if (cache.has(ua)) { + return cache.get(ua); + } + + return new Promise(resolve => chrome.storage.local.get({ + 'userAgentData': true + }, prefs => { + const p = (new UAParser(ua)).getResult(); + + const r = { + ua + }; + r.uad = prefs.userAgentData && + p.browser && p.browser.major && ['Opera', 'Chrome', 'Edge'].includes(p.browser.name); + + if (r.uad) { + r.platform = p?.os?.name || 'Windows'; + if (r.platform.toLowerCase().includes('mac')) { + r.platform = 'macOS'; + } + else if (r.platform.toLowerCase().includes('debian')) { + r.platform = 'Linux'; + } + + r.major = p?.browser?.major || 100; + + r.name = p?.browser?.name || 'Google Chrome'; + if (r.name === 'Chrome') { + r.name = 'Google Chrome'; + } + + r.mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua); + + r.architecture = 'x86'; + + r.bitness = '64'; + + r.platformVersion = '10.0.0'; + } + + cache.set(ua, r); + + resolve(r); + })); + }; +} + +policy.configure = (...methods) => new Promise(resolve => chrome.storage.local.get(PREFS, prefs => { + for (const method of methods) { + method(prefs); + } + resolve(); +})); diff --git a/v3/request.js b/v3/request.js new file mode 100644 index 0000000..570f640 --- /dev/null +++ b/v3/request.js @@ -0,0 +1,51 @@ +/* global policy */ + +const request = {}; + +request.network = async prefs => { + const p = await policy.parse(); + + const condition = { + 'isUrlFilterCaseSensitive': false, + 'resourceTypes': Object.values(chrome.declarativeNetRequest.ResourceType) + }; + const one = { + 'id': 1, + 'priority': 1, + 'action': { + 'type': 'modifyHeaders', + 'requestHeaders': [{ + 'operation': 'set', + 'header': 'user-agent', + 'value': p.ua + }] + }, + 'condition': { + ...condition + } + }; + const o = { + addRules: [one], + removeRuleIds: (await chrome.declarativeNetRequest.getDynamicRules()).map(o => o.id) + }; + + if (prefs.enabled) { + if (prefs.mode === 'blacklist') { + one.condition.excludedInitiatorDomains = prefs['blacklist-exception-hosts']; + } + else { + if (prefs['whitelist-hosts'].length) { + one.condition.initiatorDomains = prefs['whitelist-hosts']; + } + else { + console.info('matching list is empty'); + o.addRules.length = 0; + } + } + } + + chrome.declarativeNetRequest.updateDynamicRules(o); +}; + +chrome.declarativeNetRequest.onRuleMatchedDebug.addListener(d => console.log(d)); + diff --git a/v3/scripting.js b/v3/scripting.js new file mode 100644 index 0000000..dbac8ba --- /dev/null +++ b/v3/scripting.js @@ -0,0 +1,104 @@ +/* global port, policy */ + +const scripting = {}; + +scripting.page = async prefs => { + await chrome.scripting.unregisterContentScripts(); + + if (prefs.enabled) { + const common = { + 'allFrames': true, + 'matchOriginAsFallback': true, + 'runAt': 'document_start' + }; + if (prefs.mode === 'blacklist') { + common.matches = ['*://*/*']; + if (prefs['blacklist-exception-hosts'].length) { + common.excludeMatches = prefs['blacklist-exception-hosts'].map(h => [`*://${h}/*`, `*://*.${h}/*`]).flat(); + } + } + else if (prefs['whitelist-hosts'].length) { + common.matches = prefs['whitelist-hosts'].map(h => [`*://${h}/*`, `*://*.${h}/*`]).flat(); + } + + if (common.matches.length) { + await chrome.scripting.registerContentScripts([{ + ...common, + 'id': 'protected', + 'js': ['/data/inject/isolated.js'], + 'world': 'ISOLATED' + }, { + ...common, + 'id': 'unprotected', + 'js': ['/data/inject/main.js'], + 'world': 'MAIN' + }]); + } + else { + console.info('matching list is empty'); + } + } +}; + +// web navigation +{ + const onCommitted = async d => { + const p = await policy.parse(d); + + if (p) { + chrome.scripting.executeScript({ + target: { + tabId: d.tabId, + frameIds: [d.frameId] + }, + injectImmediately: true, + func: p => { + if (typeof port === 'undefined') { + self.prefs = p; + } + else { + Object.assign(port.dataset, p); + } + }, + args: [p] + }); + } + }; + const onCommittedIgnore = d => { + chrome.scripting.executeScript({ + target: { + tabId: d.tabId, + frameIds: [d.frameId] + }, + injectImmediately: true, + func: () => { + if (typeof port === 'undefined') { + port.dataset.enabled = false; + } + else { + self.ingored = true; + } + } + }).catch(() => {}); + }; + + scripting.commit = prefs => { + if (prefs.enabled && prefs.mode === 'blacklist') { + chrome.webNavigation.onCommitted.addListener(onCommitted); + if (prefs['blacklist-exception-hosts'].length) { + chrome.webNavigation.onCommitted.addListener(onCommittedIgnore, { + url: prefs['blacklist-exception-hosts'].map(hostContains => ({ + hostContains + })) + }); + } + } + else if (prefs['whitelist-hosts'].length) { + chrome.webNavigation.onCommitted.addListener(onCommitted, { + url: prefs['whitelist-hosts'].map(hostContains => ({ + hostContains + })) + }); + } + }; +} diff --git a/v3/worker.js b/v3/worker.js index 2a5aab5..8d40dd7 100644 --- a/v3/worker.js +++ b/v3/worker.js @@ -1,72 +1,22 @@ -const enable = () => chrome.storage.local.get({ - enabled: true -}, async prefs => { - await chrome.scripting.unregisterContentScripts(); +/* global policy, scripting, request */ - if (prefs.enabled) { - const common = { - 'matches': ['*://*/*'], - 'allFrames': true, - 'matchOriginAsFallback': true, - 'runAt': 'document_start' - }; +self.importScripts('./policy.js'); +self.importScripts('./scripting.js'); +self.importScripts('./request.js'); - await chrome.scripting.registerContentScripts([{ - ...common, - 'id': 'protected', - 'js': ['/data/inject/isolated.js'], - 'world': 'ISOLATED' - }, { - ...common, - 'id': 'unprotected', - 'js': ['/data/inject/main.js'], - 'world': 'MAIN' - }]); - } -}); -chrome.runtime.onStartup.addListener(enable); -chrome.runtime.onInstalled.addListener(enable); - -const policy = () => ({ - ua: 'Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36', - uad: true, - major: 100, - name: 'Google Chrome', - mobile: false, - platform: 'Windows', - architecture: 'x86', - bitness: '64', - platformVersion: '10.0.0' -}); - -// web navigation -const onCommitted = d => { - const p = policy(d); - - if (p) { - chrome.scripting.executeScript({ - target: { - tabId: d.tabId, - frameIds: [d.frameId] - }, - injectImmediately: true, - func: p => { - /* global port */ - if (typeof port === 'undefined') { - self.prefs = p; - } - else { - Object.assign(port.dataset, p); - } - }, - args: [p] - }); - } -}; -chrome.storage.local.get({ - enabled: true -}, prefs => { - if (prefs.enabled) { - chrome.webNavigation.onCommitted.addListener(onCommitted); +// run on each wake up +policy.configure(scripting.commit, request.network); + +// run once +{ + const once = () => policy.configure(scripting.page); + + chrome.runtime.onStartup.addListener(once); + chrome.runtime.onInstalled.addListener(once); +} + +chrome.storage.onChanged.addListener(ps => { + if (ps.enabled || ps.mode || ps['blacklist-exception-hosts'] || ps['whitelist-hosts']) { + policy.configure(scripting.commit, scripting.page, request.network); } });