mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-29 21:15:37 +02:00
13f7e351da
New stealth.ts export that turns the GSTACK_* env vars (already populated
by gbrowser's gbd from host_profile.go) into the --gstack-* cmdline
switches the Pack 1 Chromium patches read at WebGL getParameter,
NavigatorUA::userAgentData, NavigatorConcurrentHardware::hardwareConcurrency,
and NavigatorDeviceMemory::deviceMemory time.
Wired into all three launchArgs sites: launch() (headless), launchHeaded()
(real product path), and handoff() (headless → headed re-launch).
Mapping:
GSTACK_GPU_VENDOR → --gstack-gpu-vendor
GSTACK_GPU_RENDERER → --gstack-gpu-renderer
GSTACK_PLATFORM → --gstack-ua-platform (with mapping:
MacARM/MacIntel → macOS, Win32 → Windows,
Linux x86_64 → Linux)
GSTACK_GPU_CHIPSET → --gstack-ua-model
GSTACK_HW_CONCURRENCY → --gstack-hw-concurrency
GSTACK_DEVICE_MEMORY → --gstack-device-memory
Each switch is emitted only when its env var is non-empty — empty
values fall through to the patch's "no override" path, which returns
the real Chromium native value. Safe to ship on Chromium builds
without the Pack 1 patches applied (zero behavior change).
The patches themselves live in the gbrowser repo at chromium/patches/
{webgl-vendor-spoof,ua-client-hints-stealth,worker-navigator-stealth}.patch.
Both halves (gstack arg construction + gbrowser C++ patches) must
land + Chromium rebuild before the spoof reaches the WebGL/UA-CH/
hardware accessors. Currently dormant until then.
Tests (browse/test/stealth-layer-c.test.ts):
7 new buildGStackLaunchArgs cases — empty env, all-populated, partial,
platform mapping (MacARM/MacIntel/Win32/Linux), unrecognized platform
fallthrough, vendor-with-spaces escape-safety.
All 32 stealth/browser-manager tests pass.
For GBrowser specifically: gstack-side half of the Pack 1 flag plumbing.
gbrowser repo will bump the submodule pointer to this commit, then re-run
bun run test/anti-bot/evidence-run.ts to verify creepjs's "33% headless"
score drops after Pack 1 + Chromium rebuild.
220 lines
8.8 KiB
TypeScript
220 lines
8.8 KiB
TypeScript
/**
|
|
* stealth.ts Layer C additions (T3 + D6 for GBrowser anti-detection):
|
|
* verifies the build-time scaffolding without requiring a live browser.
|
|
*
|
|
* Live-browser verification of these spoofs in actual page contexts is
|
|
* covered by the gbrowser-side `test/anti-bot.test.sh` (Phase 1 / T7)
|
|
* which loads the probe page through the built GBrowser app post-bundle.
|
|
* These tests only exercise the JS script builder + the static export
|
|
* shapes — fast, hermetic, no chromium launch.
|
|
*/
|
|
import { describe, test, expect } from 'bun:test';
|
|
import {
|
|
buildStealthScript,
|
|
buildGStackLaunchArgs,
|
|
WEBDRIVER_MASK_SCRIPT,
|
|
STEALTH_LAUNCH_ARGS,
|
|
STEALTH_IGNORE_DEFAULT_ARGS,
|
|
} from '../src/stealth';
|
|
|
|
describe('STEALTH_IGNORE_DEFAULT_ARGS — T1', () => {
|
|
test('includes --enable-automation (kills infobar)', () => {
|
|
expect(STEALTH_IGNORE_DEFAULT_ARGS).toContain('--enable-automation');
|
|
});
|
|
test('includes the 4 Patchright-recommended adds', () => {
|
|
expect(STEALTH_IGNORE_DEFAULT_ARGS).toContain('--disable-popup-blocking');
|
|
expect(STEALTH_IGNORE_DEFAULT_ARGS).toContain('--disable-component-update');
|
|
expect(STEALTH_IGNORE_DEFAULT_ARGS).toContain('--disable-default-apps');
|
|
});
|
|
test('preserves the original extension-loading blockers', () => {
|
|
expect(STEALTH_IGNORE_DEFAULT_ARGS).toContain('--disable-extensions');
|
|
expect(STEALTH_IGNORE_DEFAULT_ARGS).toContain('--disable-component-extensions-with-background-pages');
|
|
});
|
|
});
|
|
|
|
describe('buildStealthScript — T3 Layer C', () => {
|
|
const hw = { platform: 'MacARM', hwConcurrency: 16, deviceMemory: 8 };
|
|
|
|
test('builds a self-invoking function (atomic injection)', () => {
|
|
const s = buildStealthScript(hw);
|
|
expect(s.trim().startsWith('(() => {')).toBe(true);
|
|
expect(s.trim().endsWith('})();')).toBe(true);
|
|
});
|
|
|
|
test('installs the Function.prototype.toString Proxy FIRST', () => {
|
|
const s = buildStealthScript(hw);
|
|
const proxyIdx = s.indexOf('new Proxy(nativeToString');
|
|
const webdriverIdx = s.indexOf("'webdriver'");
|
|
expect(proxyIdx).toBeGreaterThan(0);
|
|
expect(webdriverIdx).toBeGreaterThan(proxyIdx);
|
|
});
|
|
|
|
test('navigator.webdriver getter returns false', () => {
|
|
const s = buildStealthScript(hw);
|
|
expect(s).toMatch(/Object\.defineProperty\(navigator, 'webdriver'/);
|
|
expect(s).toMatch(/return false/);
|
|
});
|
|
|
|
test('window.chrome.runtime ships full enum shape', () => {
|
|
const s = buildStealthScript(hw);
|
|
expect(s).toContain('OnInstalledReason');
|
|
expect(s).toContain('PlatformArch');
|
|
expect(s).toContain('PlatformOs');
|
|
expect(s).toContain('RequestUpdateCheckStatus');
|
|
// sendMessage / connect must throw native-shaped errors
|
|
expect(s).toContain('runtime.connect');
|
|
expect(s).toContain('runtime.sendMessage');
|
|
});
|
|
|
|
test('chrome.csi and chrome.loadTimes provide method bodies', () => {
|
|
const s = buildStealthScript(hw);
|
|
expect(s).toContain('chrome.csi = markNative(function csi()');
|
|
expect(s).toContain('chrome.loadTimes = markNative(function loadTimes()');
|
|
// loadTimes shape must include wasFetchedViaSpdy/connectionInfo —
|
|
// those are what real Chrome's loadTimes() returns on HTTP/2 sites.
|
|
expect(s).toContain('wasFetchedViaSpdy');
|
|
expect(s).toContain('connectionInfo');
|
|
});
|
|
|
|
test('Notification.permission aligned to default', () => {
|
|
const s = buildStealthScript(hw);
|
|
expect(s).toMatch(/Notification, 'permission'/);
|
|
expect(s).toMatch(/return 'default'/);
|
|
});
|
|
|
|
test('hardware values interpolated from host profile (NOT hardcoded)', () => {
|
|
const s = buildStealthScript({ platform: 'MacARM', hwConcurrency: 12, deviceMemory: 4 });
|
|
expect(s).toContain('return 12');
|
|
expect(s).toContain('return 4');
|
|
expect(s).not.toMatch(/return 8;.*hardwareConcurrency/);
|
|
});
|
|
|
|
test('cleans up Selenium 25 globals + Playwright + Phantom + Nightmare', () => {
|
|
const s = buildStealthScript(hw);
|
|
// Spot-check a few from each category
|
|
expect(s).toContain('__webdriver_evaluate'); // Selenium
|
|
expect(s).toContain('domAutomationController'); // Chrome Driver classic
|
|
expect(s).toContain('__pwInitScripts'); // Playwright
|
|
expect(s).toContain('callPhantom'); // PhantomJS
|
|
expect(s).toContain('__nightmare'); // NightmareJS
|
|
expect(s).toContain('_Selenium_IDE_Recorder'); // Selenium IDE
|
|
});
|
|
|
|
test('uses markNative wrapper for every patched function', () => {
|
|
const s = buildStealthScript(hw);
|
|
// Every getter (hardwareConcurrency, deviceMemory, webdriver, Notification.permission)
|
|
// should be wrapped through markNative so the toString Proxy covers it.
|
|
const markNativeMatches = s.match(/markNative\(/g) || [];
|
|
// At least 8 markNative wrappings (webdriver, csi, loadTimes, connect, sendMessage,
|
|
// notification permission, hwConcurrency, deviceMemory)
|
|
expect(markNativeMatches.length).toBeGreaterThanOrEqual(7);
|
|
});
|
|
|
|
test('script does not include "GStackBrowser" branding string', () => {
|
|
const s = buildStealthScript(hw);
|
|
// D6: dropped from UA, must not leak in via stealth payload either.
|
|
expect(s).not.toContain('GStackBrowser');
|
|
});
|
|
});
|
|
|
|
describe('buildGStackLaunchArgs — Pack 1 cmdline-switch construction', () => {
|
|
// Helper: clear all GSTACK_* env, run test body, restore env.
|
|
function withEnv(env: Record<string, string | undefined>, body: () => void) {
|
|
const saved: Record<string, string | undefined> = {};
|
|
for (const k of Object.keys(process.env)) {
|
|
if (k.startsWith('GSTACK_')) {
|
|
saved[k] = process.env[k];
|
|
delete process.env[k];
|
|
}
|
|
}
|
|
for (const [k, v] of Object.entries(env)) {
|
|
if (v !== undefined) process.env[k] = v;
|
|
}
|
|
try {
|
|
body();
|
|
} finally {
|
|
for (const k of Object.keys(process.env)) {
|
|
if (k.startsWith('GSTACK_')) delete process.env[k];
|
|
}
|
|
for (const [k, v] of Object.entries(saved)) {
|
|
if (v !== undefined) process.env[k] = v;
|
|
}
|
|
}
|
|
}
|
|
|
|
test('empty env produces empty arg list', () => {
|
|
withEnv({}, () => {
|
|
expect(buildGStackLaunchArgs()).toEqual([]);
|
|
});
|
|
});
|
|
|
|
test('all env values populated → all 6 switches emitted', () => {
|
|
withEnv({
|
|
GSTACK_GPU_VENDOR: 'Apple Inc.',
|
|
GSTACK_GPU_RENDERER: 'ANGLE (Apple, ANGLE Metal Renderer: Apple M4 Max, Unspecified Version)',
|
|
GSTACK_PLATFORM: 'MacARM',
|
|
GSTACK_GPU_CHIPSET: 'Apple M4 Max',
|
|
GSTACK_HW_CONCURRENCY: '16',
|
|
GSTACK_DEVICE_MEMORY: '8',
|
|
}, () => {
|
|
const args = buildGStackLaunchArgs();
|
|
expect(args).toContain('--gstack-gpu-vendor=Apple Inc.');
|
|
expect(args).toContain('--gstack-gpu-renderer=ANGLE (Apple, ANGLE Metal Renderer: Apple M4 Max, Unspecified Version)');
|
|
expect(args).toContain('--gstack-ua-platform=macOS');
|
|
expect(args).toContain('--gstack-ua-model=Apple M4 Max');
|
|
expect(args).toContain('--gstack-hw-concurrency=16');
|
|
expect(args).toContain('--gstack-device-memory=8');
|
|
expect(args.length).toBe(6);
|
|
});
|
|
});
|
|
|
|
test('platform mapping: MacARM and MacIntel both → macOS', () => {
|
|
withEnv({ GSTACK_PLATFORM: 'MacARM' }, () => {
|
|
expect(buildGStackLaunchArgs()).toContain('--gstack-ua-platform=macOS');
|
|
});
|
|
withEnv({ GSTACK_PLATFORM: 'MacIntel' }, () => {
|
|
expect(buildGStackLaunchArgs()).toContain('--gstack-ua-platform=macOS');
|
|
});
|
|
});
|
|
|
|
test('platform mapping: Win32 → Windows, Linux x86_64 → Linux', () => {
|
|
withEnv({ GSTACK_PLATFORM: 'Win32' }, () => {
|
|
expect(buildGStackLaunchArgs()).toContain('--gstack-ua-platform=Windows');
|
|
});
|
|
withEnv({ GSTACK_PLATFORM: 'Linux x86_64' }, () => {
|
|
expect(buildGStackLaunchArgs()).toContain('--gstack-ua-platform=Linux');
|
|
});
|
|
});
|
|
|
|
test('partial env: only set switches that have values', () => {
|
|
withEnv({ GSTACK_HW_CONCURRENCY: '12' }, () => {
|
|
const args = buildGStackLaunchArgs();
|
|
expect(args).toEqual(['--gstack-hw-concurrency=12']);
|
|
});
|
|
});
|
|
|
|
test('unrecognized platform falls through without --gstack-ua-platform', () => {
|
|
withEnv({ GSTACK_PLATFORM: 'OS/2' }, () => {
|
|
const args = buildGStackLaunchArgs();
|
|
expect(args.some(a => a.startsWith('--gstack-ua-platform='))).toBe(false);
|
|
});
|
|
});
|
|
|
|
test('GPU vendor with spaces survives intact (no quote/escape regression)', () => {
|
|
withEnv({ GSTACK_GPU_VENDOR: 'NVIDIA Corporation' }, () => {
|
|
const args = buildGStackLaunchArgs();
|
|
expect(args).toContain('--gstack-gpu-vendor=NVIDIA Corporation');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('backwards-compat exports', () => {
|
|
test('WEBDRIVER_MASK_SCRIPT still exported', () => {
|
|
expect(WEBDRIVER_MASK_SCRIPT).toContain("'webdriver'");
|
|
expect(WEBDRIVER_MASK_SCRIPT).toContain('false');
|
|
});
|
|
test('STEALTH_LAUNCH_ARGS still includes blink-features=AutomationControlled', () => {
|
|
expect(STEALTH_LAUNCH_ARGS).toContain('--disable-blink-features=AutomationControlled');
|
|
});
|
|
});
|