Add periodic user agent string updates

This commit is contained in:
hensm
2025-12-27 02:13:57 +00:00
parent 50851b4831
commit 1f200bef9a
7 changed files with 131 additions and 27 deletions

9
docs/ua.json Normal file
View File

@@ -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"
}
}

22
docs/ua.schema.json Normal file
View File

@@ -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
}

View File

@@ -12,6 +12,7 @@ import deviceManager from "./deviceManager";
import { initAction } from "./action"; import { initAction } from "./action";
import { initMenus } from "./menus"; import { initMenus } from "./menus";
import { initWhitelist } from "./whitelist"; import { initWhitelist } from "./whitelist";
import { cacheUaInfo, getChromeUserAgentString } from "../lib/userAgents";
const _ = browser.i18n.getMessage; const _ = browser.i18n.getMessage;
@@ -143,5 +144,6 @@ async function init() {
}); });
} }
cacheUaInfo();
cacheBaseConfig(); cacheBaseConfig();
init(); init();

View File

@@ -1,7 +1,7 @@
import logger from "../lib/logger"; import logger from "../lib/logger";
import options from "../lib/options"; import options from "../lib/options";
import { getChromeUserAgent } from "../lib/userAgents"; import { cacheUaInfo, getChromeUserAgentString } from "../lib/userAgents";
import { RemoteMatchPattern } from "../lib/matchPattern"; import { RemoteMatchPattern } from "../lib/matchPattern";
import { import {
@@ -41,11 +41,17 @@ let customUserAgent: string | undefined;
export async function initWhitelist() { export async function initWhitelist() {
logger.info("init (whitelist)"); logger.info("init (whitelist)");
await cacheUaInfo();
if (!platform) { if (!platform) {
const browserInfo = await browser.runtime.getBrowserInfo();
// TODO: Allow hybrid UA to be configurable // TODO: Allow hybrid UA to be configurable
platform = (await browser.runtime.getPlatformInfo()).os; platform = (await browser.runtime.getPlatformInfo()).os;
chromeUserAgent = getChromeUserAgent(platform); chromeUserAgent = await getChromeUserAgentString(platform);
chromeUserAgentHybrid = getChromeUserAgent(platform, true); chromeUserAgentHybrid = await getChromeUserAgentString(platform, {
hybridFirefoxVersion: browserInfo.version
});
if (!chromeUserAgent) { if (!chromeUserAgent) {
throw logger.error("Failed to get Chrome UA string"); throw logger.error("Failed to get Chrome UA string");
} }

View File

@@ -106,4 +106,4 @@ export default {
siteWhitelistCustomUserAgent: "", siteWhitelistCustomUserAgent: "",
showAdvancedOptions: false showAdvancedOptions: false
} as Options; } satisfies Options as Options;

View File

@@ -1,25 +1,90 @@
const PLATFORM_MAC = "Macintosh; Intel Mac OS X 12_5"; import logger from "./logger";
const PLATFORM_MAC_HYBRID = "Macintosh; Intel Mac OS X 12_5; rv:103.0"; import { TypedStorageArea } from "./TypedStorageArea";
const PLATFORM_WIN = "Windows NT 10.0; Win64; x64";
const PLATFORM_LINUX = "X11; Linux x86_64";
const UA_CHROME = // Bundle UA info at time of build (as a fallback)
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"; import bundledUaInfo from "../../../docs/ua.json";
const UA_HYBRID = "Chrome/104.0.0.0 Gecko/20100101 Firefox/103.0";
export function getChromeUserAgent(platform: string, hybrid = false) { const UA_INFO_ENDPOINT = "https://hensm.github.io/fx_cast/ua.json";
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 browserComponent = hybrid ? UA_HYBRID : UA_CHROME; interface UaInfo {
version: 1;
return `Mozilla/5.0 (${platformComponent}) ${browserComponent}`; platforms: {
[key: string]: string;
};
}
const uaInfoStorage = new TypedStorageArea<{
uaInfo: UaInfo | undefined;
uaInfoUpdated: number | undefined;
}>(browser.storage.local);
async function fetchUaInfo(): Promise<UaInfo | null> {
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;
}
}
} }

View File

@@ -10,7 +10,7 @@
import options, { Options } from "../../lib/options"; import options, { Options } from "../../lib/options";
import defaultOptions from "../../defaultOptions"; import defaultOptions from "../../defaultOptions";
import { getChromeUserAgent } from "../../lib/userAgents"; import { getChromeUserAgentString } from "../../lib/userAgents";
const _ = browser.i18n.getMessage; const _ = browser.i18n.getMessage;
@@ -23,7 +23,7 @@
let opts: Options | undefined; let opts: Options | undefined;
onMount(async () => { onMount(async () => {
const platform = (await browser.runtime.getPlatformInfo()).os; const platform = (await browser.runtime.getPlatformInfo()).os;
defaultUserAgent = getChromeUserAgent(platform); defaultUserAgent = await getChromeUserAgentString(platform);
opts = await options.getAll(); opts = await options.getAll();
options.addEventListener("changed", async () => { options.addEventListener("changed", async () => {