feat(stealth): 增强浏览器级 UA 覆盖并修复背景特征

This commit is contained in:
leeguooooo
2026-02-24 14:50:03 +09:00
parent 2ed0c6f8ec
commit c5b2292caa
2 changed files with 109 additions and 28 deletions
+5
View File
@@ -30,6 +30,7 @@ import {
import { import {
STEALTH_CHROMIUM_ARGS, STEALTH_CHROMIUM_ARGS,
applyStealthScripts, applyStealthScripts,
applyBrowserLevelStealth,
type StealthScriptOptions, type StealthScriptOptions,
} from './stealth.js'; } from './stealth.js';
@@ -1576,6 +1577,10 @@ export class BrowserManager {
}); });
this.cdpEndpoint = null; this.cdpEndpoint = null;
if (stealthPolicy.enabled && browserType === 'chromium') {
await applyBrowserLevelStealth(this.browser);
}
if (!options.userAgent && stealthPolicy.enabled && browserType === 'chromium') { if (!options.userAgent && stealthPolicy.enabled && browserType === 'chromium') {
const runtimeVersion = this.extractChromiumVersion(this.browser.version()); const runtimeVersion = this.extractChromiumVersion(this.browser.version());
if (runtimeVersion) { if (runtimeVersion) {
+90 -14
View File
@@ -6,7 +6,7 @@
* Puppeteer / headless Chrome. * Puppeteer / headless Chrome.
*/ */
import type { BrowserContext, Page } from 'playwright-core'; import type { Browser, BrowserContext, Page } from 'playwright-core';
export interface StealthScriptOptions { export interface StealthScriptOptions {
locale?: string; locale?: string;
@@ -16,7 +16,11 @@ export interface StealthScriptOptions {
* Chromium args that reduce automation fingerprint. * Chromium args that reduce automation fingerprint.
* Intended to be merged into the user-supplied args array at launch time. * 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. * Apply all stealth patches to a BrowserContext.
@@ -36,23 +40,82 @@ export async function applyStealthScripts(
context.on('page', (page: Page) => applyCDPStealthToPage(page)); 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<void> {
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<void> { async function applyCDPStealthToPage(page: Page): Promise<void> {
try { try {
const cdp = await page.context().newCDPSession(page); const cdp = await page.context().newCDPSession(page);
const ua = await cdp.send('Browser.getVersion').catch(() => null); const ua = await cdp.send('Browser.getVersion').catch(() => null);
const rawUA = ua?.userAgent ?? ''; const rawUA = ua?.userAgent ?? '';
const patchedUA = rawUA.replace(/HeadlessChrome/g, 'Chrome'); const patchedUA = rawUA.replace(/HeadlessChrome/g, 'Chrome');
const metadata = buildUserAgentMetadata(patchedUA);
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`;
await cdp.send('Emulation.setUserAgentOverride', { await cdp.send('Emulation.setUserAgentOverride', {
userAgent: patchedUA, userAgent: patchedUA,
acceptLanguage: 'en-US,en;q=0.9', acceptLanguage: 'en-US,en;q=0.9',
platform: getPlatformString(), platform: getPlatformString(),
userAgentMetadata: { userAgentMetadata: metadata,
});
// 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: [ brands: [
{ brand: 'Chromium', version: majorVersion }, { brand: 'Chromium', version: majorVersion },
{ brand: 'Google Chrome', version: majorVersion }, { brand: 'Google Chrome', version: majorVersion },
@@ -71,12 +134,7 @@ async function applyCDPStealthToPage(page: Page): Promise<void> {
mobile: false, mobile: false,
bitness: '64', bitness: '64',
wow64: false, wow64: false,
}, };
});
await cdp.detach();
} catch {
// CDP not available (non-Chromium) -- silently skip
}
} }
function getPlatformString(): string { function getPlatformString(): string {
@@ -153,6 +211,7 @@ function buildStealthScript(options: StealthScriptOptions): string {
patchUserAgentData(), patchUserAgentData(),
patchUserAgent(), patchUserAgent(),
patchPerformanceMemory(), patchPerformanceMemory(),
patchDefaultBackgroundColor(),
].join('\n'); ].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)';
}
}
})();`;
}