From 4ec6a2c56d90477392b9e739456c00c765fdacd0 Mon Sep 17 00:00:00 2001 From: Paramtamtam <7326800+tarampampam@users.noreply.github.com> Date: Thu, 28 Oct 2021 10:11:19 +0500 Subject: [PATCH] Aggressive User-Agent detection fixed (#165) --- .codecov.yml | 3 + CHANGELOG.md | 13 +++ README.md | 20 ----- package.json | 1 + src/background.ts | 4 + src/content-script.ts | 150 +++++++++++++++++++++------------- src/hooks/headers-received.ts | 83 +++++++++++++++++++ webpack/webpack.common.js | 7 ++ yarn.lock | 18 ++++ 9 files changed, 220 insertions(+), 79 deletions(-) create mode 100644 src/hooks/headers-received.ts diff --git a/.codecov.yml b/.codecov.yml index 0e9b4e14..a3a8cc4a 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,5 +1,8 @@ # Docs: +github_checks: # https://docs.codecov.com/docs/github-checks#disabling-github-checks-patch-annotations-via-yaml + annotations: false + coverage: # coverage lower than 50 is red, higher than 90 green range: 30..80 diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3045b5..68b96907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog][keepachangelog] and this project adheres to [Semantic Versioning][semver]. +## UNRELEASED + +### Added + +- Watching for the dynamically created iframes and pathing them + +### Fixed + +- Aggressive User-Agent detection (now even the inline scripts cannot detect the real User-Agent; thanks to [@neroux](https://github.com/neroux) for the idea) [#26], [#36] + +[#26]:https://github.com/tarampampam/random-user-agent/issues/26 +[#36]:https://github.com/tarampampam/random-user-agent/issues/36 + ## v3.1.1 ### Fixed diff --git a/README.md b/README.md index 0015d7a3..0ec0fe66 100644 --- a/README.md +++ b/README.md @@ -37,26 +37,6 @@ means and then combine that with your randomly changing `User-Agent` to pretty e see [this GitHub issue](https://github.com/tarampampam/random-user-agent/issues/47). -
- User-agent can't be replaced (for now) in Google Chrome for pages with aggressive (inline JavaScript) detection - -Example: - -```html - - - - - - -``` - -This method is quite rare (usually JavaScript code is wrapped in `Promises`, `setTimeout` or event listeners), but so -far no way around this kind of checking has been invented. -
- ## 🧩 Install Follow up by one of the links at the top 👆 of this page, or download directly the latest release from the diff --git a/package.json b/package.json index d8c09e84..171a17e6 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "filemanager-webpack-plugin": "^6.1.7", "jest": "^27.3.1", "json-minimizer-webpack-plugin": "^3.1.0", + "randomstring": "^1.2.1", "sass": "^1.43.3", "sass-loader": "^12.2.0", "terser-webpack-plugin": "^5.2.4", diff --git a/src/background.ts b/src/background.ts index 36cea23d..b0c99cdc 100644 --- a/src/background.ts +++ b/src/background.ts @@ -18,6 +18,7 @@ import GetSettings from './messaging/handlers/get-settings' import Useragent, {UseragentStateEvent} from './useragent/useragent' import GetUseragent from './messaging/handlers/get-useragent' import UpdateUseragent from './messaging/handlers/update-useragent' +import HeadersReceived from './hooks/headers-received' // define default errors handler for the background page const errorsHandler: (err: Error) => void = console.error @@ -103,6 +104,9 @@ useragent.load().then((): void => { // load useragent state // this hook is required for the HTTP headers modification new BeforeSendHeaders(settings, useragent, filterService).listen() + + // this hook allows to send important data to the content script without using sendMessage() + new HeadersReceived(settings, useragent, filterService).listen() }).catch(errorsHandler) }).catch(errorsHandler) }).catch(errorsHandler) diff --git a/src/content-script.ts b/src/content-script.ts index 31d1424e..11dbb542 100644 --- a/src/content-script.ts +++ b/src/content-script.ts @@ -2,83 +2,115 @@ import {RuntimeSender} from './messaging/runtime' import {applicableToURI, ApplicableToURIResponse} from './messaging/handlers/applicable-to-uri' import {getSettings, GetSettingsResponse} from './messaging/handlers/get-settings' import {getUseragent, GetUseragentResponse} from './messaging/handlers/get-useragent' +import {CookieName, decode, Payload} from './hooks/headers-received' -new RuntimeSender() - .send( // order is important! - applicableToURI(window.location.href), - getSettings(), - getUseragent(), - ) - .then((resp): void => { // <-- the promise is the main problem for hiding from inline scripts detection - const applicable = (resp[0] as ApplicableToURIResponse).payload.applicable - const settings = (resp[1] as GetSettingsResponse).payload - const useragent = (resp[2] as GetUseragentResponse).payload.useragent - - if (applicable && settings.jsProtection.enabled) { - const script = document.createElement('script'), parent = document.head || document.documentElement - - script.textContent = '(' + function (useragent: string): void { - // allows to overload object property with a getter function (without potential exceptions) - const overloadPropertyWithGetter = (object: any, property: string, value: any): void => { - if (typeof object === 'object') { - if (Object.getOwnPropertyDescriptor(object, property) === undefined) { - Object.defineProperty(object, property, {get: (): any => value}) - } +new Promise((resolve: (p: Payload) => void, reject: (e: Error) => void) => { + // make an attempt to fetch the payload from the cookies + const cookies = document.cookie.split(';') + + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trimLeft() + + if (cookie.startsWith(CookieName + '=')) { + const parts = cookie.split('=') + + if (parts.length >= 2) { + document.cookie = `${CookieName}=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/` // remove the cookie + + return resolve(decode(parts[1])) + } + } + } + + // and as a fallback - sending requests to the background script + new RuntimeSender() + .send( // order is important! + applicableToURI(window.location.href), + getSettings(), + getUseragent(), + ) + .then((resp): void => { // <-- the promise is the main problem for hiding from inline scripts detection + const applicable = (resp[0] as ApplicableToURIResponse).payload.applicable + const settings = (resp[1] as GetSettingsResponse).payload + const useragent = (resp[2] as GetUseragentResponse).payload.useragent + + if (applicable && settings.jsProtection.enabled && typeof useragent === 'string') { + return resolve({ + useragent: useragent, + }) + } + }) + .catch(reject) +}) + .then((p: Payload): string => '(' + function (p: Payload): void { + // allows to overload object property with a getter function (without potential exceptions) + const overloadPropertyWithGetter = (object: object, property: string, value: any): void => { + if (typeof object === 'object') { + if (Object.getOwnPropertyDescriptor(object, property) === undefined) { + Object.defineProperty(object, property, {get: (): any => value}) } } + } - // makes required navigator object modifications - const patchNavigator = (navigator: Navigator): void => { - if (typeof navigator === 'object') { - overloadPropertyWithGetter(navigator, 'userAgent', useragent) + // makes required navigator object modifications + const patchNavigator = (navigator: Navigator): void => { + if (typeof navigator === 'object') { + overloadPropertyWithGetter(navigator, 'userAgent', p.useragent) - // app version should not contain "Mozilla/" prefix - overloadPropertyWithGetter(navigator, 'appVersion', useragent.replace(/^Mozilla\//i, '')) + // app version should not contain "Mozilla/" prefix + overloadPropertyWithGetter(navigator, 'appVersion', p.useragent.replace(/^Mozilla\//i, '')) - // firefox always with an empty vendor - if (useragent.toLowerCase().includes('firefox\/')) { - overloadPropertyWithGetter(navigator, 'vendor', '') - } + // firefox always with an empty vendor + if (p.useragent.toLowerCase().includes('firefox\/')) { + overloadPropertyWithGetter(navigator, 'vendor', '') } } + } - // patch current window navigator - patchNavigator(window.navigator) + // patch current window navigator + patchNavigator(window.navigator) - // handler for patching navigator object for the iframes - // issue: - const patchIFramesHandler = (): void => { - try { - const iframes = document.getElementsByTagName('iframe') + // handler for patching navigator object for the iframes + // issue: + window.addEventListener('load', (): void => { + const iframes = document.getElementsByTagName('iframe') - for (let i = 0; i < iframes.length; i++) { - const contentWindow = iframes[i].contentWindow + for (let i = 0; i < iframes.length; i++) { + const contentWindow = iframes[i].contentWindow + + if (typeof contentWindow === 'object' && contentWindow !== null) { + patchNavigator(contentWindow.navigator) + } + } + }, {once: true, passive: true}) + + // watch for the new iframes dynamic creation + new MutationObserver((mutations): void => { + mutations.forEach((mutation): void => { + mutation.addedNodes.forEach((addedNode): void => { + if (addedNode.nodeName === 'IFRAME') { + const iframe = addedNode as HTMLIFrameElement, contentWindow = iframe.contentWindow if (typeof contentWindow === 'object' && contentWindow !== null) { patchNavigator(contentWindow.navigator) } } - } finally { - window.removeEventListener('load', patchIFramesHandler) - } - } + }) + }) + }).observe(document, {childList: true, subtree: true}) + } + `)(${JSON.stringify(p)})`, + ) + .then((scriptContent: string): void => { + const script = document.createElement('script'), parent = document.head || document.documentElement - window.addEventListener('load', patchIFramesHandler) - } + `)("${useragent}")` + script.textContent = scriptContent - // script.defer = false - // script.async = false - parent.appendChild(script) // execute the script + // script.defer = false + // script.async = false + parent.appendChild(script) // execute the script - setTimeout(() => { - parent.removeChild(script) - }) // and remove them on a next tick - } + setTimeout(() => { + parent.removeChild(script) + }) // and remove them on a next tick }) .catch(console.warn) - -// Duty, but workable hack: -// const when = Date.now() + 500 -// while (Date.now() < when) { -// // do nothing -// } diff --git a/src/hooks/headers-received.ts b/src/hooks/headers-received.ts new file mode 100644 index 00000000..4dfe86ae --- /dev/null +++ b/src/hooks/headers-received.ts @@ -0,0 +1,83 @@ +import Settings from '../settings/settings' +import Useragent from '../useragent/useragent' +import FilterService from '../services/filter-service' +import BlockingResponse = chrome.webRequest.BlockingResponse +import WebResponseHeadersDetails = chrome.webRequest.WebResponseHeadersDetails + +declare var __UNIQUE_RUA_COOKIE_NAME__: string // see the webpack config, section "plugins" (webpack.DefinePlugin) +export const CookieName: string = __UNIQUE_RUA_COOKIE_NAME__ + +export interface Payload { + useragent: string +} + +export function encode(payload: Payload): string { + return window.btoa( + unescape( + encodeURIComponent( + JSON.stringify(payload), + ), + ), + ).replace(/=/g, '-') +} + +export function decode(str: string): Payload { + return JSON.parse( + decodeURIComponent( + escape( + window.atob( + str.replace(/-/g, '='), + ), + ), + ), + ) +} + +export default class HeadersReceived { + private readonly settings: Settings + private readonly useragent: Useragent + private readonly filterService: FilterService + + constructor(settings: Settings, useragent: Useragent, filterService: FilterService) { + this.settings = settings + this.useragent = useragent + this.filterService = filterService + } + + /** + * Great thanks to (your idea is amazing!) + * + * @link https://developer.chrome.com/docs/extensions/reference/webRequest/ chrome.webRequest + */ + listen(): void { + chrome.webRequest.onHeadersReceived.addListener( + (details: WebResponseHeadersDetails): BlockingResponse | void => { + if (details.type === 'main_frame' || details.type === 'sub_frame') { + const settings = this.settings.get() + + if (settings.enabled && settings.jsProtection.enabled && this.filterService.applicableToURI(details.url)) { + const useragent = this.useragent.get().useragent + + if (details.responseHeaders && typeof useragent === 'string') { + const date = new Date() + date.setTime(date.getTime() + 60 * 1000) // +60 seconds + + const payload: Payload = { + useragent: useragent, + } + + details.responseHeaders.push({ + name: 'Set-Cookie', + value: `${CookieName}=${encode(payload)}; expires=${date.toUTCString()}; path=/`, + }) + + return {responseHeaders: details.responseHeaders} + } + } + } + }, + {urls: ['']}, + ['blocking', 'responseHeaders', 'extraHeaders'], // extraHeaders - https://stackoverflow.com/a/66558910/2252921 + ) + } +} diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index b087b0bb..d39f7241 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -4,6 +4,7 @@ const CopyPlugin = require('copy-webpack-plugin') const ManifestVersionSyncPlugin = require('./plugins/manifest-version-sync') const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin') const TerserPlugin = require('terser-webpack-plugin') +const randomstring = require('randomstring') const {VueLoaderPlugin} = require('vue-loader') const srcDir = path.join(__dirname, '..', 'src') @@ -63,6 +64,12 @@ module.exports = { extensions: ['.ts', '.js'], }, plugins: [ + new webpack.DefinePlugin({ + __UNIQUE_RUA_COOKIE_NAME__: JSON.stringify(randomstring.generate({ + length: Math.floor(Math.random() * 12 + 5), + charset: 'alphabetic', + })), + }), new webpack.DefinePlugin({ // https://github.com/vuejs/vue-next/tree/master/packages/vue#bundler-build-feature-flags __VUE_OPTIONS_API__: true, __VUE_PROD_DEVTOOLS__: false, diff --git a/yarn.lock b/yarn.lock index addbfc3a..d250b002 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1113,6 +1113,11 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +array-uniq@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.2.tgz#5fcc373920775723cfd64d65c64bef53bf9eba6d" + integrity sha1-X8w3OSB3VyPP1k1lxkvvU7+eum0= + array-uniq@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" @@ -3811,6 +3816,11 @@ randexp@^0.5.3: drange "^1.0.2" ret "^0.2.0" +randombytes@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.3.tgz#674c99760901c3c4112771a31e521dc349cc09ec" + integrity sha1-Z0yZdgkBw8QRJ3GjHlIdw0nMCew= + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -3818,6 +3828,14 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +randomstring@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/randomstring/-/randomstring-1.2.1.tgz#71cd3cda24ad1b7e0b65286b3aa5c10853019349" + integrity sha512-eMnfell9XuU3jfCx3f4xCaFAt0YMFPZhx9R3PSStmLarDKg5j5vivqKhf/8pvG+VX/YkxsckHK/VPUrKa5V07A== + dependencies: + array-uniq "1.0.2" + randombytes "2.0.3" + react-is@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"