feat(daemon): add stealth navigator property injection (Task 11)

This commit is contained in:
2026-08-12 21:04:49 +08:00
parent 2ee59182bb
commit 5cef8cdbc4
2 changed files with 188 additions and 0 deletions
@@ -0,0 +1,136 @@
// Integration tests for injectNavigatorStealth / 隐身导航注入集成测试
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 { injectNavigatorStealth } from '../stealth/navigator.js';
chromium.use(StealthPlugin());
const integration = process.env.VISIONL_INTEGRATION === '1' ? describe : describe.skip;
const testProfile: FingerprintProfile = {
id: 'fp_stealth_nav',
name: 'Stealth Navigator 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('injectNavigatorStealth', () => {
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 navigator.webdriver to false', async () => {
context = await browser.newContext();
await injectNavigatorStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const webdriver = await page.evaluate(() => navigator.webdriver);
expect(webdriver).toBe(false);
});
it('should override navigator.languages from profile', async () => {
context = await browser.newContext();
await injectNavigatorStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const langs = await page.evaluate(() => navigator.languages);
expect(langs).toEqual(testProfile.languages);
});
it('should override navigator.platform from profile', async () => {
context = await browser.newContext();
await injectNavigatorStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const platform = await page.evaluate(() => navigator.platform);
expect(platform).toBe(testProfile.platform);
});
it('should set navigator.vendor to "Google Inc."', async () => {
context = await browser.newContext();
await injectNavigatorStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const vendor = await page.evaluate(() => navigator.vendor);
expect(vendor).toBe('Google Inc.');
});
it('should set navigator.productSub to "20030107"', async () => {
context = await browser.newContext();
await injectNavigatorStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const productSub = await page.evaluate(() => navigator.productSub);
expect(productSub).toBe('20030107');
});
it('should inject navigator.connection if missing', async () => {
context = await browser.newContext();
await injectNavigatorStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const connection = await page.evaluate(() => {
const c = (navigator as any).connection;
if (!c) return null;
return { downlink: c.downlink, effectiveType: c.effectiveType, rtt: c.rtt, saveData: c.saveData };
});
expect(connection).not.toBeNull();
expect(connection!.downlink).toBe(10);
expect(connection!.effectiveType).toBe('4g');
expect(connection!.rtt).toBe(50);
expect(connection!.saveData).toBe(false);
});
it('should keep navigator.plugins accessible', async () => {
context = await browser.newContext();
await injectNavigatorStealth(context, testProfile);
const page = await context.newPage();
await page.goto('about:blank');
const plugins = await page.evaluate(() => {
const p = navigator.plugins;
return { length: p.length, hasItem: typeof p.item === 'function', hasNamedItem: typeof p.namedItem === 'function' };
});
expect(typeof plugins.length).toBe('number');
expect(plugins.hasItem).toBe(true);
expect(plugins.hasNamedItem).toBe(true);
});
});
+52
View File
@@ -0,0 +1,52 @@
// Stealth navigator property overrides / 隐身导航属性覆盖
import type { BrowserContext } from 'playwright';
import type { FingerprintProfile } from '@visionl/core';
export async function injectNavigatorStealth(
context: BrowserContext,
profile: FingerprintProfile
): Promise<void> {
await context.addInitScript((opts) => {
Object.defineProperty(navigator, 'webdriver', {
get: () => false,
});
if (navigator.plugins.length === 0) {
Object.defineProperty(navigator, 'plugins', {
get: () => {
const arr = Object.create(PluginArray.prototype);
arr.length = 0;
arr.item = () => null;
arr.namedItem = () => null;
arr.refresh = () => {};
return arr;
},
});
}
Object.defineProperty(navigator, 'languages', {
get: () => opts.languages,
});
Object.defineProperty(navigator, 'platform', {
get: () => opts.platform,
});
if (!('connection' in navigator)) {
Object.defineProperty(navigator, 'connection', {
get: () => ({
downlink: 10,
effectiveType: '4g',
rtt: 50,
saveData: false,
}),
});
}
Object.defineProperty(navigator, 'vendor', { get: () => 'Google Inc.' });
Object.defineProperty(navigator, 'productSub', { get: () => '20030107' });
}, {
languages: profile.languages,
platform: profile.platform,
});
}