Skip to content

Commit

Permalink
wip: 🔕 temporary commit
Browse files Browse the repository at this point in the history
  • Loading branch information
tarampampam committed Apr 29, 2024
1 parent 6ae1b13 commit e1613ac
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 76 deletions.
8 changes: 5 additions & 3 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@
"declarativeNetRequest"
],
"$what_the_specified_permissions_are_for": {
"storage": "To keep user settings and sync them between browser sessions ('chrome.storage' API)",
"declarativeNetRequest": "To modify HTTP headers ('chrome.declarativeNetRequest' API)",
"alarms": "To schedule user-agent renewing and other periodic tasks ('chrome.alarms' API)"
"tabs": "Obtaining the current tab URL to verify if the extension is enabled for the specific URL in the popup",
"alarms": "Scheduling user-agent renewal and other periodic tasks",
"storage": "Managing and synchronizing user settings across browser sessions",
"scripting": "Injecting user-agent modification code into web pages",
"declarativeNetRequest": "Modifying HTTP headers"
},
"incognito": "spanning",
"minimum_chrome_version": "120"
Expand Down
47 changes: 24 additions & 23 deletions src/entrypoints/content/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,19 @@ try {
if (!payload) {
// no payload = no fun
//
// probably, the script was removed by the content script, registered with the `world: 'MAIN'` property
// (Chromium-based browsers only)
// probably, the page was already patched and the script tag was removed by this script, but registered with
// the `world: 'MAIN'` property (Chromium-based browsers only)
return
}

/** @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 extends Navigator>(target: T, prop: keyof T | 'oscpu', value: unknown): void => {
if (prop in target) {
Object.getOwnPropertyDescriptor(target, prop) || Object.defineProperty(target, prop, { get: () => value })
const descriptor = Object.getOwnPropertyDescriptor(target, prop)

if (descriptor && descriptor.configurable) {
Object.defineProperty(target, prop, { get: () => value })
}
}

Expand Down Expand Up @@ -126,39 +128,38 @@ try {
}
}

/** Patches the navigator object for the iframe. */
const patchNavigatorInIframe = (iframe: Node): void => {
if (iframe.nodeName !== 'IFRAME') {
return
}

const iFrame = iframe as HTMLIFrameElement

try {
iFrame.contentWindow && patchNavigator(iFrame.contentWindow.navigator)
} catch (_) {
// do nothing
}
}

// patch the current navigator object
console.debug(`%c👻 RUA: Patching the current navigator object`, 'font-weight:bold')
patchNavigator(navigator)
patchNavigator(Navigator.prototype)

// handler for patching navigator object for the iframes
window.addEventListener(
'load',
(): void => {
;[...document.getElementsByTagName('iframe')].forEach((iFrame) => {
if (iFrame.contentWindow && 'navigator' in iFrame.contentWindow) {
patchNavigator(iFrame.contentWindow.navigator)
}
})
;[...document.getElementsByTagName('iframe')].forEach(patchNavigatorInIframe)
},
{ once: true, passive: true }
)
console.debug(`%c👻 RUA: Patching the navigator objects of the iframes`, 'font-weight:bold')

// watch for the new iframes and patch their navigator objects
new MutationObserver((mutations): void => {
mutations.forEach((mutation): void => {
mutation.addedNodes.forEach((addedNode): void => {
if (addedNode.nodeName === 'IFRAME') {
const iFrame = addedNode as HTMLIFrameElement

if (iFrame.contentWindow && 'navigator' in iFrame.contentWindow) {
patchNavigator(iFrame.contentWindow.navigator)
}
}
})
})
mutations.forEach((mutation): void => mutation.addedNodes.forEach(patchNavigatorInIframe))
}).observe(document, { childList: true, subtree: true })
console.debug(`%c👻 RUA: Watching for the new iframes and patching their navigator objects`, 'font-weight:bold')
})()
} catch (err) {
console.debug(`%c👻 RUA: An error occurred in the injected script`, 'font-weight:bold', err)
Expand Down
145 changes: 95 additions & 50 deletions website/sandbox/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script>
/**
* @typedef {Object} Item
* @property {{
* userAgent: string
* platform: string
* oscpu: string
* vendor: string
* userAgentData: {
* brands: Array<Record<string, string>>
* mobile: boolean
* platform: string
* toJSON: Record<string, unknown>
* highEntropyValues: Record<string, unknown>|undefined
* }
* }} navigator
* @property {number} timestamp
*/

/** @type {Item} */
const veryFirstUserAgentData = { // emulate aggressive user-agent data capturing
navigator: {
userAgent: navigator.userAgent,
platform: navigator.platform,
oscpu: navigator.oscpu,
vendor: navigator.vendor,
userAgentData: {
brands: navigator.userAgentData?.brands,
mobile: navigator.userAgentData?.mobile,
platform: navigator.userAgentData?.platform,
toJSON: navigator.userAgentData?.toJSON(),
getHighEntropyValues: undefined, // do not call it here, coz it's async
},
},
timestamp: Date.now(),
}

console.log('The first line of JavaScript code is executed now')
</script>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"/>
Expand Down Expand Up @@ -145,27 +184,9 @@
</main>

<script type="module">
/* global hljs */

console.log('The page is loaded and the script is running...')
/* global hljs */

/**
* @typedef {Object} Item
* @property {{
* userAgent: string
* platform: string
* oscpu: string
* vendor: string
* userAgentData: {
* brands: string
* mobile: boolean
* platform: string
* toJSON: string
* getHighEntropyValues: string
* }
* }} navigator
* @property {number} timestamp
*/
console.log('The page is loaded and the script is running. The very first captured user-agent data:', veryFirstUserAgentData)

/** @type {HTMLUListElement} */
const historyList = document.getElementById('history')
Expand Down Expand Up @@ -212,6 +233,7 @@

// loop over the array and render each item
arr.map((item, i) => {
const emptyPlaceholder = '<empty>'
const li = document.createElement('li')
const table = document.createElement('table')
const tbody = document.createElement('tbody')
Expand All @@ -220,23 +242,26 @@
item.timestamp && addTableRow(tbody, '⏱ timestamp', new Date(item.timestamp).toISOString() + (sincePrev === 0 ? '' : ` (+${sincePrev}ms)`))

addTableRow(tbody,
'🤖 navigator.userAgent', item.navigator.userAgent,
'🤖 navigator.userAgent', item.navigator.userAgent ?? emptyPlaceholder,
'https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgent'
)
addTableRow(tbody,
'🗑️ navigator.platform', item.navigator.platform,
'🗑️ navigator.platform', item.navigator.platform ?? emptyPlaceholder,
'https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform'
)
addTableRow(tbody,
'🗑️ navigator.oscpu (🦊)', item.navigator.oscpu,
'🗑️ navigator.oscpu (🦊)', item.navigator.oscpu ?? emptyPlaceholder,
'https://developer.mozilla.org/en-US/docs/Web/API/Navigator/oscpu#browser_compatibility'
)
addTableRow(tbody,
'🗑️ navigator.vendor', item.navigator.vendor,
'🗑️ navigator.vendor', item.navigator.vendor ?? emptyPlaceholder,
'https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vendor'
)
addTableRow(tbody,
'🧪 userAgentData.brands (!🦊 !🧭)', item.navigator.userAgentData.brands,
'🧪 userAgentData.brands (!🦊 !🧭)',
item.navigator.userAgentData.brands
? JSON.stringify(item.navigator.userAgentData.brands)
: emptyPlaceholder,
'https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/brands'
)
addTableRow(tbody,
Expand All @@ -248,11 +273,17 @@
'https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/platform'
)
addTableRow(tbody,
'🧪 userAgentData.toJSON() (!🦊 !🧭)', item.navigator.userAgentData.toJSON,
'🧪 userAgentData.toJSON() (!🦊 !🧭)',
item.navigator.userAgentData.toJSON
? JSON.stringify(item.navigator.userAgentData.toJSON)
: emptyPlaceholder,
'https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/toJSON'
)
addTableRow(tbody,
'🧪 userAgentData.getHighEntropyValues() (!🦊 !🧭)', item.navigator.userAgentData.getHighEntropyValues,
item.navigator.userAgentData.highEntropyValues && addTableRow(tbody,
'🧪 userAgentData.getHighEntropyValues() (!🦊 !🧭)',
item.navigator.userAgentData.highEntropyValues
? JSON.stringify(item.navigator.userAgentData.highEntropyValues)
: emptyPlaceholder,
'https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues'
)

Expand Down Expand Up @@ -289,17 +320,12 @@
})

/**
* Check the user-agent and add it to the history.
* Capture the high-entropy values.
*
* @return {Promise<void>}
* @return {Promise<Record<string, unknown>|undefined>}
*/
const check = async () => {
const emptyPlaceholder = '<empty>'
const timestamp = Date.now()

const navigatorBrands = navigator?.userAgentData?.brands
const userAgentDataAsJson = navigator?.userAgentData?.toJSON()
const highEntropyValues = await navigator?.userAgentData?.getHighEntropyValues([
const captureHighEntropyValues = async () => {
return navigator?.userAgentData?.getHighEntropyValues([
'architecture',
'bitness',
'formFactor',
Expand All @@ -309,42 +335,61 @@
'uaFullVersion',
'wow64',
])
}

/**
* Check the user-agent and add it to the history.
*
* @return {Promise<void>}
*/
const check = async () => {
// capture everything, including the high-entropy values
const data = {
navigator: {
userAgent: navigator.userAgent ?? emptyPlaceholder,
platform: navigator.platform ?? emptyPlaceholder,
oscpu: navigator?.oscpu ?? emptyPlaceholder,
vendor: navigator?.vendor ?? emptyPlaceholder,
userAgent: navigator.userAgent,
platform: navigator.platform,
oscpu: navigator?.oscpu,
vendor: navigator?.vendor,
userAgentData: {
brands: navigatorBrands ? JSON.stringify(navigatorBrands) : emptyPlaceholder,
mobile: navigator?.userAgentData?.mobile ?? emptyPlaceholder,
platform: navigator?.userAgentData?.platform ?? emptyPlaceholder,
toJSON: userAgentDataAsJson ? JSON.stringify(userAgentDataAsJson) : emptyPlaceholder,
getHighEntropyValues: highEntropyValues ? JSON.stringify(highEntropyValues) : emptyPlaceholder,
brands: navigator?.userAgentData?.brands,
mobile: navigator?.userAgentData?.mobile,
platform: navigator?.userAgentData?.platform,
toJSON: navigator?.userAgentData?.toJSON(),
highEntropyValues: undefined, // set it later
},
},
timestamp: 0, // set the timestamp later (we need zero for the comparison)
}

// if the last array item is the same as the current data, don't add it (compare without the timestamp, of course)
if (historyItems.length > 0) {
const lastItem = {...historyItems[historyItems.length - 1]}
const lastItem = structuredClone(historyItems[historyItems.length - 1])
lastItem.timestamp = 0
lastItem.navigator.userAgentData.highEntropyValues = undefined

if (JSON.stringify(lastItem) === JSON.stringify(data)) {
return
}
}

// push the data to the history
historyItems.push({...data, timestamp})
data.timestamp = Date.now()
data.navigator.userAgentData.highEntropyValues = await captureHighEntropyValues()

// and sort the history by the timestamp
console.log('The user-agent data has changed:', data)

// sort the history by the timestamp
historyItems.sort((a, b) => a.timestamp - b.timestamp)

// push the data to the history
historyItems.push(data)
}

// add high-entropy values to the very first item (later is better than never)
veryFirstUserAgentData.navigator.userAgentData.highEntropyValues = await captureHighEntropyValues()

// add the very first item to the history
historyItems.push(veryFirstUserAgentData)

// initial check
check().then(() => console.log('The initial check is done')).catch(console.error)

Expand Down

0 comments on commit e1613ac

Please sign in to comment.