feat: add stealth modules for chrome runtime, screen, and permissions (Task 12)

- chrome-runtime.ts: injects window.chrome object with runtime, loadTimes, csi, app
- screen.ts: overrides screen.width/height/avail/colorDepth, window.outerWidth/Height
- permissions.ts: hooks navigator.permissions.query() for 4 permission types
- tests: 15 integration tests (6 chrome, 6 screen, 5 permissions), skip when VISIONL_INTEGRATION != '1'
- typecheck: passed (0 errors)
This commit is contained in:
2026-08-12 21:08:13 +08:00
parent 5cef8cdbc4
commit f0fa14f898
4 changed files with 448 additions and 0 deletions
@@ -0,0 +1,290 @@
// Integration tests for chrome-runtime, screen, and permissions stealth modules / chrome-runtime、screen、permissions 隐身模块集成测试
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium } from 'playwright-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import type { Browser, BrowserContext } from 'playwright';
import type { FingerprintProfile } from '@visionl/core';
import { injectChromeRuntime } from '../stealth/chrome-runtime.js';
import { injectScreenStealth } from '../stealth/screen.js';
import { injectPermissionsStealth } from '../stealth/permissions.js';
chromium.use(StealthPlugin());
const integration = process.env.VISIONL_INTEGRATION === '1' ? describe : describe.skip;
const testProfile: FingerprintProfile = {
id: 'fp_stealth_chrome_screen',
name: 'Stealth Chrome+Screen+Permissions 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 },
};
// ==================== chrome-runtime / Chrome 运行时注入测试 ====================
integration('injectChromeRuntime', () => {
let browser: Browser;
let context: BrowserContext;
beforeAll(async () => {
browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
});
afterAll(async () => {
if (context) await context.close();
if (browser) await browser.close();
});
it('should set typeof window.chrome to "object"', async () => {
context = await browser.newContext();
await injectChromeRuntime(context);
const page = await context.newPage();
await page.goto('about:blank');
const result = await page.evaluate(() => typeof (window as any).chrome);
expect(result).toBe('object');
});
it('should have chrome.runtime defined', async () => {
context = await browser.newContext();
await injectChromeRuntime(context);
const page = await context.newPage();
await page.goto('about:blank');
const hasRuntime = await page.evaluate(() => !!(window as any).chrome.runtime);
expect(hasRuntime).toBe(true);
});
it('should have chrome.loadTimes defined as function', async () => {
context = await browser.newContext();
await injectChromeRuntime(context);
const page = await context.newPage();
await page.goto('about:blank');
const loadTimesType = await page.evaluate(() => typeof (window as any).chrome.loadTimes);
expect(loadTimesType).toBe('function');
});
it('should have chrome.csi defined as function', async () => {
context = await browser.newContext();
await injectChromeRuntime(context);
const page = await context.newPage();
await page.goto('about:blank');
const csiType = await page.evaluate(() => typeof (window as any).chrome.csi);
expect(csiType).toBe('function');
});
it('should have chrome.app defined as object', async () => {
context = await browser.newContext();
await injectChromeRuntime(context);
const page = await context.newPage();
await page.goto('about:blank');
const appType = await page.evaluate(() => typeof (window as any).chrome.app);
expect(appType).toBe('object');
});
it('should have chrome.app.isInstalled === false', async () => {
context = await browser.newContext();
await injectChromeRuntime(context);
const page = await context.newPage();
await page.goto('about:blank');
const isInstalled = await page.evaluate(() => (window as any).chrome.app.isInstalled);
expect(isInstalled).toBe(false);
});
});
// ==================== screen / 屏幕属性隐身测试 ====================
integration('injectScreenStealth', () => {
let browser: Browser;
let context: BrowserContext;
beforeAll(async () => {
browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
});
afterAll(async () => {
if (context) await context.close();
if (browser) await browser.close();
});
it('should set screen.width from profile', async () => {
context = await browser.newContext({ viewport: testProfile.viewport });
await injectScreenStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const width = await page.evaluate(() => screen.width);
expect(width).toBe(testProfile.screen.width);
});
it('should set screen.height from profile', async () => {
context = await browser.newContext({ viewport: testProfile.viewport });
await injectScreenStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const height = await page.evaluate(() => screen.height);
expect(height).toBe(testProfile.screen.height);
});
it('should set screen.availHeight < screen.height (taskbar subtracted)', async () => {
context = await browser.newContext({ viewport: testProfile.viewport });
await injectScreenStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const { height, availHeight } = await page.evaluate(() => ({
height: screen.height,
availHeight: screen.availHeight,
}));
expect(availHeight).toBeLessThan(height);
});
it('should set screen.colorDepth from profile', async () => {
context = await browser.newContext({ viewport: testProfile.viewport });
await injectScreenStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const colorDepth = await page.evaluate(() => screen.colorDepth);
expect(colorDepth).toBe(testProfile.screen.colorDepth);
});
it('should set window.outerWidth > window.innerWidth (decorations)', async () => {
context = await browser.newContext({ viewport: testProfile.viewport });
await injectScreenStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const { outer, inner } = await page.evaluate(() => ({
outer: window.outerWidth,
inner: window.innerWidth,
}));
expect(outer).toBeGreaterThan(inner);
});
it('should set window.outerHeight > window.innerHeight (decorations)', async () => {
context = await browser.newContext({ viewport: testProfile.viewport });
await injectScreenStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const { outer, inner } = await page.evaluate(() => ({
outer: window.outerHeight,
inner: window.innerHeight,
}));
expect(outer).toBeGreaterThan(inner);
});
});
// ==================== permissions / 权限查询隐身测试 ====================
integration('injectPermissionsStealth', () => {
let browser: Browser;
let context: BrowserContext;
beforeAll(async () => {
browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
});
afterAll(async () => {
if (context) await context.close();
if (browser) await browser.close();
});
it('should override notifications permission from profile', async () => {
context = await browser.newContext();
await injectPermissionsStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const status = await page.evaluate(async () => {
const result = await navigator.permissions.query({ name: 'notifications' });
return result.state;
});
expect(status).toBe(testProfile.permissions.notifications);
});
it('should override geolocation permission from profile', async () => {
context = await browser.newContext();
await injectPermissionsStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const status = await page.evaluate(async () => {
const result = await navigator.permissions.query({ name: 'geolocation' });
return result.state;
});
expect(status).toBe(testProfile.permissions.geolocation);
});
it('should override camera permission from profile', async () => {
context = await browser.newContext();
await injectPermissionsStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const status = await page.evaluate(async () => {
const result = await navigator.permissions.query({ name: 'camera' });
return result.state;
});
expect(status).toBe(testProfile.permissions.camera);
});
it('should override microphone permission from profile', async () => {
context = await browser.newContext();
await injectPermissionsStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const status = await page.evaluate(async () => {
const result = await navigator.permissions.query({ name: 'microphone' });
return result.state;
});
expect(status).toBe(testProfile.permissions.microphone);
});
it('should not intercept unknown permission names', async () => {
context = await browser.newContext();
await injectPermissionsStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const shouldThrow = await page.evaluate(async () => {
try {
await navigator.permissions.query({ name: 'unknown-perm' as any });
return false;
} catch {
return true;
}
});
expect(shouldThrow).toBe(true);
});
});
@@ -0,0 +1,66 @@
// Stealth — inject window.chrome object for headless detection avoidance / 隐身 — 注入 window.chrome 对象以规避无头检测
import type { BrowserContext } from 'playwright';
export async function injectChromeRuntime(context: BrowserContext): Promise<void> {
await context.addInitScript(() => {
// Headless Chrome has typeof window.chrome === 'undefined' — inject it / 无头模式下缺少 chrome 对象,注入以伪装真实 Chrome
const win = window as any;
if (typeof win.chrome === 'undefined') {
win.chrome = {};
}
if (!win.chrome.runtime) {
win.chrome.runtime = {
PlatformOs: { mac: 'mac', win: 'win', android: 'android', cros: 'cros', linux: 'linux', openbsd: 'openbsd', fuchsia: 'fuchsia' },
PlatformArch: { arm: 'arm', arm64: 'arm64', x86_32: 'x86-32', x86_64: 'x86-64', mips: 'mips', mips64: 'mips64' },
PlatformNaclArch: { arm: 'arm', x86_32: 'x86-32', x86_64: 'x86-64', mips: 'mips', mips64: 'mips64' },
RequestUpdateCheckStatus: { throttled: 'throttled', no_update: 'no_update', update_available: 'update_available' },
OnInstalledReason: { install: 'install', update: 'update', chrome_update: 'chrome_update', shared_module_update: 'shared_module_update' },
OnRestartRequiredReason: { app_update: 'app_update', os_update: 'os_update', periodic: 'periodic' },
id: void 0,
getManifest(): object { return { version: '0.0.0', name: '', manifest_version: 3 }; },
getURL(path: string): string { return `chrome-extension://invalid/${path}`; },
lastError: void 0,
};
}
if (!win.chrome.loadTimes) {
win.chrome.loadTimes = () => ({
requestTime: Date.now() / 1000,
startLoadTime: Date.now() / 1000,
commitLoadTime: Date.now() / 1000,
finishDocumentLoadTime: Date.now() / 1000,
finishLoadTime: Date.now() / 1000,
firstPaintTime: Date.now() / 1000,
firstPaintAfterLoadTime: 0,
navigationType: 'Other',
wasFetchedViaSpdy: true,
wasNpnNegotiated: true,
npnNegotiatedProtocol: 'http/1.1',
wasAlternateProtocolAvailable: false,
connectionInfo: 'http/1.1',
});
}
if (!win.chrome.csi) {
win.chrome.csi = () => ({
startE: 0,
onloadT: 0,
pageT: 0,
tran: 15,
});
}
if (!win.chrome.app) {
win.chrome.app = {
isInstalled: false,
InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
getDetails(): Array<never> { return []; },
getIsInstalled(): boolean { return false; },
runningState(): string { return 'cannot_run'; },
};
}
});
}
@@ -0,0 +1,45 @@
// Stealth — override navigator.permissions.query() at JS level / 隐身 — 在 JS 层面覆盖 navigator.permissions.query()
import type { BrowserContext } from 'playwright';
import type { FingerprintProfile, PermissionState } from '@visionl/core';
const WATCHED_NAMES = ['notifications', 'geolocation', 'camera', 'microphone'] as const;
export async function injectPermissionsStealth(
context: BrowserContext,
profile: FingerprintProfile,
): Promise<void> {
const perms: Record<string, PermissionState> = {};
for (const name of WATCHED_NAMES) {
perms[name] = profile.permissions[name];
}
await context.addInitScript((opts) => {
const permissionsNameSet = new Set<string>(opts.watchedNames);
const permissionsMap: Record<string, string> = opts.stateMap;
const origQuery = navigator.permissions.query.bind(navigator.permissions);
navigator.permissions.query = function (descriptor: PermissionDescriptor): Promise<PermissionStatus> {
const name = descriptor.name;
// Intercept watched permission names only / 仅拦截关注的权限名称
if (permissionsNameSet.has(name) && name in permissionsMap) {
return Promise.resolve({
name,
state: permissionsMap[name],
onchange: null,
addEventListener() {},
removeEventListener() {},
dispatchEvent(): boolean { return true; },
} as PermissionStatus);
}
// Pass through other queries / 透传其他权限查询
return origQuery(descriptor);
};
}, {
watchedNames: [...WATCHED_NAMES],
stateMap: perms,
});
}
+47
View File
@@ -0,0 +1,47 @@
// Stealth — override screen and window dimension properties / 隐身 — 覆盖屏幕和窗口尺寸属性
import type { BrowserContext } from 'playwright';
import type { FingerprintProfile } from '@visionl/core';
const TASKBAR_HEIGHT = 40; // typical taskbar height in px / 典型任务栏高度(像素)
export async function injectScreenStealth(
context: BrowserContext,
profile: FingerprintProfile,
): Promise<void> {
const { screen: screenProfile, viewport } = profile;
await context.addInitScript((opts) => {
// Override screen object / 覆盖 screen 对象
Object.defineProperty(screen, 'width', {
get: () => opts.screenWidth,
});
Object.defineProperty(screen, 'height', {
get: () => opts.screenHeight,
});
Object.defineProperty(screen, 'availWidth', {
get: () => opts.availWidth,
});
Object.defineProperty(screen, 'availHeight', {
get: () => opts.availHeight,
});
Object.defineProperty(screen, 'colorDepth', {
get: () => opts.colorDepth,
});
// Override window.outerWidth/Height > inner for window decorations / 覆盖 outerWidth/Height 使其 > inner 模拟窗口装饰
Object.defineProperty(window, 'outerWidth', {
get: () => opts.outerWidth,
});
Object.defineProperty(window, 'outerHeight', {
get: () => opts.outerHeight,
});
}, {
screenWidth: screenProfile.width,
screenHeight: screenProfile.height,
availWidth: screenProfile.width,
availHeight: screenProfile.height - TASKBAR_HEIGHT,
colorDepth: screenProfile.colorDepth,
outerWidth: viewport.width + 16, // window frame chrome / 窗口边框
outerHeight: viewport.height + 72, // title bar + window frame / 标题栏 + 窗口边框
});
}