feat(daemon): add stealth navigator property injection (Task 11)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user