diff --git a/main.js b/main.js index 14bfbb98..8284a457 100644 --- a/main.js +++ b/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;