From 4d25c834147f1df45fad19b58340a243f99c6a39 Mon Sep 17 00:00:00 2001 From: Paramtamtam <7326800+tarampampam@users.noreply.github.com> Date: Tue, 30 Apr 2024 02:23:08 +0400 Subject: [PATCH] =?UTF-8?q?wip:=20=F0=9F=94=95=20temporary=20commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entrypoints/content/content.ts | 2 - src/entrypoints/content/inject.ts | 213 +++++++++++++++++++++++++---- tsconfig.json | 2 +- 3 files changed, 190 insertions(+), 27 deletions(-) diff --git a/src/entrypoints/content/content.ts b/src/entrypoints/content/content.ts index 2080585f..332fad1e 100644 --- a/src/entrypoints/content/content.ts +++ b/src/entrypoints/content/content.ts @@ -18,8 +18,6 @@ script.setAttribute('id', __UNIQUE_INJECT_FILENAME__) script.src = chrome.runtime.getURL(__UNIQUE_INJECT_FILENAME__) - Object.defineProperty(Navigator.prototype, 'userAgent', { get: () => 'foobar' }) - parent.prepend(script) } catch (err) { console.warn('🧨 RUA: An error occurred in the content script', err) diff --git a/src/entrypoints/content/inject.ts b/src/entrypoints/content/inject.ts index a9609aaf..fede4d61 100644 --- a/src/entrypoints/content/inject.ts +++ b/src/entrypoints/content/inject.ts @@ -1,8 +1,20 @@ // ⚠ DO NOT IMPORT ANYTHING EXCEPT TYPES HERE DUE THE `import()` ERRORS ⚠ import type { ContentScriptPayload } from '~/shared/types' +import type { DeepWriteable } from '~/types' // wrap everything to avoid polluting the global scope ;(() => { + // prevent the script from running multiple times + { + const [key, ds] = [__UNIQUE_HEADER_KEY_NAME__.toLowerCase(), document.documentElement.dataset] + + if (ds[key] === 'true') { + return + } + + ds[key] = 'true' + } + const debug = (m: string, ...a: unknown[]): void => console.debug(`%c💣 [inject.js]: ${m}`, 'font-weight:bold', ...a) debug('Injected script is running') @@ -50,16 +62,79 @@ import type { ContentScriptPayload } from '~/shared/types' /** @link https://developer.mozilla.org/en-US/docs/Web/API/Navigator */ const patchNavigator = (n: Navigator): void => { /** Overloads the navigator object property with the new value. */ - const overload = (target: T, prop: keyof T | 'oscpu', value: unknown): void => { - const descriptor = Object.getOwnPropertyDescriptor(target, prop) + const overload = ( + t: T, + prop: T extends Navigator ? keyof T | 'oscpu' : keyof T, + value: unknown, + force: boolean = false + ): void => { + let target: T = t + + try { + while (target !== null) { + const descriptor = Object.getOwnPropertyDescriptor(target, prop) + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty + if (descriptor && descriptor.configurable) { + const newAttributes: PropertyDescriptor = { configurable: false, enumerable: true } + + // respect the original value getting method + if (descriptor.get) { + newAttributes.get = () => value + } else { + newAttributes.value = value + newAttributes.writable = false + } + + Object.defineProperty(target, prop, newAttributes) + } else if (force) { + Object.defineProperty(target, prop, { + value, + configurable: false, + enumerable: true, + writable: false, + }) + } - if (descriptor && descriptor.configurable) { - Object.defineProperty(target, prop, { get: () => value }) + target = Object.getPrototypeOf(target) + } + } catch (_) { + // do nothing } } - overload(n, 'userAgent', payload.current.userAgent) + // to test, execute in the console: `console.log(navigator.userAgent)` + overload( + n, + 'userAgent', + ((): string => { + switch (payload.current.browser) { + case 'chrome': + case 'opera': + case 'edge': // blink engine + // mask the browser (and under the hood) versions, keeping only the major version (e.g., 92.0.4515.107 -> 92.0.0.0) + const masked = payload.current.userAgent.replaceAll( + payload.current.version.browser.full, + payload.current.version.browser.major + + '.0'.repeat(Math.max(0, payload.current.version.browser.full.split('.').length - 1)) + ) + + if (payload.current.version.underHood) { + return masked.replaceAll( + payload.current.version.underHood.full || '', + payload.current.version.underHood.major + + '.0'.repeat(Math.max(0, payload.current.version.underHood.full.split('.').length - 1)) + ) + } + + return masked + } + return payload.current.userAgent + })() + ) + + // to test, execute in the console: `console.log(navigator.appVersion)` overload( n, 'appVersion', @@ -78,39 +153,39 @@ import type { ContentScriptPayload } from '~/shared/types' return payload.current.userAgent.replace(/^Mozilla\//i, '') })() ) - - // to test, execute in the console: : `debug(navigator.platform, navigator.oscpu)` - switch (payload.platform) { - case 'Windows': + debug('payload', payload) + // to test, execute in the console: `console.log(navigator.platform, navigator.oscpu)` + switch (payload.current.os) { + case 'windows': overload(n, 'platform', 'Win32') - overload(n, 'oscpu', 'Windows NT; Win64; x64') + overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Windows NT; Win64; x64' : undefined, true) break - case 'Linux': + case 'linux': overload(n, 'platform', 'Linux x86_64') - overload(n, 'oscpu', 'Linux x86_64') + overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Linux x86_64' : undefined, true) break - case 'Android': + case 'android': overload(n, 'platform', 'Linux armv8l') - overload(n, 'oscpu', 'Linux armv8l') + overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Linux armv8l' : undefined, true) break case 'macOS': overload(n, 'platform', 'MacIntel') - overload(n, 'oscpu', 'Mac OS X') + overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Mac OS X' : undefined, true) break case 'iOS': overload(n, 'platform', 'iPhone') - overload(n, 'oscpu', 'Mac OS X') + overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Mac OS X' : undefined, true) break default: - overload(n, 'oscpu', undefined) + overload(n, 'oscpu', undefined, true) } - // to test, execute in the console: : `debug(navigator.vendor)` + // to test, execute in the console: `console.log(navigator.vendor)` switch (payload.current.browser) { case 'chrome': case 'opera': @@ -123,15 +198,105 @@ import type { ContentScriptPayload } from '~/shared/types' break case 'safari': // webkit engine - overload(n, 'vendor', 'Apple Computer Inc.') + overload(n, 'vendor', 'Apple Computer, Inc.') break + + default: + overload(n, 'vendor', undefined) } - /** @link https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData#browser_compatibility */ - // to test, execute in the console: : `debug(navigator.userAgentData, navigator.userAgentData.toJSON())` - if ('userAgentData' in n) { - // TODO + /** + * @link https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData#browser_compatibility + * @link https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/blink/renderer/core/frame/navigator_ua_data.cc + */ + switch (payload.current.browser) { + case 'firefox': + // FireFox does not support the `userAgentData` property yet + overload(n, 'userAgentData', undefined) + break + + case 'safari': + const staticData = { brands: [], mobile: false, platform: '' } + + overload(n, 'userAgentData', { ...staticData, toJSON: () => staticData }) + break + + default: + // TODO: write a code HERE } + // if ('userAgentData' in n && typeof n.userAgentData === 'object') { + // // to test, execute in the console: `console.log(navigator.userAgentData.toJSON())` + // overload( + // n.userAgentData, + // 'toJSON', + // new Proxy(n.userAgentData.toJSON, { + // apply(target, self, args) { + // return payload.current.browser === 'firefox' || payload.current.browser === 'safari' + // ? { brands: [], mobile: false, platform: '' } + // : { + // ...Reflect.apply(target, self, args), + // brands: payload.brands.major.map(({ brand, version }) => ({ brand, version })), + // mobile: payload.isMobile, + // platform: payload.platform, + // } + // }, + // }) + // ) + // + // // to test, execute in the console: `console.log(await navigator.userAgentData.getHighEntropyValues([...]))` + // overload( + // n.userAgentData, + // 'getHighEntropyValues', + // new Proxy(n.userAgentData.getHighEntropyValues, { + // apply(target, self, args) { + // return new Promise((resolve: (v: UADataValues) => void, reject: () => void): void => { + // if (payload.current.browser === 'firefox' || payload.current.browser === 'safari') { + // // TODO: how it looks like in Firefox and Safari? + // return resolve({ brands: [], mobile: false, platform: '' }) + // } + // + // // get the original high entropy values + // Reflect.apply(target, self, args) + // .then((values: UADataValues): void => { + // const data: DeepWriteable = { + // ...values, + // brands: payload.brands.major.map(({ brand, version }) => ({ brand, version })), + // fullVersionList: payload.brands.full.map(({ brand, version }) => ({ brand, version })), + // mobile: payload.isMobile, + // model: '', + // platform: payload.platform, + // platformVersion: ((): string => { + // switch (payload.platform) { + // case 'Windows': + // return '10.0.0' + // case 'Linux': + // return '6.5.0' + // case 'Android': + // return '13.0.0' + // case 'macOS': + // case 'iOS': + // return '14.2.1' + // } + // + // return '' + // })(), + // } + // + // if ('uaFullVersion' in values) { + // data.uaFullVersion = payload.current.version.browser.full + // } + // + // resolve(data) + // }) + // .catch(reject) + // }) + // }, + // }) + // ) + // + // // to test, execute in the console: `console.log(navigator.userAgentData.brands)` + // // overload(n.userAgentData, 'brands') + // } } /** Patches the navigator object for the iframe. */ @@ -152,7 +317,7 @@ import type { ContentScriptPayload } from '~/shared/types' } // patch the current navigator object - patchNavigator(Navigator.prototype) + patchNavigator(navigator) // patch iframes navigators { diff --git a/tsconfig.json b/tsconfig.json index 53260143..a22bf5ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "types": ["vite/client", "node", "chrome", "user-agent-data-types"], "useDefineForClassFields": true, "lib": [ - "ES2020", + "ES2022", "DOM", "DOM.Iterable" ],