From 5cef8cdbc4ebfd8edea5f5c67079571d97d8dfbb Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 12 Aug 2026 21:04:49 +0800 Subject: [PATCH] feat(daemon): add stealth navigator property injection (Task 11) --- .../src/__tests__/stealth-navigator.test.ts | 136 ++++++++++++++++++ packages/daemon/src/stealth/navigator.ts | 52 +++++++ 2 files changed, 188 insertions(+) create mode 100644 packages/daemon/src/__tests__/stealth-navigator.test.ts create mode 100644 packages/daemon/src/stealth/navigator.ts diff --git a/packages/daemon/src/__tests__/stealth-navigator.test.ts b/packages/daemon/src/__tests__/stealth-navigator.test.ts new file mode 100644 index 0000000..bcdf35e --- /dev/null +++ b/packages/daemon/src/__tests__/stealth-navigator.test.ts @@ -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); + }); +}); diff --git a/packages/daemon/src/stealth/navigator.ts b/packages/daemon/src/stealth/navigator.ts new file mode 100644 index 0000000..e02415c --- /dev/null +++ b/packages/daemon/src/stealth/navigator.ts @@ -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 { + 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, + }); +}