mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-09 17:38:40 +02:00
feat: buildGStackLaunchArgs — Pack 1 cmdline-switch construction for gbrowser
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.
This commit is contained in:
@@ -213,8 +213,8 @@ export class BrowserManager {
|
|||||||
// BROWSE_EXTENSIONS_DIR points to an unpacked Chrome extension directory.
|
// BROWSE_EXTENSIONS_DIR points to an unpacked Chrome extension directory.
|
||||||
// Extensions only work in headed mode, so we use an off-screen window.
|
// Extensions only work in headed mode, so we use an off-screen window.
|
||||||
const extensionsDir = process.env.BROWSE_EXTENSIONS_DIR;
|
const extensionsDir = process.env.BROWSE_EXTENSIONS_DIR;
|
||||||
const { STEALTH_LAUNCH_ARGS } = await import('./stealth');
|
const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth');
|
||||||
const launchArgs: string[] = [...STEALTH_LAUNCH_ARGS];
|
const launchArgs: string[] = [...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()];
|
||||||
let useHeadless = true;
|
let useHeadless = true;
|
||||||
|
|
||||||
// Docker/CI/root: Chromium sandbox requires unprivileged user namespaces which
|
// Docker/CI/root: Chromium sandbox requires unprivileged user namespaces which
|
||||||
@@ -295,11 +295,18 @@ export class BrowserManager {
|
|||||||
|
|
||||||
// Find the gstack extension directory for auto-loading
|
// Find the gstack extension directory for auto-loading
|
||||||
const extensionPath = this.findExtensionPath();
|
const extensionPath = this.findExtensionPath();
|
||||||
|
const { buildGStackLaunchArgs } = await import('./stealth');
|
||||||
const launchArgs = [
|
const launchArgs = [
|
||||||
'--hide-crash-restore-bubble',
|
'--hide-crash-restore-bubble',
|
||||||
// Anti-bot-detection: remove the navigator.webdriver flag that Playwright sets.
|
// Anti-bot-detection: remove the navigator.webdriver flag that Playwright sets.
|
||||||
// Sites like Google and NYTimes check this to block automation browsers.
|
// Sites like Google and NYTimes check this to block automation browsers.
|
||||||
'--disable-blink-features=AutomationControlled',
|
'--disable-blink-features=AutomationControlled',
|
||||||
|
// GStack Pack 1: per-install hardware/GPU/UA-CH overrides for the
|
||||||
|
// C++ patches in gbrowser's Chromium build. Each switch is a no-op
|
||||||
|
// on Chromium builds without the corresponding patch (the patch's
|
||||||
|
// empty-fallback returns native), so this is safe on stock Playwright
|
||||||
|
// Chromium too.
|
||||||
|
...buildGStackLaunchArgs(),
|
||||||
];
|
];
|
||||||
if (extensionPath) {
|
if (extensionPath) {
|
||||||
// Skip --load-extension when running against a custom Chromium build
|
// Skip --load-extension when running against a custom Chromium build
|
||||||
@@ -1296,7 +1303,8 @@ export class BrowserManager {
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const extensionPath = this.findExtensionPath();
|
const extensionPath = this.findExtensionPath();
|
||||||
const launchArgs = ['--hide-crash-restore-bubble'];
|
const { buildGStackLaunchArgs } = await import('./stealth');
|
||||||
|
const launchArgs: string[] = ['--hide-crash-restore-bubble', ...buildGStackLaunchArgs()];
|
||||||
if (extensionPath) {
|
if (extensionPath) {
|
||||||
launchArgs.push(`--disable-extensions-except=${extensionPath}`);
|
launchArgs.push(`--disable-extensions-except=${extensionPath}`);
|
||||||
launchArgs.push(`--load-extension=${extensionPath}`);
|
launchArgs.push(`--load-extension=${extensionPath}`);
|
||||||
|
|||||||
@@ -270,6 +270,62 @@ export const STEALTH_LAUNCH_ARGS = [
|
|||||||
'--disable-blink-features=AutomationControlled',
|
'--disable-blink-features=AutomationControlled',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the `--gstack-*=` cmdline switches that the Pack 1 Chromium
|
||||||
|
* patches read (webgl-vendor-spoof, ua-client-hints-stealth, worker-
|
||||||
|
* navigator-stealth). Values come from the GSTACK_* env vars that
|
||||||
|
* gbd populates from host_profile.go at startup.
|
||||||
|
*
|
||||||
|
* Each switch is only emitted when its env var is non-empty — empty
|
||||||
|
* env values fall through to the patch's "no override" path, which
|
||||||
|
* returns the real Chromium native value. This keeps the helper safe
|
||||||
|
* on builds that DO NOT have the C++ patches applied (gbrowser
|
||||||
|
* pre-Pack-1) and on hosts where gbd hasn't yet populated some
|
||||||
|
* fields (legacy installs).
|
||||||
|
*
|
||||||
|
* Mapping (gbd env → Chromium cmdline switch → C++ patch consumer):
|
||||||
|
* GSTACK_GPU_VENDOR → --gstack-gpu-vendor → webgl-vendor-spoof.patch
|
||||||
|
* GSTACK_GPU_RENDERER → --gstack-gpu-renderer → webgl-vendor-spoof.patch
|
||||||
|
* GSTACK_PLATFORM → --gstack-ua-platform → ua-client-hints-stealth.patch
|
||||||
|
* (maps MacARM/MacIntel → "macOS")
|
||||||
|
* GSTACK_GPU_CHIPSET → --gstack-ua-model → ua-client-hints-stealth.patch
|
||||||
|
* GSTACK_HW_CONCURRENCY → --gstack-hw-concurrency → worker-navigator-stealth.patch
|
||||||
|
* GSTACK_DEVICE_MEMORY → --gstack-device-memory → worker-navigator-stealth.patch
|
||||||
|
*/
|
||||||
|
export function buildGStackLaunchArgs(): string[] {
|
||||||
|
const env = (globalThis as any).process?.env ?? {};
|
||||||
|
const args: string[] = [];
|
||||||
|
|
||||||
|
const vendor = env.GSTACK_GPU_VENDOR;
|
||||||
|
if (vendor) args.push(`--gstack-gpu-vendor=${vendor}`);
|
||||||
|
|
||||||
|
const renderer = env.GSTACK_GPU_RENDERER;
|
||||||
|
if (renderer) args.push(`--gstack-gpu-renderer=${renderer}`);
|
||||||
|
|
||||||
|
// Map gbd's "MacARM"/"MacIntel" classification to the UA-CH "macOS"
|
||||||
|
// platform string Chromium emits natively. Other future platforms
|
||||||
|
// would map similarly (Win32 → "Windows", Linux → "Linux").
|
||||||
|
const platform = env.GSTACK_PLATFORM;
|
||||||
|
if (platform === 'MacARM' || platform === 'MacIntel') {
|
||||||
|
args.push('--gstack-ua-platform=macOS');
|
||||||
|
} else if (platform === 'Win32') {
|
||||||
|
args.push('--gstack-ua-platform=Windows');
|
||||||
|
} else if (platform && platform.startsWith('Linux')) {
|
||||||
|
args.push('--gstack-ua-platform=Linux');
|
||||||
|
}
|
||||||
|
|
||||||
|
const chipset = env.GSTACK_GPU_CHIPSET;
|
||||||
|
if (chipset) args.push(`--gstack-ua-model=${chipset}`);
|
||||||
|
|
||||||
|
const hw = env.GSTACK_HW_CONCURRENCY;
|
||||||
|
if (hw) args.push(`--gstack-hw-concurrency=${hw}`);
|
||||||
|
|
||||||
|
const memory = env.GSTACK_DEVICE_MEMORY;
|
||||||
|
if (memory) args.push(`--gstack-device-memory=${memory}`);
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Playwright default args to strip via ignoreDefaultArgs.
|
* Playwright default args to strip via ignoreDefaultArgs.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import { describe, test, expect } from 'bun:test';
|
import { describe, test, expect } from 'bun:test';
|
||||||
import {
|
import {
|
||||||
buildStealthScript,
|
buildStealthScript,
|
||||||
|
buildGStackLaunchArgs,
|
||||||
WEBDRIVER_MASK_SCRIPT,
|
WEBDRIVER_MASK_SCRIPT,
|
||||||
STEALTH_LAUNCH_ARGS,
|
STEALTH_LAUNCH_ARGS,
|
||||||
STEALTH_IGNORE_DEFAULT_ARGS,
|
STEALTH_IGNORE_DEFAULT_ARGS,
|
||||||
@@ -116,6 +117,97 @@ describe('buildStealthScript — T3 Layer C', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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', () => {
|
describe('backwards-compat exports', () => {
|
||||||
test('WEBDRIVER_MASK_SCRIPT still exported', () => {
|
test('WEBDRIVER_MASK_SCRIPT still exported', () => {
|
||||||
expect(WEBDRIVER_MASK_SCRIPT).toContain("'webdriver'");
|
expect(WEBDRIVER_MASK_SCRIPT).toContain("'webdriver'");
|
||||||
|
|||||||
Reference in New Issue
Block a user