From c5b2292caa058c286df80cb2a38a3ee08bb80c33 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Tue, 24 Feb 2026 14:50:03 +0900 Subject: [PATCH] =?UTF-8?q?feat(stealth):=20=E5=A2=9E=E5=BC=BA=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E7=BA=A7=20UA=20=E8=A6=86=E7=9B=96=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=83=8C=E6=99=AF=E7=89=B9=E5=BE=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/browser.ts | 5 ++ src/stealth.ts | 132 ++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 109 insertions(+), 28 deletions(-) diff --git a/src/browser.ts b/src/browser.ts index e7ff4e3..82e2239 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -30,6 +30,7 @@ import { import { STEALTH_CHROMIUM_ARGS, applyStealthScripts, + applyBrowserLevelStealth, type StealthScriptOptions, } from './stealth.js'; @@ -1576,6 +1577,10 @@ export class BrowserManager { }); this.cdpEndpoint = null; + if (stealthPolicy.enabled && browserType === 'chromium') { + await applyBrowserLevelStealth(this.browser); + } + if (!options.userAgent && stealthPolicy.enabled && browserType === 'chromium') { const runtimeVersion = this.extractChromiumVersion(this.browser.version()); if (runtimeVersion) { diff --git a/src/stealth.ts b/src/stealth.ts index 87cb05d..7c041cb 100644 --- a/src/stealth.ts +++ b/src/stealth.ts @@ -6,7 +6,7 @@ * Puppeteer / headless Chrome. */ -import type { BrowserContext, Page } from 'playwright-core'; +import type { Browser, BrowserContext, Page } from 'playwright-core'; export interface StealthScriptOptions { locale?: string; @@ -16,7 +16,11 @@ export interface StealthScriptOptions { * Chromium args that reduce automation fingerprint. * Intended to be merged into the user-supplied args array at launch time. */ -export const STEALTH_CHROMIUM_ARGS: string[] = ['--disable-blink-features=AutomationControlled']; +export const STEALTH_CHROMIUM_ARGS: string[] = [ + '--disable-blink-features=AutomationControlled', + '--use-gl=angle', + '--use-angle=default', +]; /** * Apply all stealth patches to a BrowserContext. @@ -36,49 +40,103 @@ export async function applyStealthScripts( context.on('page', (page: Page) => applyCDPStealthToPage(page)); } +/** + * Apply browser-level CDP overrides that affect all targets (including Workers). + * Call this right after browser.launch() and before creating pages. + */ +export async function applyBrowserLevelStealth(browser: Browser): Promise { + try { + const cdp = await (browser as any).newBrowserCDPSession(); + const version = await cdp.send('Browser.getVersion'); + const rawUA = version?.userAgent ?? ''; + if (!rawUA.includes('HeadlessChrome')) { + await cdp.detach(); + return; + } + const patchedUA = rawUA.replace(/HeadlessChrome/g, 'Chrome'); + const metadata = buildUserAgentMetadata(patchedUA); + + // Override on all existing targets + const { targetInfos } = await cdp.send('Target.getTargets'); + for (const target of targetInfos) { + try { + const { sessionId } = await cdp.send('Target.attachToTarget', { + targetId: target.targetId, + flatten: true, + }); + await cdp.send('Emulation.setUserAgentOverride', { + userAgent: patchedUA, + acceptLanguage: 'en-US,en;q=0.9', + platform: getPlatformString(), + userAgentMetadata: metadata, + }); + await cdp.send('Target.detachFromTarget', { sessionId }).catch(() => {}); + } catch { + // Some targets don't support Emulation domain + } + } + await cdp.detach(); + } catch { + // newBrowserCDPSession not available -- silently skip + } +} + async function applyCDPStealthToPage(page: Page): Promise { try { const cdp = await page.context().newCDPSession(page); const ua = await cdp.send('Browser.getVersion').catch(() => null); const rawUA = ua?.userAgent ?? ''; const patchedUA = rawUA.replace(/HeadlessChrome/g, 'Chrome'); - - const versionMatch = patchedUA.match(/Chrome\/(\d+)/); - const majorVersion = versionMatch?.[1] ?? '120'; - const fullVersionMatch = patchedUA.match(/Chrome\/([\d.]+)/); - const fullVersion = fullVersionMatch?.[1] ?? `${majorVersion}.0.0.0`; + const metadata = buildUserAgentMetadata(patchedUA); await cdp.send('Emulation.setUserAgentOverride', { userAgent: patchedUA, acceptLanguage: 'en-US,en;q=0.9', platform: getPlatformString(), - userAgentMetadata: { - brands: [ - { brand: 'Chromium', version: majorVersion }, - { brand: 'Google Chrome', version: majorVersion }, - { brand: 'Not-A.Brand', version: '99' }, - ], - fullVersionList: [ - { brand: 'Chromium', version: fullVersion }, - { brand: 'Google Chrome', version: fullVersion }, - { brand: 'Not-A.Brand', version: '99.0.0.0' }, - ], - fullVersion: fullVersion, - platform: getPlatformHint(), - platformVersion: getPlatformVersionHint(), - architecture: 'x86', - model: '', - mobile: false, - bitness: '64', - wow64: false, - }, + userAgentMetadata: metadata, }); - await cdp.detach(); + + // Set default background color to opaque white (headless default is transparent) + await cdp + .send('Emulation.setDefaultBackgroundColorOverride', { + color: { r: 255, g: 255, b: 255, a: 1 }, + }) + .catch(() => {}); + + // Keep CDP session alive so the override persists for Workers } catch { // CDP not available (non-Chromium) -- silently skip } } +function buildUserAgentMetadata(patchedUA: string): any { + const versionMatch = patchedUA.match(/Chrome\/(\d+)/); + const majorVersion = versionMatch?.[1] ?? '120'; + const fullVersionMatch = patchedUA.match(/Chrome\/([\d.]+)/); + const fullVersion = fullVersionMatch?.[1] ?? `${majorVersion}.0.0.0`; + + return { + brands: [ + { brand: 'Chromium', version: majorVersion }, + { brand: 'Google Chrome', version: majorVersion }, + { brand: 'Not-A.Brand', version: '99' }, + ], + fullVersionList: [ + { brand: 'Chromium', version: fullVersion }, + { brand: 'Google Chrome', version: fullVersion }, + { brand: 'Not-A.Brand', version: '99.0.0.0' }, + ], + fullVersion: fullVersion, + platform: getPlatformHint(), + platformVersion: getPlatformVersionHint(), + architecture: 'x86', + model: '', + mobile: false, + bitness: '64', + wow64: false, + }; +} + function getPlatformString(): string { if (typeof process !== 'undefined') { if (process.platform === 'darwin') return 'macOS'; @@ -153,6 +211,7 @@ function buildStealthScript(options: StealthScriptOptions): string { patchUserAgentData(), patchUserAgent(), patchPerformanceMemory(), + patchDefaultBackgroundColor(), ].join('\n'); } @@ -924,3 +983,20 @@ function patchPerformanceMemory(): string { } })();`; } + +/** + * Headless Chrome has a transparent default background (rgba(0,0,0,0)). + * Real browsers have an opaque white background. Set it early to avoid + * the "hasKnownBgColor" detection. + */ +function patchDefaultBackgroundColor(): string { + return `(function(){ + if (document.documentElement) { + const style = getComputedStyle(document.documentElement); + const bg = style.backgroundColor; + if (!bg || bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent') { + document.documentElement.style.backgroundColor = 'rgb(255, 255, 255)'; + } + } +})();`; +}