diff --git a/docs/ua.json b/docs/ua.json new file mode 100644 index 0000000..4533961 --- /dev/null +++ b/docs/ua.json @@ -0,0 +1,9 @@ +{ + "$schema": "./ua.schema.json", + "version": 1, + "platforms": { + "win": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36", + "mac": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36", + "linux": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" + } +} diff --git a/docs/ua.schema.json b/docs/ua.schema.json new file mode 100644 index 0000000..9ba6f60 --- /dev/null +++ b/docs/ua.schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "ua_string": { "type": "string", "minLength": 1 } + }, + "properties": { + "$schema": { "type": "string" }, + "version": { "type": "integer", "const": 1 }, + "platforms": { + "type": "object", + "properties": { + "win": { "$ref": "#/$defs/ua_string" }, + "mac": { "$ref": "#/$defs/ua_string" }, + "linux": { "$ref": "#/$defs/ua_string" } + }, + "required": ["win", "mac", "linux"], + "additionalProperties": false + } + }, + "required": ["version", "platforms"], + "additionalProperties": false +} diff --git a/extension/src/background/background.ts b/extension/src/background/background.ts index a01283a..98edf47 100755 --- a/extension/src/background/background.ts +++ b/extension/src/background/background.ts @@ -12,6 +12,7 @@ import deviceManager from "./deviceManager"; import { initAction } from "./action"; import { initMenus } from "./menus"; import { initWhitelist } from "./whitelist"; +import { cacheUaInfo, getChromeUserAgentString } from "../lib/userAgents"; const _ = browser.i18n.getMessage; @@ -143,5 +144,6 @@ async function init() { }); } +cacheUaInfo(); cacheBaseConfig(); init(); diff --git a/extension/src/background/whitelist.ts b/extension/src/background/whitelist.ts index aa89041..33f3ad1 100644 --- a/extension/src/background/whitelist.ts +++ b/extension/src/background/whitelist.ts @@ -1,7 +1,7 @@ import logger from "../lib/logger"; import options from "../lib/options"; -import { getChromeUserAgent } from "../lib/userAgents"; +import { cacheUaInfo, getChromeUserAgentString } from "../lib/userAgents"; import { RemoteMatchPattern } from "../lib/matchPattern"; import { @@ -41,11 +41,17 @@ let customUserAgent: string | undefined; export async function initWhitelist() { logger.info("init (whitelist)"); + await cacheUaInfo(); + if (!platform) { + const browserInfo = await browser.runtime.getBrowserInfo(); + // TODO: Allow hybrid UA to be configurable platform = (await browser.runtime.getPlatformInfo()).os; - chromeUserAgent = getChromeUserAgent(platform); - chromeUserAgentHybrid = getChromeUserAgent(platform, true); + chromeUserAgent = await getChromeUserAgentString(platform); + chromeUserAgentHybrid = await getChromeUserAgentString(platform, { + hybridFirefoxVersion: browserInfo.version + }); if (!chromeUserAgent) { throw logger.error("Failed to get Chrome UA string"); } diff --git a/extension/src/defaultOptions.ts b/extension/src/defaultOptions.ts index 5e09007..c8d0aad 100644 --- a/extension/src/defaultOptions.ts +++ b/extension/src/defaultOptions.ts @@ -106,4 +106,4 @@ export default { siteWhitelistCustomUserAgent: "", showAdvancedOptions: false -} as Options; +} satisfies Options as Options; diff --git a/extension/src/lib/userAgents.ts b/extension/src/lib/userAgents.ts index 4288b60..308a8a6 100644 --- a/extension/src/lib/userAgents.ts +++ b/extension/src/lib/userAgents.ts @@ -1,25 +1,90 @@ -const PLATFORM_MAC = "Macintosh; Intel Mac OS X 12_5"; -const PLATFORM_MAC_HYBRID = "Macintosh; Intel Mac OS X 12_5; rv:103.0"; -const PLATFORM_WIN = "Windows NT 10.0; Win64; x64"; -const PLATFORM_LINUX = "X11; Linux x86_64"; +import logger from "./logger"; +import { TypedStorageArea } from "./TypedStorageArea"; -const UA_CHROME = - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"; -const UA_HYBRID = "Chrome/104.0.0.0 Gecko/20100101 Firefox/103.0"; +// Bundle UA info at time of build (as a fallback) +import bundledUaInfo from "../../../docs/ua.json"; -export function getChromeUserAgent(platform: string, hybrid = false) { - let platformComponent: string; - if (platform === "mac") { - platformComponent = hybrid ? PLATFORM_MAC_HYBRID : PLATFORM_MAC; - } else if (platform === "win") { - platformComponent = PLATFORM_WIN; - } else if (platform === "linux") { - platformComponent = PLATFORM_LINUX; - } else { - return; - } +const UA_INFO_ENDPOINT = "https://hensm.github.io/fx_cast/ua.json"; - const browserComponent = hybrid ? UA_HYBRID : UA_CHROME; - - return `Mozilla/5.0 (${platformComponent}) ${browserComponent}`; +interface UaInfo { + version: 1; + platforms: { + [key: string]: string; + }; +} + +const uaInfoStorage = new TypedStorageArea<{ + uaInfo: UaInfo | undefined; + uaInfoUpdated: number | undefined; +}>(browser.storage.local); + +async function fetchUaInfo(): Promise { + try { + const res: UaInfo = await fetch(UA_INFO_ENDPOINT).then(r => r.json()); + if (typeof res.version !== "number" || res.version !== 1) { + throw logger.error( + `UA info version mismatch (got ${res.version}, expected 1)` + ); + } else if ( + typeof res.platforms !== "object" || + res.platforms === null + ) { + throw logger.error("UA info platforms missing or invalid"); + } + return res; + } catch (err) { + logger.error("Failed to fetch UA info:", err); + return null; + } +} + +export async function cacheUaInfo() { + const { uaInfoUpdated } = await uaInfoStorage.get("uaInfoUpdated"); + + // If never updated or updated more than 24 hours ago + if (!uaInfoUpdated || (Date.now() - uaInfoUpdated) / 1000 >= 86400) { + logger.info("Fetching updated Chrome User-Agent strings..."); + const uaInfo = await fetchUaInfo(); + if (uaInfo) { + await uaInfoStorage.set({ uaInfo, uaInfoUpdated: Date.now() }); + } + } +} + +/** Get a chrome user */ +export async function getChromeUserAgentString( + platform: string, + opts?: { hybridFirefoxVersion?: string } +) { + const { uaInfo } = await uaInfoStorage.get("uaInfo"); + if (!uaInfo) { + logger.warn("UA info not cached (using bundled version)"); + } + const maybeBundledUaInfo = uaInfo ?? (bundledUaInfo as UaInfo); + const userAgentString = maybeBundledUaInfo.platforms[platform]; + if (userAgentString) { + // Return hybrid UA (mostly Firefox UA) if requested + if (opts?.hybridFirefoxVersion) { + // Sans-patch (e.g. "145.0.1" -> "145.0") + const firefoxMajorMinorVersionMatch = + opts.hybridFirefoxVersion.match(/^\d+\.\d+/); + const componentMatch = userAgentString.match( + /Mozilla\/5.0 \(([^\(]+)\) (.+)/ + ); + if (firefoxMajorMinorVersionMatch && componentMatch) { + const [rv] = firefoxMajorMinorVersionMatch; + const [, platformComponent, browserComponent] = componentMatch; + // Extract Chrome/xxx part + const chromeVersionMatch = browserComponent.match( + /(?<= )Chrome\/[^ ]+(?= )/ + ); + if (chromeVersionMatch) { + const [chromeVersion] = chromeVersionMatch; + return `Mozilla/5.0 (${platformComponent}; rv:${rv}) ${chromeVersion} Gecko/20100101 Firefox/${rv}`; + } + } + } else { + return userAgentString; + } + } } diff --git a/extension/src/ui/options/Options.svelte b/extension/src/ui/options/Options.svelte index ae58e9e..4ff70ee 100644 --- a/extension/src/ui/options/Options.svelte +++ b/extension/src/ui/options/Options.svelte @@ -10,7 +10,7 @@ import options, { Options } from "../../lib/options"; import defaultOptions from "../../defaultOptions"; - import { getChromeUserAgent } from "../../lib/userAgents"; + import { getChromeUserAgentString } from "../../lib/userAgents"; const _ = browser.i18n.getMessage; @@ -23,7 +23,7 @@ let opts: Options | undefined; onMount(async () => { const platform = (await browser.runtime.getPlatformInfo()).os; - defaultUserAgent = getChromeUserAgent(platform); + defaultUserAgent = await getChromeUserAgentString(platform); opts = await options.getAll(); options.addEventListener("changed", async () => {