From 060db386cd3273130cb7c5f49bd16df0b29294a7 Mon Sep 17 00:00:00 2001 From: Paramtamtam <7326800+tarampampam@users.noreply.github.com> Date: Tue, 30 Apr 2024 17:59:12 +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 --- .../deploy-website-cf-challenge-test.yml | 33 ++ src/entrypoints/content/inject.ts | 355 ++++++++++-------- website/cf-challenge-test/favicon.ico | Bin 0 -> 15086 bytes website/cf-challenge-test/index.html | 39 ++ website/cf-challenge-test/robots.txt | 2 + website/sandbox/robots.txt | 0 6 files changed, 273 insertions(+), 156 deletions(-) create mode 100644 .github/workflows/deploy-website-cf-challenge-test.yml create mode 100644 website/cf-challenge-test/favicon.ico create mode 100644 website/cf-challenge-test/index.html create mode 100644 website/cf-challenge-test/robots.txt create mode 100644 website/sandbox/robots.txt diff --git a/.github/workflows/deploy-website-cf-challenge-test.yml b/.github/workflows/deploy-website-cf-challenge-test.yml new file mode 100644 index 00000000..c448c9ee --- /dev/null +++ b/.github/workflows/deploy-website-cf-challenge-test.yml @@ -0,0 +1,33 @@ +name: 🚀 Deploy the CF challenge test website + +on: + workflow_dispatch: {} + push: + branches: [rewrite] #[master, main] # TODO: change this + tags-ignore: ['**'] + paths: [website/cf-challenge-test/**, .github/workflows/deploy-website-cf-challenge-test.yml] + +concurrency: + group: ${{ github.ref }}-cf-challenge-test-website + cancel-in-progress: true + +jobs: + publish: + name: 🚀 Publish the site + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: cloudflare/wrangler-action@v3 + env: + PROJECT_NAME: random-user-agent-cf-challenge-test + DIST_DIR: ./website/cf-challenge-test + CF_BRANCH_NAME: main # to deploy as "Production" environment on Cloudflare Pages + with: + apiToken: ${{ secrets.CLOUDFLARE_PAGES_DEPLOY_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: + pages deploy ${{ env.DIST_DIR }} + --project-name=${{ env.PROJECT_NAME }} + --branch ${{ env.CF_BRANCH_NAME }} + --commit-dirty=true diff --git a/src/entrypoints/content/inject.ts b/src/entrypoints/content/inject.ts index fede4d61..721917ab 100644 --- a/src/entrypoints/content/inject.ts +++ b/src/entrypoints/content/inject.ts @@ -6,13 +6,14 @@ import type { DeepWriteable } from '~/types' ;(() => { // prevent the script from running multiple times { - const [key, ds] = [__UNIQUE_HEADER_KEY_NAME__.toLowerCase(), document.documentElement.dataset] - - if (ds[key] === 'true') { + const [key, ds, flag] = [__UNIQUE_HEADER_KEY_NAME__.toLowerCase(), document.documentElement.dataset, 'true'] + if (ds[key] === flag) { return } - ds[key] = 'true' + ds[key] = flag + + setTimeout(() => delete ds[key], 1000) // remove the dataset attribute after 1 second } const debug = (m: string, ...a: unknown[]): void => console.debug(`%c💣 [inject.js]: ${m}`, 'font-weight:bold', ...a) @@ -37,7 +38,6 @@ import type { DeepWriteable } from '~/types' /** Finds and removes the injected script */ const findAndRemoveScriptTag = (): boolean => { const injectedScript = document.getElementById(__UNIQUE_INJECT_FILENAME__) as HTMLScriptElement | null - if (injectedScript) { injectedScript.remove() @@ -47,9 +47,53 @@ import type { DeepWriteable } from '~/types' return false } + /** Overloads the object property with the new value. */ + const overload = ( + t: T, + prop: T extends Navigator ? keyof T | 'oscpu' : keyof T, + value: unknown, + options: { force?: boolean; configurable?: boolean } = { force: false, configurable: 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: options.configurable, 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 (options.force && Object.getPrototypeOf(t) === Object.getPrototypeOf(target)) { + Object.defineProperty(target, prop, { + value, + configurable: options.configurable, + enumerable: true, + writable: false, + }) + } + + target = Object.getPrototypeOf(target) + } + } catch (_) { + // do nothing + } + } + try { + // first of all, remove the injected script findAndRemoveScriptTag() + // check the payload existence and do nothing if it is not found const payload = extractPayload() if (!payload) { // no payload = no fun @@ -59,48 +103,14 @@ import type { DeepWriteable } from '~/types' return } - /** @link https://developer.mozilla.org/en-US/docs/Web/API/Navigator */ + /** + * Function to patch the navigator object. + * + * @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 = ( - 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, - }) - } - - target = Object.getPrototypeOf(target) - } - } catch (_) { - // do nothing - } + if (n === null || typeof n !== 'object' || !('userAgent' in n)) { + return } // to test, execute in the console: `console.log(navigator.userAgent)` @@ -153,36 +163,38 @@ import type { DeepWriteable } from '~/types' return payload.current.userAgent.replace(/^Mozilla\//i, '') })() ) - 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', payload.current.browser === 'firefox' ? 'Windows NT; Win64; x64' : undefined, true) + overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Windows NT; Win64; x64' : undefined, { + force: true, + }) break case 'linux': overload(n, 'platform', 'Linux x86_64') - overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Linux x86_64' : undefined, true) + overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Linux x86_64' : undefined, { force: true }) break case 'android': overload(n, 'platform', 'Linux armv8l') - overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Linux armv8l' : undefined, true) + overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Linux armv8l' : undefined, { force: true }) break case 'macOS': overload(n, 'platform', 'MacIntel') - overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Mac OS X' : undefined, true) + overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Mac OS X' : undefined, { force: true }) break case 'iOS': overload(n, 'platform', 'iPhone') - overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Mac OS X' : undefined, true) + overload(n, 'oscpu', payload.current.browser === 'firefox' ? 'Mac OS X' : undefined, { force: true }) break default: - overload(n, 'oscpu', undefined, true) + overload(n, 'oscpu', undefined, { force: true }) } // to test, execute in the console: `console.log(navigator.vendor)` @@ -211,108 +223,136 @@ import type { DeepWriteable } from '~/types' */ 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 }) + // FireFox and Safari does not support the `userAgentData` property yet + overload(n, 'userAgentData', undefined, { force: true }) break default: - // TODO: write a code HERE + // check the `userAgentData` property availability in current (real) browser + const isAvailable = 'userAgentData' in n && typeof n.userAgentData === 'object' + + // store the original `userAgentData`, or craft a mock object if it does not exist + const agentDataObject: NavigatorUAData = isAvailable + ? n.userAgentData + : { + brands: [], + mobile: false, + platform: '', + toJSON(): UALowEntropyJSON { + return { brands: [], mobile: false, platform: '' } + }, + getHighEntropyValues(_: Array): Promise { + return Promise.resolve({ brands: [], mobile: false, platform: '' }) + }, + } + + // if the real browser does not support the `userAgentData` property, then overload it with the mock object + // this is necessary to avoid errors during overload the `userAgentData` properties + if (!isAvailable) { + overload(n, 'userAgentData', agentDataObject, { force: true, configurable: true }) + } + + // to test, execute in the console: `console.log(navigator.userAgentData.brands)` + overload( + n.userAgentData, + 'brands', + payload.brands.major.map(({ brand, version }) => ({ brand, version })) + ) + + // to test, execute in the console: `console.log(navigator.userAgentData.mobile)` + overload(n.userAgentData, 'mobile', payload.isMobile) + + // to test, execute in the console: `console.log(navigator.userAgentData.platform)` + overload(n.userAgentData, 'platform', payload.platform) + + // to test, execute in the console: `console.log(navigator.userAgentData.toJSON())` + overload( + n.userAgentData, + 'toJSON', + new Proxy(agentDataObject.toJSON, { + apply(target, self, args) { + return { + ...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(agentDataObject.getHighEntropyValues, { + apply(target, self, args) { + return new Promise((resolve: (v: UADataValues) => void, reject: () => void): void => { + // 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) + }) + }, + }) + ) } - // 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. */ - const patchNavigatorInIframe = (iframe: Node): void => { - if (iframe.nodeName !== 'IFRAME') { + const patchNavigatorInIframe = (node: Node): void => { + if (typeof node !== 'object' || node == null || node.nodeName !== 'IFRAME' || !('contentWindow' in node)) { return } - debug('path an iframe', iframe) + try { + const iFrame = node as HTMLIFrameElement - const iFrame = iframe as HTMLIFrameElement + if (typeof iFrame.contentWindow !== 'object' || iFrame.contentWindow == null) { + return + } + + const [key, ds, flag] = [__UNIQUE_HEADER_KEY_NAME__.toLowerCase(), iFrame.dataset, 'true'] + if (ds[key] === flag) { + return // already patched + } + + ds[key] = flag - try { iFrame.contentWindow && patchNavigator(iFrame.contentWindow.navigator) } catch (_) { - // do nothing + // An error occurred while patching the navigator object in the iframe } } @@ -324,23 +364,26 @@ import type { DeepWriteable } from '~/types' // currently existing Array(...document.getElementsByTagName('iframe')).forEach(patchNavigatorInIframe) - // override the appendChild method to patch the navigator object for the new iframes - Node.prototype.appendChild = new Proxy(Node.prototype.appendChild, { - apply(target, thisArg, args): unknown { - Array(...args).forEach((node): void => { - patchNavigatorInIframe(node) - }) + const overloadOpts: Parameters[3] = { configurable: true, force: true } + const proxyInvoke = any>(what: T): T => + new Proxy(what, { + apply(target, thisArg, args) { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/apply + const result = Reflect.apply(target, thisArg, args) - return Reflect.apply(target, thisArg, args) - }, - }) + // patch the navigator object in the appended node + Array.isArray(args) && args.forEach((node) => patchNavigatorInIframe(node)) - // every created during the page load - window.addEventListener( - 'load', - (): void => Array(...document.getElementsByTagName('iframe')).forEach(patchNavigatorInIframe), - { once: true, passive: true } - ) + return result + }, + }) + + // patch the methods that can add new nodes to the DOM + // TY @Certseeds for the idea (https://github.com/tarampampam/random-user-agent/pull/173) + overload(Node.prototype, 'appendChild', proxyInvoke(Node.prototype.appendChild), overloadOpts) + overload(Node.prototype, 'insertBefore', proxyInvoke(Node.prototype.insertBefore), overloadOpts) + overload(Element.prototype, 'append', proxyInvoke(Element.prototype.append), overloadOpts) + overload(Element.prototype, 'prepend', proxyInvoke(Element.prototype.prepend), overloadOpts) // watch for the new dynamically created iframes new MutationObserver((mutations): void => { @@ -348,6 +391,6 @@ import type { DeepWriteable } from '~/types' }).observe(document, { childList: true, subtree: true }) } } catch (err) { - debug('An error occurred in the injected script', err) + console.warn('💣 RUA: An error occurred in the injected script', err) } })() diff --git a/website/cf-challenge-test/favicon.ico b/website/cf-challenge-test/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..53c4c7a1a63c565251586c04e78588e6c5a7f7fa GIT binary patch literal 15086 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 + + + + + + CloudFlare challenge test page + + +
+

CloudFlare challenge test page

+

+ This page is used to test if the CloudFlare challenge is working correctly. +

+

+ If you see this page, it means that the challenge has been successfully completed. +

+
+ + diff --git a/website/cf-challenge-test/robots.txt b/website/cf-challenge-test/robots.txt new file mode 100644 index 00000000..1f53798b --- /dev/null +++ b/website/cf-challenge-test/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/website/sandbox/robots.txt b/website/sandbox/robots.txt new file mode 100644 index 00000000..e69de29b