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 {
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) {
+104 -28
View File
@@ -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<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> {
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)';
}
}
})();`;
}