feat(daemon): add HTTP header stealth injection (Task 14)

Inject Sec-CH-UA family headers on outgoing page requests:
- sec-ch-ua, sec-ch-ua-platform, sec-ch-ua-mobile
- sec-ch-ua-arch (from process.arch)
- sec-ch-ua-bitness ('64')
- sec-ch-ua-full-version (extracted from profile.userAgent)

Skips localhost/127.0.0.1 traffic (daemon internal) and WebSocket upgrades.
This commit is contained in:
2026-08-12 21:17:06 +08:00
parent 6085c9e65a
commit 78a35bafc4
2 changed files with 225 additions and 0 deletions
@@ -0,0 +1,171 @@
// Integration tests for injectHeaderStealth / 隐身请求头注入集成测试
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium } from 'playwright-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import http from 'http';
import os from 'os';
import type { AddressInfo } from 'net';
import type { Browser, BrowserContext, Page } from 'playwright';
import type { FingerprintProfile } from '@visionl/core';
import { injectHeaderStealth } from '../stealth/headers.js';
chromium.use(StealthPlugin());
const integration = process.env.VISIONL_INTEGRATION === '1' ? describe : describe.skip;
function getNonLoopbackIPv4(): string | null {
const nets = os.networkInterfaces();
for (const name of Object.keys(nets)) {
const iface = nets[name];
if (!iface) continue;
for (const net of iface) {
if (net.family === 'IPv4' && !net.internal) {
return net.address;
}
}
}
return null;
}
const testProfile: FingerprintProfile = {
id: 'fp_stealth_hdr',
name: 'Stealth Headers Test',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
platform: 'Win32',
languages: ['en-US', 'en'],
acceptLanguage: 'en-US,en;q=0.9',
screen: { width: 1920, height: 1080, colorDepth: 24, pixelRatio: 1 },
viewport: { width: 1280, height: 720 },
webgl: { vendor: 'Google Inc.', renderer: 'ANGLE (NVIDIA GeForce RTX 3060)' },
timezone: 'America/New_York',
permissions: {
notifications: 'denied',
geolocation: 'granted',
camera: 'denied',
microphone: 'denied',
},
behavior: {
mouseMoveDelay: { min: 50, max: 150 },
keyPressDelay: { min: 80, max: 200 },
scrollStepDelay: { min: 30, max: 100 },
},
canvasNoise: { enabled: true, strength: 0.5 },
};
integration('injectHeaderStealth', () => {
let browser: Browser;
let context: BrowserContext;
let page: Page;
let server: http.Server;
let capturedHeaders: Record<string, string | string[] | undefined> = {};
let serverUrl: string;
beforeAll(async () => {
server = http.createServer((req, res) => {
capturedHeaders = { ...req.headers };
if (req.url === '/check') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(capturedHeaders));
} else {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`<!DOCTYPE html><html><body>
<script>
fetch('/check')
.then(function(r) { return r.json(); })
.then(function(h) { document.title = JSON.stringify(h); })
.catch(function(e) { document.title = 'ERROR:' + e.message; });
</script>
</body></html>`);
}
});
await new Promise<void>((resolve) => {
// Bind to 0.0.0.0 so the browser can reach it from any non-loopback IP
server.listen(0, '0.0.0.0', resolve);
});
const port = (server.address() as AddressInfo).port;
// Find a non-loopback IP so that the URL does not start with http://127.0.0.1
// or http://localhost (those are explicitly skipped by injectHeaderStealth).
const nonLoopback = getNonLoopbackIPv4();
if (nonLoopback) {
serverUrl = `http://${nonLoopback}:${port}`;
} else {
serverUrl = `http://127.0.0.1:${port}`;
}
browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
});
afterAll(async () => {
if (page) await page.close();
if (context) await context.close();
if (browser) await browser.close();
await new Promise<void>((resolve) => server.close(() => resolve()));
});
it('should inject sec-ch-ua headers on outgoing requests', async () => {
context = await browser.newContext();
page = await context.newPage();
await injectHeaderStealth(page, testProfile);
// Navigate to our test page which fetches /check
await page.goto(serverUrl);
// Wait for the fetch to finish and the title to be updated
await page.waitForFunction(
() => document.title.startsWith('{') || document.title.startsWith('ERROR'),
{ timeout: 10_000 },
);
const title = await page.title();
if (title.startsWith('ERROR')) {
throw new Error(`Test page fetch failed: ${title}`);
}
const headers = JSON.parse(title) as Record<string, string | string[] | undefined>;
// sec-ch-ua should contain browser brand hints
const chUa = String(headers['sec-ch-ua'] ?? '');
expect(chUa).toContain('"Chromium"');
expect(chUa).toContain('"Google Chrome"');
expect(chUa).toContain('"Not?A_Brand"');
// sec-ch-ua-platform should match the profile platform ("Win32" → "Windows")
expect(headers['sec-ch-ua-platform']).toBe('"Windows"');
// sec-ch-ua-mobile should be ?0 (desktop)
expect(headers['sec-ch-ua-mobile']).toBe('?0');
// sec-ch-ua-arch should match process.arch
expect(headers['sec-ch-ua-arch']).toBe(process.arch);
// sec-ch-ua-bitness should be "64"
expect(headers['sec-ch-ua-bitness']).toBe('64');
// sec-ch-ua-full-version extracted from profile.userAgent Chrome/120.0.0.0
expect(headers['sec-ch-ua-full-version']).toBe('120.0.0.0');
});
it('should not inject headers on localhost traffic', async () => {
context = await browser.newContext();
page = await context.newPage();
await injectHeaderStealth(page, testProfile);
// Navigate directly to the check endpoint on localhost — should be bypassed
const localPort = (server.address() as AddressInfo).port;
await page.goto(`http://127.0.0.1:${localPort}/check`);
const body = await page.evaluate(() => document.body.textContent ?? '{}');
const headers = JSON.parse(body) as Record<string, string | string[] | undefined>;
// sec-ch-ua should NOT be injected on localhost
expect(headers['sec-ch-ua']).toBeUndefined();
});
});
+54
View File
@@ -0,0 +1,54 @@
// Stealth — HTTP request header injection / 隐身 — HTTP 请求头注入
// Injects Sec-CH-UA family headers to match a browser profile.
// 注入 Sec-CH-UA 系列请求头以匹配浏览器配置。
import type { Page } from 'playwright';
import type { FingerprintProfile } from '@visionl/core';
/** Extract Chrome major version from userAgent string / 从 userAgent 提取 Chrome 主版本号 */
function extractChromeVersion(ua: string): { major: string; full: string } {
const m = ua.match(/Chrome\/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
if (m) {
return {
major: m[1],
full: `${m[1]}.${m[2]}.${m[3]}.${m[4]}`,
};
}
return { major: '132', full: '132.0.6834.160' };
}
/** Resolve platform string for Sec-CH-UA-Platform / 根据 profile.platform 推导平台字符串 */
function resolvePlatform(platform: string): string {
if (platform.includes('Windows')) return 'Windows';
if (platform.includes('Mac')) return 'macOS';
return 'Linux';
}
export async function injectHeaderStealth(page: Page, profile: FingerprintProfile): Promise<void> {
const { major, full: fullVersion } = extractChromeVersion(profile.userAgent);
const platform = resolvePlatform(profile.platform);
const arch = process.arch; // aarch64 / x64 / ...
await page.route('**/*', (route) => {
const request = route.request();
const url = request.url();
// Skip WebSocket upgrades and daemon-internal traffic / 跳过 WebSocket 升级和守护进程内部流量
if (url.startsWith('ws://') || url.startsWith('wss://')) {
return route.continue();
}
// Do NOT intercept requests to localhost / 127.0.0.1 (health endpoint etc.)
if (url.startsWith('http://127.0.0.1') || url.startsWith('http://localhost')) {
return route.continue();
}
const headers = { ...request.headers() };
headers['sec-ch-ua'] = `"Chromium";v="${major}", "Google Chrome";v="${major}", "Not?A_Brand";v="99"`;
headers['sec-ch-ua-platform'] = `"${platform}"`;
headers['sec-ch-ua-mobile'] = '?0';
headers['sec-ch-ua-arch'] = arch;
headers['sec-ch-ua-bitness'] = '64';
headers['sec-ch-ua-full-version'] = fullVersion;
route.continue({ headers });
});
}