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 28, 2024
1 parent 9ba3096 commit 6ae1b13
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 39 deletions.
14 changes: 2 additions & 12 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,7 @@
},
"default_title": "__MSG_manifest_action_default_title__"
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"content.js"
],
"all_frames": true,
"run_at": "document_start"
}
],

"commands": {
"renew-useragent": {
"description": "__MSG_manifest_command_renew_useragent__",
Expand All @@ -59,6 +48,7 @@
"tabs",
"alarms",
"storage",
"scripting",
"declarativeNetRequest"
],
"$what_the_specified_permissions_are_for": {
Expand Down
1 change: 1 addition & 0 deletions src/entrypoints/background/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { setRequestHeaders, unsetRequestHeaders } from './http-requests'
export { setBridgeData, unsetBridgeData } from './content-script-bridge'
export { registerContentScripts } from './scripting'
41 changes: 41 additions & 0 deletions src/entrypoints/background/hooks/scripting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import RegisteredContentScript = chrome.scripting.RegisteredContentScript

// the common properties for the content scripts
const common: Omit<RegisteredContentScript, 'id'> = {
matches: ['<all_urls>'],
allFrames: true,
runAt: 'document_start',
}

// properties for the content script that will be executed in the isolated world (as a content script)
const content: RegisteredContentScript = { ...common, id: 'content', js: ['content.js'] }

// properties for the content script that will be executed in the main world (as an injected script)
const inject: RegisteredContentScript = { ...common, id: 'inject', js: [__UNIQUE_INJECT_FILENAME__] }

/** Register the content scripts */
export async function registerContentScripts() {
// first, unregister (probably) previously registered content scripts
await chrome.scripting.unregisterContentScripts()

try {
await chrome.scripting.registerContentScripts([
{ ...content, world: 'ISOLATED' },
{ ...inject, world: 'MAIN' },
])
} catch (err) {
if (
err instanceof Error &&
err.message.toLowerCase().includes('unexpected property') &&
err.message.includes('world')
) {
// if so, it means that the "world" property is not supported by the current browser (FireFox at this moment)
// so we need to register the content scripts without the "world" property
//
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/RegisteredContentScript#browser_compatibility
return await chrome.scripting.registerContentScripts([content, inject])
}

throw err
}
}
5 changes: 4 additions & 1 deletion src/entrypoints/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { checkPermissions, detectBrowser, watchPermissionsChange } from '~/share
import { type HandlersMap, listen as listenRuntime } from '~/shared/messaging'
import { newErrorEvent, newExtensionLoadedEvent } from '~/shared/stats'
import { isApplicableForDomain, reloadRequestHeadersAndBridge, renewUserAgent, updateRemoteUserAgentList } from './api'
import { unsetBridgeData, unsetRequestHeaders } from './hooks'
import { registerContentScripts, unsetBridgeData, unsetRequestHeaders } from './hooks'
import { registerHotkeys } from './hotkeys'
import { CurrentUserAgent, RemoteUserAgentList, Settings, StorageArea, UserID } from './persistent'
import { type Collector as StatsCollector, GaCollector } from './stats'
Expand All @@ -17,6 +17,9 @@ let stats: StatsCollector | undefined = undefined

// run the background script
;(async () => {
// register the content scripts
await registerContentScripts()

// at least FireFox does not allow the extension to work with all URLs by default, and the user must grant the
// necessary permissions. in addition, the user can revoke the permissions at any time, so we need to monitor the
// changes in the permissions and open the onboarding page if the necessary permissions are missing
Expand Down
24 changes: 22 additions & 2 deletions src/entrypoints/content/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const isApplicableToDomain = (currentDomain: string, p: ContentScriptPayload): b
try {
const key = __UNIQUE_PAYLOAD_KEY_NAME__

// get the payload from the storage
chrome.storage.local.get([key], (items) => {
if (chrome.runtime.lastError) {
throw new Error(chrome.runtime.lastError.message)
Expand All @@ -59,12 +60,31 @@ try {
const payload = items[key] as ContentScriptPayload
const currentDomain = window.location.hostname

console.log('currentDomain', currentDomain, isApplicableToDomain(currentDomain, payload), payload)

if (!isApplicableToDomain(currentDomain, payload)) {
return // the script is not applicable to the current domain
}

// Important Note:
//
// Chromium-based browsers (like Chrome, Edge, Opera, etc.) support the `world` property in the
// `chrome.scripting.registerContentScripts` API. This property allows content scripts to run in the "isolated"
// world, which is necessary for accessing the payload from the storage using the `chrome.store` API. Additionally,
// they can run in the "main" world as an "inject" script, which modifies the user agent.
//
// However, FireFox does not yet support the `world` property. Therefore, I need to ensure that the "inject" script
// code is executed in both environments.
//
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/RegisteredContentScript
//
// For Chromium-based browsers, the "inject" script is registered as a content script in the
// `registerContentScripts` function, resulting in faster execution without adding a <script> tag to the DOM.
// FireFox, on the other hand, still requires the "inject" script to be injected using a <script> tag with a src
// attribute.
//
// In summary, the <script> tag is used to pass the payload to the "inject" script in all browsers. Chromium-based
// browsers read the payload directly from the <script> tag properties, while FireFox executes the "inject" script
// using the <script> tag in the DOM.

injectAndExecuteScript(payload)
})
} catch (err) {
Expand Down
73 changes: 49 additions & 24 deletions src/entrypoints/content/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,61 @@
import type { ContentScriptPayload } from '~/shared/types'

/** Extracts the payload from the script tag. */
const extractPayload = (): ContentScriptPayload | undefined => {
const injectedScript = document.getElementById(__UNIQUE_INJECT_FILENAME__) as HTMLScriptElement | null
if (!injectedScript) {
return // no script to execute = no fun
}

try {
const rawPayload = injectedScript.getAttribute('payload')
if (!rawPayload) {
return
}
const extractPayload = async (
timeout: number = 2000,
interval: number = 1
): Promise<ContentScriptPayload | undefined> => {
const startedAt = Date.now()

return new Promise((resolve: (_: Awaited<ReturnType<typeof extractPayload>>) => void): void => {
const t = setInterval(() => {
// if the timeout is reached, resolve with `undefined`
if (Date.now() - startedAt > timeout) {
clearInterval(t)

return resolve(undefined)
}

try {
return JSON.parse(rawPayload)
} catch (_) {
// do nothing
}
} finally {
injectedScript.remove()
}
const injectedScript = document.getElementById(__UNIQUE_INJECT_FILENAME__) as HTMLScriptElement | null
if (!injectedScript) {
// no script tag with the specified ID = the script was removed by the previous run of this function OR
// the script was not injected yet by the content script
return // try again later
}

try {
// read the payload from the script tag attribute
const rawPayload = injectedScript.getAttribute('payload')
if (!rawPayload) {
return // try again later
}

// since script tag was found and the payload is not empty, we can clear the interval immediately
clearInterval(t)

try {
// parse and resolve the payload
return resolve(JSON.parse(rawPayload))
} catch (_) {
// do nothing
}
} finally {
injectedScript.remove() // remove the script tag
}
}, interval)
})
}

/** Run the script */
try {
;(() => {
console.log(`%c👻 RUA: Injected script is running`, 'font-weight:bold')

const payload = extractPayload()
;(async () => {
const payload = await extractPayload()
if (!payload) {
return // no payload = no fun
// no payload = no fun
//
// probably, the script was removed by the content script, registered with the `world: 'MAIN'` property
// (Chromium-based browsers only)
return
}

/** @link https://developer.mozilla.org/en-US/docs/Web/API/Navigator */
Expand Down

0 comments on commit 6ae1b13

Please sign in to comment.