fix(theseus): present a consistent stock-Chrome identity to bot filters
estore.asus.com (DataDome, "AI Threats Detection") served its block page to Theseus while a plain Chromium on the same connection got the product page. Three things in our identity were wrong: - The client-hint brand list was hand-written with "Google Chrome" first — a permutation real Chrome never sends. It is now computed the way Chromium does it (GREASE brand from the major version, per-major brand order). - The page-side navigator.userAgentData still said "Chromium" only, so headers and JS disagreed. The same metadata is now installed per tab via Emulation.setUserAgentOverride, so both sides match. - Accept-Language went out as "en-US,en;q=0.8;q=0.9": we appended a q-value and Chromium appended another. Chromium now gets a plain language list. That makes the identity self-consistent, but DataDome still blocks on the version: Chromium 130 (Electron 33) is a year old, and claiming Chrome 152 (THESEUS_CHROME_VERSION, added here for exactly this test) loads the page. The real fix is a current Electron; this commit removes the other tells.
This commit is contained in:
parent
3b29654954
commit
640e44a089
1 changed files with 74 additions and 9 deletions
83
main.js
83
main.js
|
|
@ -722,6 +722,23 @@ async function applyFingerprint(wc) {
|
|||
const tz = effTimezone();
|
||||
await wc.debugger.sendCommand("Emulation.setTimezoneOverride", { timezoneId: tz || "" });
|
||||
const loc = effLocale();
|
||||
// Identity: stock-Chrome UA plus matching client hints on BOTH sides —
|
||||
// the request headers and navigator.userAgentData — see chromeBrandMetadata.
|
||||
try {
|
||||
const meta = chromeBrandMetadata();
|
||||
const lang = loc || app.getLocale() || "en-US";
|
||||
await wc.debugger.sendCommand("Emulation.setUserAgentOverride", {
|
||||
userAgent: stockChromeUA() || session.defaultSession.getUserAgent(),
|
||||
acceptLanguage: acceptLanguageList(lang),
|
||||
platform: meta.navigatorPlatform,
|
||||
userAgentMetadata: {
|
||||
brands: meta.brands, fullVersionList: meta.fullVersionList,
|
||||
platform: meta.platform, platformVersion: meta.platformVersion,
|
||||
architecture: meta.architecture, model: meta.model, mobile: meta.mobile,
|
||||
bitness: meta.bitness, wow64: meta.wow64,
|
||||
},
|
||||
});
|
||||
} catch (e) { console.warn("userAgent override failed:", e?.message); }
|
||||
await wc.debugger.sendCommand("Emulation.setLocaleOverride", loc ? { locale: loc } : {});
|
||||
// setLocaleOverride covers Intl but NOT navigator.language(s) — inject a getter.
|
||||
await wc.debugger.sendCommand("Page.enable");
|
||||
|
|
@ -790,19 +807,70 @@ async function clearHistoryNow() {
|
|||
// without the theseus + Electron tokens returns 200. Brave / Vivaldi / Slack
|
||||
// (Electron) all do the same strip — a Chromium browser identifying itself
|
||||
// as vanilla Chrome is standard practice in the Electron ecosystem.
|
||||
// The Chrome version we claim. Normally the embedded Chromium's own; the
|
||||
// env override exists to test how sites react to a different version
|
||||
// (a year-old Chromium build is itself a bot signal to some filters).
|
||||
const CHROME_VERSION_CLAIMED = /^\d+(\.\d+){3}$/.test(process.env.THESEUS_CHROME_VERSION || "")
|
||||
? process.env.THESEUS_CHROME_VERSION : String(process.versions.chrome || "130.0.0.0");
|
||||
function stockChromeUA() {
|
||||
try {
|
||||
return session.defaultSession.getUserAgent()
|
||||
.replace(/ *theseus-navigator\/\S+/i, "")
|
||||
.replace(/ *Electron\/\S+/i, "");
|
||||
.replace(/ *Electron\/\S+/i, "")
|
||||
.replace(/Chrome\/\d+(\.\d+){3}/, "Chrome/" + CHROME_VERSION_CLAIMED);
|
||||
} catch { return null; }
|
||||
}
|
||||
// Client-hint identity of stock Google Chrome for the Chromium we embed —
|
||||
// computed the way Chromium itself does it, so it is indistinguishable from
|
||||
// the real thing:
|
||||
// - the GREASE brand ("Not?A_Brand";v="99" for 130) is derived from the
|
||||
// major version with Chromium's character/version tables;
|
||||
// - the order of the three brands is Chromium's per-major permutation
|
||||
// (130 → Chromium, Google Chrome, Not?A_Brand).
|
||||
// The previous hand-written list put "Google Chrome" first — a permutation
|
||||
// real Chrome 130 never sends — and the page-side navigator.userAgentData
|
||||
// still said "Chromium" only. DataDome ("AI Threats Detection") blocked on
|
||||
// exactly that header/JS mismatch (estore.asus.com, 2026-09-20), so the same
|
||||
// metadata is now also installed in every tab via Emulation.setUserAgentOverride.
|
||||
function chromeBrandMetadata() {
|
||||
const full = CHROME_VERSION_CLAIMED;
|
||||
const major = parseInt(full.split(".")[0], 10) || 130;
|
||||
const chars = [" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"];
|
||||
const greaseBrand = "Not" + chars[major % chars.length] + "A" + chars[(major + 1) % chars.length] + "Brand";
|
||||
const greaseVer = ["8", "99", "24"][major % 3];
|
||||
const base = [[greaseBrand, greaseVer, greaseVer + ".0.0.0"], ["Chromium", String(major), full], ["Google Chrome", String(major), full]];
|
||||
const orders = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]];
|
||||
const order = orders[major % 6];
|
||||
const out = [];
|
||||
base.forEach((b, i) => { out[order[i]] = b; });
|
||||
const platform = process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : "Linux";
|
||||
return {
|
||||
brands: out.map(([brand, version]) => ({ brand, version })),
|
||||
fullVersionList: out.map(([brand, , version]) => ({ brand, version })),
|
||||
headerBrands: out.map(([b, v]) => `"${b}";v="${v}"`).join(", "),
|
||||
headerFullList: out.map(([b, , v]) => `"${b}";v="${v}"`).join(", "),
|
||||
platform,
|
||||
platformVersion: process.platform === "win32" ? "10.0.0" : process.platform === "darwin" ? "14.0.0" : "6.0.0",
|
||||
navigatorPlatform: process.platform === "darwin" ? "MacIntel" : process.platform === "win32" ? "Win32" : "Linux x86_64",
|
||||
architecture: "x86", bitness: "64", model: "", mobile: false, wow64: false,
|
||||
};
|
||||
}
|
||||
// "en-US" → "en-US,en"; "de" → "de". Chromium turns the list into the header
|
||||
// with its own q-values (en-US,en;q=0.9), exactly like stock Chrome.
|
||||
function acceptLanguageList(loc) {
|
||||
const l = String(loc || "en-US");
|
||||
const base = l.split("-")[0];
|
||||
return base && base !== l ? `${l},${base}` : l;
|
||||
}
|
||||
// Accept-Language header follows the locale setting (session-wide, best effort).
|
||||
function applyAcceptLanguage() {
|
||||
const loc = effLocale() || app.getLocale() || "en-US";
|
||||
try {
|
||||
const ua = stockChromeUA() || session.defaultSession.getUserAgent();
|
||||
session.defaultSession.setUserAgent(ua, `${loc},${loc.split("-")[0]};q=0.8`);
|
||||
// A plain comma list: Chromium adds the q-values itself. Passing our own
|
||||
// ";q=0.8" produced "en-US,en;q=0.8;q=0.9" on the wire — malformed, and
|
||||
// one more thing that reads as "not a browser" to bot filters.
|
||||
session.defaultSession.setUserAgent(ua, acceptLanguageList(loc));
|
||||
} catch {}
|
||||
}
|
||||
// Client-hint headers (sec-ch-ua family) rewritten to look like stock Chrome.
|
||||
|
|
@ -825,13 +893,10 @@ function applyAcceptLanguage() {
|
|||
// the actual OS name so a Linux user still looks like a Linux user.
|
||||
function applyClientHintsSpoof() {
|
||||
try {
|
||||
const chromeVer = String(process.versions.chrome || "130");
|
||||
const major = chromeVer.split(".")[0] || "130";
|
||||
const brands = `"Google Chrome";v="${major}", "Chromium";v="${major}", "Not?A_Brand";v="99"`;
|
||||
const fullList = `"Google Chrome";v="${chromeVer}", "Chromium";v="${chromeVer}", "Not?A_Brand";v="99.0.0.0"`;
|
||||
const platform = process.platform === "darwin" ? '"macOS"'
|
||||
: process.platform === "win32" ? '"Windows"'
|
||||
: '"Linux"';
|
||||
const meta = chromeBrandMetadata();
|
||||
const brands = meta.headerBrands;
|
||||
const fullList = meta.headerFullList;
|
||||
const platform = `"${meta.platform}"`;
|
||||
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||
const h = details.requestHeaders || {};
|
||||
// Header names as Chromium sends them are typically kebab-case-lowercase;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue