jumpy-lion 0.1.6-beta.7 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -607
- package/browser/fonts/macos-bundle/40-macos-aliases.conf +515 -0
- package/browser/fonts/macos-bundle/README.md +24 -0
- package/browser/fonts/macos-bundle/install.sh +83 -0
- package/dist/browser-controller.d.ts +47 -4
- package/dist/browser-controller.d.ts.map +1 -1
- package/dist/browser-controller.js +219 -278
- package/dist/browser-controller.js.map +1 -1
- package/dist/browser-plugin.d.ts +82 -110
- package/dist/browser-plugin.d.ts.map +1 -1
- package/dist/browser-plugin.js +376 -379
- package/dist/browser-plugin.js.map +1 -1
- package/dist/browser-process/align-fingerprint-config.d.ts +36 -0
- package/dist/browser-process/align-fingerprint-config.d.ts.map +1 -0
- package/dist/browser-process/align-fingerprint-config.js +74 -0
- package/dist/browser-process/align-fingerprint-config.js.map +1 -0
- package/dist/browser-process/anti-detect-browser.d.ts +2 -2
- package/dist/browser-process/anti-detect-browser.js +7 -7
- package/dist/browser-process/anti-detect-config.d.ts +131 -2
- package/dist/browser-process/anti-detect-config.d.ts.map +1 -1
- package/dist/browser-process/anti-detect-config.js +337 -127
- package/dist/browser-process/anti-detect-config.js.map +1 -1
- package/dist/browser-process/browser.d.ts +25 -92
- package/dist/browser-process/browser.d.ts.map +1 -1
- package/dist/browser-process/browser.js +429 -73
- package/dist/browser-process/browser.js.map +1 -1
- package/dist/browser-process/fingerprint-config.d.ts +146 -0
- package/dist/browser-process/fingerprint-config.d.ts.map +1 -0
- package/dist/browser-process/fingerprint-config.js +308 -0
- package/dist/browser-process/fingerprint-config.js.map +1 -0
- package/dist/browser-process/get-chrome-executable.d.ts +6 -0
- package/dist/browser-process/get-chrome-executable.d.ts.map +1 -1
- package/dist/browser-process/get-chrome-executable.js +79 -4
- package/dist/browser-process/get-chrome-executable.js.map +1 -1
- package/dist/browser-process/gpu-family-profiles.d.ts +58 -0
- package/dist/browser-process/gpu-family-profiles.d.ts.map +1 -0
- package/dist/browser-process/gpu-family-profiles.js +480 -0
- package/dist/browser-process/gpu-family-profiles.js.map +1 -0
- package/dist/browser-process/launch-options.d.ts +301 -0
- package/dist/browser-process/launch-options.d.ts.map +1 -0
- package/dist/browser-process/launch-options.js +2 -0
- package/dist/browser-process/launch-options.js.map +1 -0
- package/dist/browser-process/port-allocator.d.ts +64 -0
- package/dist/browser-process/port-allocator.d.ts.map +1 -0
- package/dist/browser-process/port-allocator.js +198 -0
- package/dist/browser-process/port-allocator.js.map +1 -0
- package/dist/browser-process/process.d.ts +85 -0
- package/dist/browser-process/process.d.ts.map +1 -1
- package/dist/browser-process/process.js +235 -19
- package/dist/browser-process/process.js.map +1 -1
- package/dist/browser-process/signal-cleanup.d.ts +40 -0
- package/dist/browser-process/signal-cleanup.d.ts.map +1 -0
- package/dist/browser-process/signal-cleanup.js +105 -0
- package/dist/browser-process/signal-cleanup.js.map +1 -0
- package/dist/browser-process/widevine-cdm.d.ts +144 -0
- package/dist/browser-process/widevine-cdm.d.ts.map +1 -0
- package/dist/browser-process/widevine-cdm.js +298 -0
- package/dist/browser-process/widevine-cdm.js.map +1 -0
- package/dist/browser-profiles/chrome/populate-profile.d.ts.map +1 -1
- package/dist/browser-profiles/chrome/populate-profile.js +21 -22
- package/dist/browser-profiles/chrome/populate-profile.js.map +1 -1
- package/dist/connection/cdp-reconnection.d.ts +50 -0
- package/dist/connection/cdp-reconnection.d.ts.map +1 -0
- package/dist/connection/cdp-reconnection.js +75 -0
- package/dist/connection/cdp-reconnection.js.map +1 -0
- package/dist/connection/page-session.d.ts +114 -0
- package/dist/connection/page-session.d.ts.map +1 -0
- package/dist/connection/page-session.js +237 -0
- package/dist/connection/page-session.js.map +1 -0
- package/dist/connection/session-client.d.ts +34 -0
- package/dist/connection/session-client.d.ts.map +1 -0
- package/dist/connection/session-client.js +129 -0
- package/dist/connection/session-client.js.map +1 -0
- package/dist/crawler.d.ts +9 -88
- package/dist/crawler.d.ts.map +1 -1
- package/dist/crawler.js +157 -8
- package/dist/crawler.js.map +1 -1
- package/dist/fingerprinting/fingerprint-injector.d.ts +24 -114
- package/dist/fingerprinting/fingerprint-injector.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-injector.js +305 -611
- package/dist/fingerprinting/fingerprint-injector.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.d.ts +1 -1
- package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.js +16 -14
- package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/index.d.ts +6 -2
- package/dist/fingerprinting/fingerprint-overrides/index.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/index.js +6 -2
- package/dist/fingerprinting/fingerprint-overrides/index.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.d.ts +1 -1
- package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.js +4 -4
- package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/locale-spoofing.js +4 -4
- package/dist/fingerprinting/fingerprint-overrides/navigator-override.d.ts +18 -0
- package/dist/fingerprinting/fingerprint-overrides/navigator-override.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/navigator-override.js +136 -0
- package/dist/fingerprinting/fingerprint-overrides/navigator-override.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/override.d.ts +151 -0
- package/dist/fingerprinting/fingerprint-overrides/override.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/override.js +14 -0
- package/dist/fingerprinting/fingerprint-overrides/override.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/platform-consistency.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/platform-consistency.js +46 -15
- package/dist/fingerprinting/fingerprint-overrides/platform-consistency.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/registry.d.ts +43 -0
- package/dist/fingerprinting/fingerprint-overrides/registry.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/registry.js +342 -0
- package/dist/fingerprinting/fingerprint-overrides/registry.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/screen-override.d.ts +16 -0
- package/dist/fingerprinting/fingerprint-overrides/screen-override.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/screen-override.js +175 -0
- package/dist/fingerprinting/fingerprint-overrides/screen-override.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.d.ts +1 -1
- package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.js +4 -4
- package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/stealth-script.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/stealth-script.js +75 -9
- package/dist/fingerprinting/fingerprint-overrides/stealth-script.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/ua-ch.d.ts +1 -1
- package/dist/fingerprinting/fingerprint-overrides/ua-ch.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/ua-ch.js +34 -20
- package/dist/fingerprinting/fingerprint-overrides/ua-ch.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.js +25 -6
- package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/webrtc-spoofing.js +1 -1
- package/dist/fingerprinting/hardware-realism.d.ts +83 -0
- package/dist/fingerprinting/hardware-realism.d.ts.map +1 -0
- package/dist/fingerprinting/hardware-realism.js +156 -0
- package/dist/fingerprinting/hardware-realism.js.map +1 -0
- package/dist/fingerprinting/injection-planner.d.ts +133 -0
- package/dist/fingerprinting/injection-planner.d.ts.map +1 -0
- package/dist/fingerprinting/injection-planner.js +497 -0
- package/dist/fingerprinting/injection-planner.js.map +1 -0
- package/dist/fingerprinting/locale-resolver.d.ts +8 -1
- package/dist/fingerprinting/locale-resolver.d.ts.map +1 -1
- package/dist/fingerprinting/locale-resolver.js +26 -3
- package/dist/fingerprinting/locale-resolver.js.map +1 -1
- package/dist/fingerprinting/non-apify-fingerprint-generator.d.ts +4 -4
- package/dist/fingerprinting/non-apify-fingerprint-generator.d.ts.map +1 -1
- package/dist/fingerprinting/non-apify-fingerprint-generator.js +24 -58
- package/dist/fingerprinting/non-apify-fingerprint-generator.js.map +1 -1
- package/dist/fingerprinting/non-apify-profiles.json +10515 -8494
- package/dist/fingerprinting/os-consistency.d.ts +28 -0
- package/dist/fingerprinting/os-consistency.d.ts.map +1 -0
- package/dist/fingerprinting/os-consistency.js +45 -0
- package/dist/fingerprinting/os-consistency.js.map +1 -0
- package/dist/fingerprinting/profile-quality.d.ts +25 -0
- package/dist/fingerprinting/profile-quality.d.ts.map +1 -0
- package/dist/fingerprinting/profile-quality.js +310 -0
- package/dist/fingerprinting/profile-quality.js.map +1 -0
- package/dist/fingerprinting/profile-selector.d.ts +102 -0
- package/dist/fingerprinting/profile-selector.d.ts.map +1 -0
- package/dist/fingerprinting/profile-selector.js +168 -0
- package/dist/fingerprinting/profile-selector.js.map +1 -0
- package/dist/fingerprinting/ua-alignment.d.ts +83 -0
- package/dist/fingerprinting/ua-alignment.d.ts.map +1 -0
- package/dist/fingerprinting/ua-alignment.js +197 -0
- package/dist/fingerprinting/ua-alignment.js.map +1 -0
- package/dist/fingerprinting/ua-ch-brands.d.ts +88 -0
- package/dist/fingerprinting/ua-ch-brands.d.ts.map +1 -0
- package/dist/fingerprinting/ua-ch-brands.js +174 -0
- package/dist/fingerprinting/ua-ch-brands.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/input/dropdown-selector.d.ts +90 -0
- package/dist/input/dropdown-selector.d.ts.map +1 -0
- package/dist/input/dropdown-selector.js +394 -0
- package/dist/input/dropdown-selector.js.map +1 -0
- package/dist/input/element-target.d.ts +152 -0
- package/dist/input/element-target.d.ts.map +1 -0
- package/dist/input/element-target.js +461 -0
- package/dist/input/element-target.js.map +1 -0
- package/dist/input/input-emulator.d.ts +85 -0
- package/dist/input/input-emulator.d.ts.map +1 -0
- package/dist/input/input-emulator.js +325 -0
- package/dist/input/input-emulator.js.map +1 -0
- package/dist/input/input-transport.d.ts +60 -0
- package/dist/input/input-transport.d.ts.map +1 -0
- package/dist/input/input-transport.js +28 -0
- package/dist/input/input-transport.js.map +1 -0
- package/dist/input/recording-transport.d.ts +32 -0
- package/dist/input/recording-transport.d.ts.map +1 -0
- package/dist/input/recording-transport.js +43 -0
- package/dist/input/recording-transport.js.map +1 -0
- package/dist/keyboard-layout.d.ts +2 -2
- package/dist/keyboard-layout.js +2 -2
- package/dist/navigation/page-navigation.d.ts +67 -0
- package/dist/navigation/page-navigation.d.ts.map +1 -0
- package/dist/navigation/page-navigation.js +122 -0
- package/dist/navigation/page-navigation.js.map +1 -0
- package/dist/network/network-watch.d.ts +110 -0
- package/dist/network/network-watch.d.ts.map +1 -0
- package/dist/network/network-watch.js +223 -0
- package/dist/network/network-watch.js.map +1 -0
- package/dist/page.d.ts +118 -104
- package/dist/page.d.ts.map +1 -1
- package/dist/page.js +311 -1130
- package/dist/page.js.map +1 -1
- package/dist/session/inject-hook.d.ts +24 -0
- package/dist/session/inject-hook.d.ts.map +1 -0
- package/dist/session/inject-hook.js +80 -0
- package/dist/session/inject-hook.js.map +1 -0
- package/dist/session/save.d.ts +21 -0
- package/dist/session/save.d.ts.map +1 -0
- package/dist/session/save.js +163 -0
- package/dist/session/save.js.map +1 -0
- package/dist/session/tar-data-dir.d.ts +18 -0
- package/dist/session/tar-data-dir.d.ts.map +1 -0
- package/dist/session/tar-data-dir.js +105 -0
- package/dist/session/tar-data-dir.js.map +1 -0
- package/dist/session/types.d.ts +80 -0
- package/dist/session/types.d.ts.map +1 -0
- package/dist/session/types.js +29 -0
- package/dist/session/types.js.map +1 -0
- package/dist/session-profile.d.ts +102 -0
- package/dist/session-profile.d.ts.map +1 -0
- package/dist/session-profile.js +163 -0
- package/dist/session-profile.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +17 -6
- package/scripts/postinstall.cjs +58 -0
- package/dist/fingerprinting/custom-fingerprint-injector.d.ts +0 -87
- package/dist/fingerprinting/custom-fingerprint-injector.d.ts.map +0 -1
- package/dist/fingerprinting/custom-fingerprint-injector.js +0 -342
- package/dist/fingerprinting/custom-fingerprint-injector.js.map +0 -1
- package/dist/launcher-wrap.d.ts +0 -10
- package/dist/launcher-wrap.d.ts.map +0 -1
- package/dist/launcher-wrap.js +0 -11
- package/dist/launcher-wrap.js.map +0 -1
|
@@ -3,12 +3,13 @@ import fs from 'fs/promises';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { log } from '@crawlee/core';
|
|
6
|
-
import {
|
|
6
|
+
import { isAntiDetectBrowserSync } from './anti-detect-browser.js';
|
|
7
|
+
import { writeTempFingerprintConfig } from './anti-detect-config.js';
|
|
8
|
+
import { getChromeExecutablePath, getAntiDetectBrowserPath } from './get-chrome-executable.js';
|
|
7
9
|
import { BrowserProcess } from './process.js';
|
|
10
|
+
import { seedWidevineCdm } from './widevine-cdm.js';
|
|
8
11
|
import { populateChromeProfile } from '../browser-profiles/chrome/populate-profile.js';
|
|
9
12
|
import { createChromeProfile } from '../browser-profiles/index.js';
|
|
10
|
-
import { isAntiDetectBrowserSync } from './anti-detect-browser.js';
|
|
11
|
-
import { writeTempFingerprintConfig } from './anti-detect-config.js';
|
|
12
13
|
const EXTENSION_PATH = path.resolve(import.meta.dirname, '../fingerprinting/all-fingerprint-defender');
|
|
13
14
|
const EXTENSION_PATH2 = path.resolve(import.meta.dirname, '../fingerprinting/anti-webgpu');
|
|
14
15
|
// --lang is intentionally NOT hardcoded below. It is appended per-session from the
|
|
@@ -18,25 +19,35 @@ const EXTENSION_PATH2 = path.resolve(import.meta.dirname, '../fingerprinting/ant
|
|
|
18
19
|
// Flag policy (see ChromeDriver default-switch audit in git history for full evidence):
|
|
19
20
|
//
|
|
20
21
|
// Dropped as actively harmful:
|
|
21
|
-
// --enable-features=NetworkService,NetworkServiceInProcess
|
|
22
|
+
// --enable-features=NetworkService,NetworkServiceInProcess - desktop default is DISABLED
|
|
22
23
|
// since M89; forcing it creates a runtime state no real user has.
|
|
23
|
-
// --disable-ipc-flooding-protection
|
|
24
|
+
// --disable-ipc-flooding-protection - JS-probeable via 201+ history.pushState()/10s.
|
|
24
25
|
//
|
|
25
26
|
// Dropped because they have a concrete page-JS detection probe:
|
|
26
|
-
// --disable-popup-blocking
|
|
27
|
-
// --disable-
|
|
28
|
-
// --disable-backgrounding-occluded-windows — same surface; internally named
|
|
29
|
-
// kDisableBackgroundingOccludedWindowsForTesting.
|
|
30
|
-
// --disable-component-extensions-with-background-pages — speechSynthesis.getVoices()
|
|
27
|
+
// --disable-popup-blocking - window.open() null-vs-non-null from a timer.
|
|
28
|
+
// --disable-component-extensions-with-background-pages - speechSynthesis.getVoices()
|
|
31
29
|
// leaks the missing TTS component extension.
|
|
32
30
|
//
|
|
31
|
+
// Conditionally added based on which binary we launch:
|
|
32
|
+
// --disable-renderer-backgrounding + --disable-backgrounding-occluded-windows
|
|
33
|
+
// auto-enabled when launching the anti-detect binary; the binary's
|
|
34
|
+
// direct source-tree edit to document.visibilityState / document.hidden
|
|
35
|
+
// hides the rAF / setTimeout throttling tell, so these flags are safe.
|
|
36
|
+
// On vanilla Chrome they remain opt-in via CHROMIUM_KEEP_BACKGROUNDING=1.
|
|
37
|
+
// macOS's own compositor-pause-on-occlusion path is in
|
|
38
|
+
// WebContentsViewCocoa and has no command-line switch - the anti-detect
|
|
39
|
+
// binary drops the kOccluded transition directly in
|
|
40
|
+
// content/app_shim_remote_cocoa/web_contents_view_cocoa.mm.
|
|
41
|
+
//
|
|
33
42
|
// Dropped because they are provably dead or placebo:
|
|
34
|
-
// --disable-translate
|
|
35
|
-
// --disable-infobars
|
|
43
|
+
// --disable-translate - string absent from Chromium switches files; noop since ~2018.
|
|
44
|
+
// --disable-infobars - infobar_manager.cc:22-31 gate requires --headless OR a
|
|
36
45
|
// Chrome-for-Testing build; never active in our setup.
|
|
37
|
-
// --disable-blink-features=AutomationControlled
|
|
46
|
+
// --disable-blink-features=AutomationControlled - only blocks the disabled-by-default
|
|
38
47
|
// RuntimeEnabledFeatures path; the actual webdriver-setter is
|
|
39
|
-
// probe::ApplyAutomationOverride via CDP, which
|
|
48
|
+
// probe::ApplyAutomationOverride via CDP, which the anti-detect binary's
|
|
49
|
+
// direct edit to inspector_emulation_agent.cc / navigator.cc already no-ops
|
|
50
|
+
// (Navigator::webdriver() hardcoded to false).
|
|
40
51
|
//
|
|
41
52
|
// Dropped because they are network-signature flags absent from modern stealth tooling
|
|
42
53
|
// (nodriver / Patchright) with no page-JS benefit on our setup:
|
|
@@ -48,6 +59,21 @@ const EXTENSION_PATH2 = path.resolve(import.meta.dirname, '../fingerprinting/ant
|
|
|
48
59
|
* (a) no known page-JS observable probe, AND
|
|
49
60
|
* (b) an operational or consistency justification (avoid startup dialogs, reduce
|
|
50
61
|
* automation-irrelevant background work, match realistic fresh-profile defaults).
|
|
62
|
+
*
|
|
63
|
+
* Two operationally-detectable flags moved to opt-in env vars:
|
|
64
|
+
* - --disable-renderer-backgrounding / --disable-backgrounding-occluded-windows
|
|
65
|
+
* (rAF throttle probe): opt in via CHROMIUM_KEEP_BACKGROUNDING=1 for local
|
|
66
|
+
* headfull dev where window focus loss would otherwise stall the crawl. The
|
|
67
|
+
* document.hidden() C++ override (cromite patch D26) handles the JS-property
|
|
68
|
+
* side of the visibility spoof regardless.
|
|
69
|
+
* - --use-angle=swiftshader (WebGL perf probe + tell on macOS): opt in via
|
|
70
|
+
* CHROMIUM_USE_SWIFTSHADER=1 for canvas/WebGL pixel parity between local-mac
|
|
71
|
+
* (Metal-capable) and Apify-linux. Off by default lets ANGLE Metal/D3D/GL
|
|
72
|
+
* run natively. The anti-detect binary's direct source-tree edit to
|
|
73
|
+
* WebGLRenderingContextBase::getParameter (in
|
|
74
|
+
* third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc)
|
|
75
|
+
* still spoofs UNMASKED_VENDOR_WEBGL / UNMASKED_RENDERER_WEBGL regardless
|
|
76
|
+
* of which backend ANGLE picks.
|
|
51
77
|
*/
|
|
52
78
|
const UNIVERSAL_STEALTH_ARGS = [
|
|
53
79
|
// CDP transport: Chrome 111+ requires an origin allow-list for WebSocket connections
|
|
@@ -61,28 +87,118 @@ const UNIVERSAL_STEALTH_ARGS = [
|
|
|
61
87
|
'--disable-session-crashed-bubble',
|
|
62
88
|
// Suppress the EU search-engine choice modal, which would block a fresh profile.
|
|
63
89
|
'--disable-search-engine-choice-screen',
|
|
64
|
-
// Predictable startup URL
|
|
90
|
+
// Predictable startup URL - blank page instead of NTP fetch.
|
|
65
91
|
'--homepage=about:blank',
|
|
66
92
|
// Operational: do not show the "Page Unresponsive" dialog mid-crawl. No JS probe.
|
|
67
93
|
'--disable-hang-monitor',
|
|
68
94
|
// Operational: suppress the "Confirm form resubmission" dialog on POST reload. No JS probe.
|
|
69
95
|
'--disable-prompt-on-repost',
|
|
70
96
|
];
|
|
97
|
+
/**
|
|
98
|
+
* Optional flags gated on env vars. Adding either of these to the launch argv
|
|
99
|
+
* narrows operational pain (headfull focus-loss stalls; canvas parity drift)
|
|
100
|
+
* but adds a fingerprint surface that real Chrome users do not have.
|
|
101
|
+
*
|
|
102
|
+
* Default-off so production crawls ship the cleanest argv we can manage. Local
|
|
103
|
+
* dev opts in via env. The anti-detect binary path also auto-enables the
|
|
104
|
+
* backgrounding flags - see `getAntiDetectBackgroundingArgs`.
|
|
105
|
+
*/
|
|
106
|
+
function getOptionalStealthArgs() {
|
|
107
|
+
const out = [];
|
|
108
|
+
if (process.env.CHROMIUM_KEEP_BACKGROUNDING === '1'
|
|
109
|
+
|| process.env.CHROMIUM_KEEP_BACKGROUNDING === 'true') {
|
|
110
|
+
// Keep the renderer running at full speed when the OS window loses focus
|
|
111
|
+
// or is occluded. Without these, headfull dev stalls when the user
|
|
112
|
+
// alt-tabs away - Chrome drops renderer priority and clamps setTimeout
|
|
113
|
+
// to ≥1s. (render_process_host_impl.cc; web_contents_impl.cc.)
|
|
114
|
+
//
|
|
115
|
+
// Trade-off: rAF/setTimeout drift while document.hidden===true is JS-probeable.
|
|
116
|
+
out.push('--disable-renderer-backgrounding');
|
|
117
|
+
out.push('--disable-backgrounding-occluded-windows');
|
|
118
|
+
}
|
|
119
|
+
if (process.env.CHROMIUM_USE_SWIFTSHADER === '1'
|
|
120
|
+
|| process.env.CHROMIUM_USE_SWIFTSHADER === 'true') {
|
|
121
|
+
// Force ANGLE → SwiftShader so canvas/WebGL pixel output is identical
|
|
122
|
+
// between local-mac (Metal-capable) and Apify-linux (no GPU). Cromite's
|
|
123
|
+
// seeded pixel noise is a delta on top of a baseline; the baseline must
|
|
124
|
+
// match to make the final hash reproducible across hosts.
|
|
125
|
+
//
|
|
126
|
+
// Trade-off: WebGL perf benchmarks (timing a heavy scene) can detect
|
|
127
|
+
// SwiftShader on macOS hardware. Mainstream anti-bot vendors hash
|
|
128
|
+
// statically, but the flag is also visible in /proc/<pid>/cmdline on
|
|
129
|
+
// Linux when --no-sandbox is active.
|
|
130
|
+
out.push('--use-angle=swiftshader');
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Backgrounding/occlusion suppression. Auto-enabled when launching the
|
|
136
|
+
* cromite anti-detect binary because cromite patch D26 pins
|
|
137
|
+
* `document.visibilityState` / `document.hidden` to "visible" at the
|
|
138
|
+
* Blink layer, so the rAF/setTimeout drift these flags would expose on
|
|
139
|
+
* vanilla Chrome is invisible to page JS under the patched binary.
|
|
140
|
+
*
|
|
141
|
+
* On macOS the equivalent of `CalculateNativeWinOcclusion` (the AURA
|
|
142
|
+
* Linux/Windows path) is `WebContentsViewCocoa.updateWebContentsVisibility`
|
|
143
|
+
* which has NO command-line kill switch - the kOccluded transition is
|
|
144
|
+
* hardcoded in content/app_shim_remote_cocoa/web_contents_view_cocoa.mm.
|
|
145
|
+
* That path is patched out in the cromite binary directly (see the
|
|
146
|
+
* "Anti-Detect: drop the kOccluded branch" patch), so no darwin-only
|
|
147
|
+
* launch flag is needed (or available) for the same effect.
|
|
148
|
+
*
|
|
149
|
+
* What these flags still do on macOS:
|
|
150
|
+
* --disable-renderer-backgrounding stops the renderer-process priority
|
|
151
|
+
* drop when our window is not the key window (CDP-driven sessions
|
|
152
|
+
* often never become key).
|
|
153
|
+
* --disable-backgrounding-occluded-windows stops the priority drop
|
|
154
|
+
* specifically on occlusion (separate from the visibility transition
|
|
155
|
+
* fixed by the cromite Cocoa patch).
|
|
156
|
+
*/
|
|
157
|
+
function getAntiDetectBackgroundingArgs(_platform) {
|
|
158
|
+
return [
|
|
159
|
+
'--disable-renderer-backgrounding',
|
|
160
|
+
'--disable-backgrounding-occluded-windows',
|
|
161
|
+
];
|
|
162
|
+
}
|
|
71
163
|
/**
|
|
72
164
|
* Linux-specific flags. Necessary for server-side operation; no page-JS probe on any:
|
|
73
165
|
* --no-sandbox: running as root or in a rootless container where seccomp-bpf is
|
|
74
166
|
* unavailable. Not observable from page JS (/proc/self/status is not reachable
|
|
75
|
-
* from the renderer).
|
|
76
|
-
*
|
|
167
|
+
* from the renderer). Can be opted out with $CHROMIUM_KEEP_SANDBOX=1 when the
|
|
168
|
+
* container is rootless AND the host kernel allows user namespaces - e.g.
|
|
169
|
+
* modern Apify runners that drop privileges to UID 1000. The kernel-level
|
|
170
|
+
* sandbox is a small extra defense and removing the --no-sandbox flag
|
|
171
|
+
* reduces /proc/<pid>/cmdline noise.
|
|
172
|
+
* --disable-dev-shm-usage: Docker's default /dev/shm is 64 MB - renderers crash when
|
|
77
173
|
* it fills. Backing switches to /tmp. Not observable from page JS.
|
|
78
174
|
* --password-store=basic: avoid blocking on gnome-keyring/kwallet prompts. Not
|
|
79
175
|
* observable from page JS.
|
|
80
176
|
*/
|
|
81
|
-
const
|
|
82
|
-
'--no-sandbox',
|
|
177
|
+
const LINUX_STEALTH_ARGS_BASE = [
|
|
83
178
|
'--disable-dev-shm-usage',
|
|
84
179
|
'--password-store=basic',
|
|
85
180
|
];
|
|
181
|
+
/**
|
|
182
|
+
* Build the Linux flag list at runtime so the $CHROMIUM_KEEP_SANDBOX opt-out is
|
|
183
|
+
* picked up per-launch (process env can change between calls in long-running
|
|
184
|
+
* processes, e.g. when an Apify actor sets the var at startup).
|
|
185
|
+
*
|
|
186
|
+
* `--disable-setuid-sandbox` ships alongside `--no-sandbox`: even with
|
|
187
|
+
* `--no-sandbox`, Chrome may try to call the setuid sandbox helper
|
|
188
|
+
* (chrome_sandbox / chrome-sandbox) at startup; on restricted containers
|
|
189
|
+
* (Apify LIMITED_PERMISSIONS, Docker rootless) the helper isn't installed
|
|
190
|
+
* or can't bind, and Chrome hangs waiting on it instead of crashing -
|
|
191
|
+
* presents to the caller as a silent CDP-connect timeout. When
|
|
192
|
+
* CHROMIUM_KEEP_SANDBOX=1, both flags drop so the user-namespace sandbox
|
|
193
|
+
* can engage normally.
|
|
194
|
+
*/
|
|
195
|
+
function getLinuxStealthArgs() {
|
|
196
|
+
const keepSandbox = process.env.CHROMIUM_KEEP_SANDBOX === '1'
|
|
197
|
+
|| process.env.CHROMIUM_KEEP_SANDBOX === 'true';
|
|
198
|
+
return keepSandbox
|
|
199
|
+
? LINUX_STEALTH_ARGS_BASE
|
|
200
|
+
: ['--no-sandbox', '--disable-setuid-sandbox', ...LINUX_STEALTH_ARGS_BASE];
|
|
201
|
+
}
|
|
86
202
|
/**
|
|
87
203
|
* macOS-specific flags. `--use-mock-keychain` substitutes the real Keychain with an
|
|
88
204
|
* in-memory mock to avoid blocking-permission dialogs. Not observable from page JS.
|
|
@@ -94,28 +210,63 @@ const DARWIN_STEALTH_ARGS = [
|
|
|
94
210
|
* Build the stealth argv list. Exported for unit tests so we can assert exact flag
|
|
95
211
|
* sets without booting a browser.
|
|
96
212
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
213
|
+
* When `usingAntiDetectBrowser` is true, the renderer-backgrounding and
|
|
214
|
+
* (on macOS) native-window-occlusion features are disabled regardless of
|
|
215
|
+
* env vars, because cromite patch D26 hides the only page-JS tell those
|
|
216
|
+
* flags would otherwise expose (document.hidden / rAF throttling). This
|
|
217
|
+
* fixes the "page goes blank until I refocus the window" symptom on
|
|
218
|
+
* headful macOS launches driven by CDP.
|
|
100
219
|
*/
|
|
101
220
|
export function buildStealthArgs(env = {}) {
|
|
102
221
|
const platform = env.platform ?? process.platform;
|
|
103
|
-
const args = [...UNIVERSAL_STEALTH_ARGS];
|
|
222
|
+
const args = [...UNIVERSAL_STEALTH_ARGS, ...getOptionalStealthArgs()];
|
|
223
|
+
if (env.usingAntiDetectBrowser) {
|
|
224
|
+
for (const flag of getAntiDetectBackgroundingArgs(platform)) {
|
|
225
|
+
if (!args.includes(flag))
|
|
226
|
+
args.push(flag);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
104
229
|
if (platform === 'linux')
|
|
105
|
-
args.push(...
|
|
230
|
+
args.push(...getLinuxStealthArgs());
|
|
106
231
|
if (platform === 'darwin')
|
|
107
232
|
args.push(...DARWIN_STEALTH_ARGS);
|
|
108
233
|
return args;
|
|
109
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Process-layer launch options: everything in BaseLaunchBrowserOptions
|
|
237
|
+
* (the single shared declaration in launch-options.ts) plus the spawn-time
|
|
238
|
+
* fields only this layer knows about.
|
|
239
|
+
*/
|
|
240
|
+
/**
|
|
241
|
+
* Resolve the platform-overrides config for an anti-detect browser launch, or
|
|
242
|
+
* THROW. The anti-detect binary exists to spoof; launching it without a config
|
|
243
|
+
* silently ships the host identity (navigator.platform / userAgent / WebGL all
|
|
244
|
+
* real) - the "30% of sessions are detectable and nobody notices" failure mode.
|
|
245
|
+
*
|
|
246
|
+
* Resolution order: the per-session config (stamped by browser-plugin.ts) first,
|
|
247
|
+
* then the static browser/config/default_fingerprint.json. If both are absent
|
|
248
|
+
* (per-session missing AND the default loader fails or yields nothing), this
|
|
249
|
+
* throws so the launch aborts instead of leaking. `loadStaticDefault` is injected
|
|
250
|
+
* so the dynamic-import path can be scripted in tests.
|
|
251
|
+
*/
|
|
252
|
+
export async function resolveAntiDetectFingerprintConfig(perSessionConfig, loadStaticDefault) {
|
|
253
|
+
const resolved = perSessionConfig ?? await loadStaticDefault().catch(() => null);
|
|
254
|
+
if (resolved == null) {
|
|
255
|
+
throw new Error('[AntiDetect] No fingerprint config resolved for the anti-detect browser '
|
|
256
|
+
+ '(per-session config absent and browser/config/default_fingerprint.json unavailable). '
|
|
257
|
+
+ 'Refusing to launch unspoofed - the session would leak the host platform, user agent, and WebGL identity.');
|
|
258
|
+
}
|
|
259
|
+
return resolved;
|
|
260
|
+
}
|
|
110
261
|
export const launchBrowserWithNewProfile = async (executablePath, options = {}) => {
|
|
111
262
|
const { anonymizedProxyUrl, debuggingPort, windowWidth = 1920, windowHeight = 1080, ignoreHttpsErrors, headless, useExperimentalFingerprints = false, maximizeWindow = true, enableStealthMode = true, populateProfile: shouldPopulateProfile = true, userDataDir: providedUserDataDir, keepUserDataDir, dnsOverHttpsServer, secureDnsMode, webrtcIpHandlingPolicy, useNativeStealth = false, } = options;
|
|
112
263
|
// Detect if we're running with the anti-detect browser binary
|
|
113
264
|
const usingAntiDetectBrowser = useNativeStealth && isAntiDetectBrowserSync(executablePath);
|
|
114
265
|
if (useNativeStealth && !usingAntiDetectBrowser) {
|
|
115
|
-
log.debug('[AntiDetect] useNativeStealth requested but anti-detect browser not detected
|
|
266
|
+
log.debug('[AntiDetect] useNativeStealth requested but anti-detect browser not detected - using standard Chrome');
|
|
116
267
|
}
|
|
117
268
|
else if (usingAntiDetectBrowser) {
|
|
118
|
-
log.debug('[AntiDetect] Anti-detect browser detected
|
|
269
|
+
log.debug('[AntiDetect] Anti-detect browser detected - native C++ stealth active');
|
|
119
270
|
}
|
|
120
271
|
// Track temp config file for cleanup
|
|
121
272
|
let fingerprintConfigPath = null;
|
|
@@ -127,8 +278,12 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
|
|
|
127
278
|
if (providedUserDataDir) {
|
|
128
279
|
await fs.mkdir(userDataDir, { recursive: true });
|
|
129
280
|
}
|
|
130
|
-
|
|
131
|
-
//
|
|
281
|
+
// Populate profile with realistic user data for 2024-2025 anti-bot evasion.
|
|
282
|
+
// populateChromeProfile is the sole Preferences writer here: it builds the
|
|
283
|
+
// merged Preferences object in memory and writes it once (no separate
|
|
284
|
+
// createChromeProfile write to immediately overwrite). createChromeProfile
|
|
285
|
+
// is only used when population is disabled, or as a fallback if population
|
|
286
|
+
// fails partway so the launch still has a valid base profile.
|
|
132
287
|
if (shouldPopulateProfile) {
|
|
133
288
|
try {
|
|
134
289
|
await populateChromeProfile(userDataDir, {
|
|
@@ -140,6 +295,22 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
|
|
|
140
295
|
}
|
|
141
296
|
catch (popError) {
|
|
142
297
|
log.debug(`Could not populate profile: ${popError}`);
|
|
298
|
+
await createChromeProfile(userDataDir).catch(() => { });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
await createChromeProfile(userDataDir);
|
|
303
|
+
}
|
|
304
|
+
// Seed the Widevine CDM from the per-host cache before Chromium starts, so
|
|
305
|
+
// startup registration picks it up and `requestMediaKeySystemAccess` resolves
|
|
306
|
+
// on the first navigation, exactly as it does in a real Chrome. On Linux this
|
|
307
|
+
// is the ONLY thing that works: the CDM is bound at startup there, so a copy
|
|
308
|
+
// fetched later in the session cannot be used until the next launch (see
|
|
309
|
+
// widevine-cdm.ts). A cold cache is a no-op; ensureWidevineCdm fills it.
|
|
310
|
+
if (usingAntiDetectBrowser) {
|
|
311
|
+
const seeded = await seedWidevineCdm(userDataDir);
|
|
312
|
+
if (seeded) {
|
|
313
|
+
log.debug(`[AntiDetect] Widevine CDM ${seeded.version} seeded into the profile before launch`);
|
|
143
314
|
}
|
|
144
315
|
}
|
|
145
316
|
const browserArgs = [
|
|
@@ -154,7 +325,7 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
|
|
|
154
325
|
'--no-default-browser-check',
|
|
155
326
|
];
|
|
156
327
|
// Add stealth arguments for anti-bot evasion. buildStealthArgs produces a
|
|
157
|
-
// platform-scoped set aligned with nodriver
|
|
328
|
+
// platform-scoped set aligned with nodriver - see the rationale block above
|
|
158
329
|
// the UNIVERSAL_STEALTH_ARGS constant for per-flag evidence.
|
|
159
330
|
if (enableStealthMode) {
|
|
160
331
|
browserArgs.push(...buildStealthArgs({ usingAntiDetectBrowser }));
|
|
@@ -165,6 +336,38 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
|
|
|
165
336
|
if (webrtcIpHandlingPolicy) {
|
|
166
337
|
browserArgs.push(`--force-webrtc-ip-handling-policy=${webrtcIpHandlingPolicy}`);
|
|
167
338
|
}
|
|
339
|
+
if (options.fingerprintOptions?.forceEmulateWebgl === true) {
|
|
340
|
+
// Two switch names, because two binaries are in play and each reads
|
|
341
|
+
// only its own. Verified with `strings` on the shipped artifacts:
|
|
342
|
+
//
|
|
343
|
+
// dist/linux-x64/chrome (151) force-gpu-emulation x2
|
|
344
|
+
// dist/linux-x64.cromite-147.bak/… (147) force-webgl-emulation x2
|
|
345
|
+
//
|
|
346
|
+
// and neither name appears in the other binary. The 147 artifact is
|
|
347
|
+
// the one currently published to the KV store, so pushing only the
|
|
348
|
+
// 151 name means `forceEmulateWebgl: true` is silently dropped for
|
|
349
|
+
// every consumer on the released browser - exactly the way stock
|
|
350
|
+
// Chrome drops an unknown switch. Pushing only the 147 name has the
|
|
351
|
+
// mirror-image problem once #148 flips.
|
|
352
|
+
//
|
|
353
|
+
// Same dual-pass shape as --blink-settings-file /
|
|
354
|
+
// --blink-platform-overrides-file above. Delete the 147 line at the
|
|
355
|
+
// #148 flip, not before.
|
|
356
|
+
//
|
|
357
|
+
// The canonical declaration is switches::kForceGpuEmulation in
|
|
358
|
+
// chrome/common/chrome_switches.{h,cc}, propagated to
|
|
359
|
+
// renderer/GPU children via the kCommonSwitchNames list in
|
|
360
|
+
// chrome/browser/chrome_content_browser_client.cc. The renderer
|
|
361
|
+
// WebGL factory hook in third_party/blink/renderer/modules/webgl/
|
|
362
|
+
// webgl_rendering_context_base.cc reads the switch and, when
|
|
363
|
+
// FingerprintConfig::IsLoaded() also holds, returns the in-process
|
|
364
|
+
// EmulatedWebGraphicsContext3DProvider directly (skipping the GPU
|
|
365
|
+
// command-buffer round-trip entirely). On stock Chrome the switch
|
|
366
|
+
// is unknown and silently dropped.
|
|
367
|
+
browserArgs.push('--force-gpu-emulation');
|
|
368
|
+
browserArgs.push('--force-webgl-emulation');
|
|
369
|
+
log.debug('[AntiDetect] forceEmulateWebgl=true → pushed --force-gpu-emulation + --force-webgl-emulation');
|
|
370
|
+
}
|
|
168
371
|
// Set window size - either specific dimensions or maximize
|
|
169
372
|
// Don't maximize if specific dimensions are provided to avoid conflicts
|
|
170
373
|
if (windowWidth && windowHeight) {
|
|
@@ -184,41 +387,87 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
|
|
|
184
387
|
browserArgs.push(`--disable-extensions-except=${extList}`, // optional, keeps Chrome from loading the rest
|
|
185
388
|
`--load-extension=${extList}`);
|
|
186
389
|
}
|
|
187
|
-
// Anti-detect browser: write
|
|
188
|
-
//
|
|
390
|
+
// Anti-detect browser: write platform-overrides JSON and pass its path to the
|
|
391
|
+
// browser via the config switch (see the two-name note below - the shipped
|
|
392
|
+
// 147 binary calls it --blink-platform-overrides-file, the vanilla-151 one
|
|
393
|
+
// --blink-settings-file). The C++ singleton reads this file at startup
|
|
394
|
+
// (lazy, first access); the browser process re-emits the content inline as
|
|
395
|
+
// the matching -data switch so sandboxed children (renderer/GPU/utility)
|
|
396
|
+
// can parse it without file IO.
|
|
189
397
|
if (usingAntiDetectBrowser) {
|
|
190
398
|
try {
|
|
191
399
|
// Prefer a pre-generated per-session config (passed by browser-plugin.ts) so that
|
|
192
400
|
// each browser instance gets unique C++-level hardware fingerprints (GPU, screen,
|
|
193
401
|
// CPU). Fall back to the static default_fingerprint.json if none was provided.
|
|
194
|
-
|
|
402
|
+
// Throws if neither source yields a config - see resolveAntiDetectFingerprintConfig.
|
|
403
|
+
const configToWrite = await resolveAntiDetectFingerprintConfig(options.fingerprintConfig, async () => {
|
|
195
404
|
const { default: def } = await import(
|
|
196
|
-
/* @vite-ignore */ new URL('../../browser/config/default_fingerprint.json', import.meta.url).href)
|
|
405
|
+
/* @vite-ignore */ new URL('../../browser/config/default_fingerprint.json', import.meta.url).href);
|
|
197
406
|
return def;
|
|
198
|
-
})
|
|
199
|
-
|
|
407
|
+
});
|
|
408
|
+
{
|
|
200
409
|
fingerprintConfigPath = await writeTempFingerprintConfig(configToWrite);
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
410
|
+
// Opaque switch name (renamed from --fingerprint-config). Mimics
|
|
411
|
+
// existing Blink runtime-flag shape so /proc/<pid>/cmdline does
|
|
412
|
+
// not advertise an anti-detect-specific flag.
|
|
413
|
+
// #145 lockstep: the vanilla-151 binary renamed this switch to
|
|
414
|
+
// --blink-settings-file (switches::kBlinkSettingsFile). It knows
|
|
415
|
+
// no other name, and because it hard-fails when neither
|
|
416
|
+
// --blink-settings-file nor --blink-settings-data is present
|
|
417
|
+
// (#78), sending only the old name does not degrade gracefully -
|
|
418
|
+
// the browser SIGTRAPs before the CDP endpoint is ready and the
|
|
419
|
+
// crawler cannot launch it at all. The SHIPPED Cromite/147
|
|
420
|
+
// artifact has the mirror-image problem: it only knows the old
|
|
421
|
+
// name and hard-fails the same way if it is not passed.
|
|
422
|
+
//
|
|
423
|
+
// TEMPORARY COMPATIBILITY SHIM, not design. Both names are
|
|
424
|
+
// pushed, pointing at the SAME file, so one crawler build drives
|
|
425
|
+
// both binaries. Each binary reads the name it knows and ignores
|
|
426
|
+
// the other: base::CommandLine::AppendSwitchesAndArguments
|
|
427
|
+
// (base/command_line.cc) stores every `--k=v` token in a plain
|
|
428
|
+
// map with no allowlist, so an unrecognised switch is inert -
|
|
429
|
+
// nothing in the tree validates the switch set, and the only
|
|
430
|
+
// switch-name-driven warning path (kBadFlags in
|
|
431
|
+
// chrome/browser/ui/startup/bad_flags_prompt.cc) is a fixed list
|
|
432
|
+
// that contains neither name. Verified against the artifacts
|
|
433
|
+
// themselves: `strings` on the 147 build exports only
|
|
434
|
+
// "blink-platform-overrides-file", on the 151 build only
|
|
435
|
+
// "blink-settings-file".
|
|
436
|
+
//
|
|
437
|
+
// DELETE the legacy line below - and this comment - once the
|
|
438
|
+
// shipped artifact is the vanilla line, i.e. after the browser
|
|
439
|
+
// submodule pointer is flipped to the 151 build (#148). At that
|
|
440
|
+
// point no binary we ship or download reads the old name, and
|
|
441
|
+
// keeping it only adds a needless token to /proc/<pid>/cmdline.
|
|
442
|
+
browserArgs.push(`--blink-settings-file=${fingerprintConfigPath}`);
|
|
443
|
+
// LEGACY (Cromite/147 only) - remove with #148.
|
|
444
|
+
browserArgs.push(`--blink-platform-overrides-file=${fingerprintConfigPath}`);
|
|
445
|
+
// Anti-Detect 2026-05-18: also pass the spoofed User-Agent via
|
|
446
|
+
// Chromium's standard `--user-agent` switch so the HTTP wire
|
|
447
|
+
// layer sees the same UA as JS-visible `navigator.userAgent`.
|
|
448
|
+
// The previous mechanism - reading FingerprintConfig directly
|
|
449
|
+
// from `net/url_request/url_request_http_job.cc` - was a
|
|
450
|
+
// net/→blink/ layering violation that broke component builds
|
|
451
|
+
// (FAST_BUILD=1). `--user-agent` is honored by
|
|
452
|
+
// embedder_support::GetUserAgent() which HttpUserAgentSettings
|
|
453
|
+
// caches per-NetworkContext, so every outgoing HTTP request
|
|
454
|
+
// carries the spoofed UA without any cross-layer dep.
|
|
455
|
+
// Accept-Language is already covered by the `--lang=` switch
|
|
456
|
+
// pushed below (line ~520) plus the `setenv("LANG"/"LC_ALL")`
|
|
457
|
+
// pin in chrome_main_delegate.cc::BasicStartupComplete.
|
|
458
|
+
const spoofedUA = configToWrite?.navigator?.userAgent;
|
|
459
|
+
if (spoofedUA) {
|
|
460
|
+
browserArgs.push(`--user-agent=${spoofedUA}`);
|
|
216
461
|
}
|
|
217
|
-
log.debug(`[
|
|
462
|
+
log.debug(`[Blink] Wrote platform-overrides config: ${fingerprintConfigPath}${options.fingerprintConfig ? ' (per-session)' : ' (static default)'}`);
|
|
218
463
|
}
|
|
219
464
|
}
|
|
220
465
|
catch (configErr) {
|
|
221
|
-
|
|
466
|
+
// Never swallow a config failure for the anti-detect binary: a
|
|
467
|
+
// browser launched without the platform-overrides config runs
|
|
468
|
+
// unspoofed and silently leaks the host identity. Surface it so
|
|
469
|
+
// the launch aborts rather than shipping a detectable session.
|
|
470
|
+
throw new Error(`[AntiDetect] Failed to prepare the fingerprint config for the anti-detect browser - refusing to launch unspoofed: ${configErr instanceof Error ? configErr.message : String(configErr)}`);
|
|
222
471
|
}
|
|
223
472
|
}
|
|
224
473
|
// --lang is understood by every Chrome binary (stock + anti-detect) and configures
|
|
@@ -235,14 +484,24 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
|
|
|
235
484
|
browserArgs.push(`--lang=${langArg || options.resolvedLocale.locale}`);
|
|
236
485
|
}
|
|
237
486
|
else {
|
|
238
|
-
// No locale resolved
|
|
487
|
+
// No locale resolved - preserve historical default so we don't change behavior
|
|
239
488
|
// for callers that never touched fingerprintOptions.
|
|
240
489
|
browserArgs.push('--lang=en-US,en');
|
|
241
490
|
}
|
|
242
491
|
if (anonymizedProxyUrl) {
|
|
243
492
|
browserArgs.push(`--proxy-server=${anonymizedProxyUrl}`);
|
|
244
493
|
}
|
|
245
|
-
|
|
494
|
+
// Anti-Detect 2026-05-18: `--ignore-certificate-errors` is JS-detectable.
|
|
495
|
+
// A page can issue a `fetch()` to a sub-resource hosted under a
|
|
496
|
+
// self-signed certificate; real Chrome blocks the load (cert error),
|
|
497
|
+
// we silently succeed. That single asymmetry is enough to flag us.
|
|
498
|
+
//
|
|
499
|
+
// When running the native anti-detect binary we therefore force real
|
|
500
|
+
// certificate validation, regardless of the caller's `ignoreHttpsErrors`
|
|
501
|
+
// setting - the stealth invariant (mimic real Chrome wire behaviour)
|
|
502
|
+
// overrides the convenience flag. Stock-Chromium / non-stealth callers
|
|
503
|
+
// keep the historical default.
|
|
504
|
+
if (ignoreHttpsErrors && !usingAntiDetectBrowser) {
|
|
246
505
|
browserArgs.push('--ignore-certificate-errors');
|
|
247
506
|
// disabled because of cloudflare blocking
|
|
248
507
|
// browserArgs.push('--disable-web-security');
|
|
@@ -251,31 +510,117 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
|
|
|
251
510
|
browserArgs.push('--headless=new');
|
|
252
511
|
}
|
|
253
512
|
log.debug(`Launching browser with user data dir: ${userDataDir}`);
|
|
254
|
-
const chromeVersion = getChromeBinaryVersion(executablePath);
|
|
255
|
-
if (chromeVersion) {
|
|
256
|
-
log.debug(`Using Chrome binary version: ${chromeVersion}`);
|
|
257
|
-
}
|
|
258
513
|
log.debug('browserArgs', browserArgs);
|
|
259
514
|
log.debug(`Stealth mode: ${enableStealthMode}, Profile populated: ${shouldPopulateProfile}, cleanup profile: ${shouldCleanupUserDataDir}`);
|
|
260
|
-
// Chrome bundles its own ICU so LANG/LC_ALL don't drive Intl
|
|
515
|
+
// Chrome bundles its own ICU so LANG/LC_ALL don't drive Intl - but a handful of
|
|
261
516
|
// system APIs (file dialogs, native notifications, extension readers) consult them.
|
|
262
517
|
// Setting these matches what a real user's shell would do and costs nothing.
|
|
518
|
+
//
|
|
519
|
+
// We also override TZ and LANGUAGE: a hardcoded host TZ (e.g. Europe/Berlin from
|
|
520
|
+
// an Apify Dockerfile) would otherwise leak through to anything that reads
|
|
521
|
+
// /etc/localtime or honors $TZ for fallback paths. Chrome's main Intl/Date path
|
|
522
|
+
// is covered by Emulation.setTimezoneOverride (called from fingerprint-injector)
|
|
523
|
+
// but workers and out-of-process subsystems can still consult $TZ.
|
|
263
524
|
const spawnEnv = { ...process.env };
|
|
264
525
|
if (options.resolvedLocale?.posix) {
|
|
265
526
|
spawnEnv.LANG = options.resolvedLocale.posix;
|
|
266
527
|
spawnEnv.LC_ALL = options.resolvedLocale.posix;
|
|
267
528
|
}
|
|
529
|
+
if (options.resolvedLocale?.languages) {
|
|
530
|
+
// GTK/glib and some apps read $LANGUAGE for fallback chains; align with the
|
|
531
|
+
// primary language (strip q-values).
|
|
532
|
+
const primaryLang = options.resolvedLocale.languages
|
|
533
|
+
.split(',')[0]
|
|
534
|
+
?.split(';')[0]
|
|
535
|
+
?.trim();
|
|
536
|
+
if (primaryLang)
|
|
537
|
+
spawnEnv.LANGUAGE = primaryLang;
|
|
538
|
+
}
|
|
539
|
+
if (options.resolvedLocale?.timezone) {
|
|
540
|
+
spawnEnv.TZ = options.resolvedLocale.timezone;
|
|
541
|
+
}
|
|
542
|
+
// Pipe stderr instead of swallowing it so a Chromium crash on Linux
|
|
543
|
+
// (missing shared lib, sandbox failure, GLIBC mismatch, …) surfaces
|
|
544
|
+
// in the logs instead of presenting as a generic "Failed to connect to
|
|
545
|
+
// browser on port: 9200" 30s timeout. Last ~50 lines are buffered and
|
|
546
|
+
// dumped at WARN level if the process exits before the CDP listener
|
|
547
|
+
// comes up.
|
|
268
548
|
const browserProcess = spawn(executablePath, browserArgs, {
|
|
269
|
-
stdio: 'ignore',
|
|
549
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
270
550
|
env: spawnEnv,
|
|
551
|
+
// Put the browser in its own process group (pgid === pid) so the
|
|
552
|
+
// whole Chromium process tree - parent + renderer/GPU/utility
|
|
553
|
+
// helpers - can be reaped at once via `process.kill(-pid, sig)`.
|
|
554
|
+
// Without this, signals hit only the parent and helpers orphan to
|
|
555
|
+
// launchd/init, leaking RAM and exhausting macOS IOSurface clients.
|
|
556
|
+
// Ctrl+C is still handled explicitly by signal-cleanup.ts, so losing
|
|
557
|
+
// the TTY's automatic SIGINT forwarding to the group is fine.
|
|
558
|
+
detached: true,
|
|
271
559
|
});
|
|
272
560
|
const proc = new BrowserProcess(browserProcess, userDataDir, shouldCleanupUserDataDir);
|
|
273
|
-
|
|
561
|
+
const stderrBuffer = [];
|
|
562
|
+
const STDERR_BUFFER_LIMIT = 50;
|
|
563
|
+
let stderrLeftover = '';
|
|
564
|
+
// Chromium writes a large volume of benign stderr on headless container
|
|
565
|
+
// launches (GPU-less init, ALSA "no sound card", dbus, WebRTC P2P
|
|
566
|
+
// resolve failures, the DevTools banner, zygote teardown races, …).
|
|
567
|
+
// None of it is actionable at runtime, so EVERY stderr line is logged at
|
|
568
|
+
// DEBUG only - it surfaces solely when the Apify log level is DEBUG.
|
|
569
|
+
// Real failures are not lost: the `exit` handler below dumps the buffered
|
|
570
|
+
// stderr tail at WARNING whenever Chromium exits with a non-zero code,
|
|
571
|
+
// which is the signature of an actual crash.
|
|
572
|
+
browserProcess.stderr?.on('data', (chunk) => {
|
|
573
|
+
stderrLeftover += chunk.toString('utf8');
|
|
574
|
+
const lines = stderrLeftover.split('\n');
|
|
575
|
+
stderrLeftover = lines.pop() ?? '';
|
|
576
|
+
for (const line of lines) {
|
|
577
|
+
if (!line)
|
|
578
|
+
continue;
|
|
579
|
+
stderrBuffer.push(line);
|
|
580
|
+
if (stderrBuffer.length > STDERR_BUFFER_LIMIT)
|
|
581
|
+
stderrBuffer.shift();
|
|
582
|
+
log.debug(`[Chromium stderr] ${line}`);
|
|
583
|
+
// Puppeteer-style readiness: Chromium prints the browser-level CDP
|
|
584
|
+
// WebSocket endpoint here once its remote-debugging server is
|
|
585
|
+
// accepting connections. Parsing it is the exact moment the browser
|
|
586
|
+
// is connectable - no TCP-port poll or fixed pre-connect delay
|
|
587
|
+
// needed.
|
|
588
|
+
const wsMatch = line.match(/DevTools listening on (ws:\S+)/);
|
|
589
|
+
if (wsMatch) {
|
|
590
|
+
proc.resolveWsEndpoint(wsMatch[1]);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
// Surface the buffered stderr if Chrome exits before something else
|
|
595
|
+
// gets a chance to inspect it (typical crash-on-startup signature), and
|
|
596
|
+
// fail the readiness promise fast with that same tail so a dead binary
|
|
597
|
+
// never presents as a generic connect timeout.
|
|
598
|
+
browserProcess.once('exit', (code, signal) => {
|
|
599
|
+
if (stderrLeftover) {
|
|
600
|
+
stderrBuffer.push(stderrLeftover);
|
|
601
|
+
stderrLeftover = '';
|
|
602
|
+
}
|
|
603
|
+
const tail = stderrBuffer.slice(-20).join('\n');
|
|
604
|
+
if (code !== 0 && code !== null) {
|
|
605
|
+
log.warning(`Chromium exited with code=${code} signal=${signal}. `
|
|
606
|
+
+ `Last stderr lines:\n${tail || '(empty - Chrome wrote nothing to stderr)'}`);
|
|
607
|
+
}
|
|
608
|
+
// No-op once the ws line was already seen (settled-once guard).
|
|
609
|
+
proc.rejectWsEndpoint(new Error(`Chromium exited (code=${code} signal=${signal}) before the CDP WebSocket endpoint was ready. `
|
|
610
|
+
+ `Last stderr lines:\n${tail || '(empty - Chrome wrote nothing to stderr)'}`));
|
|
611
|
+
});
|
|
612
|
+
// A spawn-level failure (missing binary, EACCES, …) fires `error` and may
|
|
613
|
+
// never fire `exit`, so reject readiness here too. Also settled-once.
|
|
614
|
+
browserProcess.once('error', (err) => {
|
|
615
|
+
const tail = stderrBuffer.slice(-20).join('\n');
|
|
616
|
+
proc.rejectWsEndpoint(new Error(`Browser process failed to start before the CDP WebSocket endpoint was ready: ${err.message}${tail ? `\nLast stderr lines:\n${tail}` : ''}`));
|
|
617
|
+
});
|
|
618
|
+
// Track the temp fingerprint config file on the BrowserProcess so it is
|
|
619
|
+
// removed both on normal exit (via cleanup()) and on a SIGINT/SIGTERM
|
|
620
|
+
// signal (via the signal-cleanup registry), not just the exit event -
|
|
621
|
+
// which never fires when the process is interrupted before teardown.
|
|
274
622
|
if (fingerprintConfigPath) {
|
|
275
|
-
|
|
276
|
-
browserProcess.once('exit', () => {
|
|
277
|
-
fs.unlink(configPath).catch(() => { });
|
|
278
|
-
});
|
|
623
|
+
proc.trackFingerprintConfigPath(fingerprintConfigPath);
|
|
279
624
|
}
|
|
280
625
|
return proc;
|
|
281
626
|
}
|
|
@@ -284,7 +629,18 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
|
|
|
284
629
|
throw new Error(`Error launching the browser: ${error}`);
|
|
285
630
|
}
|
|
286
631
|
};
|
|
287
|
-
|
|
632
|
+
// Cache the resolved writable base directory for the process lifetime. The
|
|
633
|
+
// mkdir + write-test probe below only depends on the filesystem layout and the
|
|
634
|
+
// process's cwd/home, none of which change underneath us mid-process, so the
|
|
635
|
+
// probe is run once and its result reused. Only a SUCCESSFUL probe is cached -
|
|
636
|
+
// a transient failure (e.g. a full disk during the first launch) must be
|
|
637
|
+
// retried on the next launch rather than pinned for the whole process. Each
|
|
638
|
+
// launch still gets its own fresh user-data-dir via fs.mkdtemp under the base.
|
|
639
|
+
let cachedBaseTempDir = null;
|
|
640
|
+
const resolveWritableBaseTempDir = async () => {
|
|
641
|
+
if (cachedBaseTempDir) {
|
|
642
|
+
return cachedBaseTempDir;
|
|
643
|
+
}
|
|
288
644
|
// Try to create temp directory in order of preference:
|
|
289
645
|
// 1. Project's temp directory (if we're in a project)
|
|
290
646
|
// 2. User's home directory temp folder
|
|
@@ -297,7 +653,6 @@ export const createTempUserDataDir = async () => {
|
|
|
297
653
|
// System temp directory (fallback)
|
|
298
654
|
path.join(os.tmpdir(), 'cdp-crawler-browsers'),
|
|
299
655
|
];
|
|
300
|
-
let baseTempDir = null;
|
|
301
656
|
for (const candidate of tempDirCandidates) {
|
|
302
657
|
try {
|
|
303
658
|
await fs.mkdir(candidate, { recursive: true });
|
|
@@ -305,17 +660,18 @@ export const createTempUserDataDir = async () => {
|
|
|
305
660
|
const testFile = path.join(candidate, '.write-test');
|
|
306
661
|
await fs.writeFile(testFile, 'test');
|
|
307
662
|
await fs.unlink(testFile);
|
|
308
|
-
|
|
309
|
-
|
|
663
|
+
cachedBaseTempDir = candidate;
|
|
664
|
+
return candidate;
|
|
310
665
|
}
|
|
311
666
|
catch (error) {
|
|
312
667
|
log.debug(`Cannot use temp directory ${candidate}: ${error}`);
|
|
313
668
|
continue;
|
|
314
669
|
}
|
|
315
670
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
671
|
+
throw new Error('Could not find a writable directory for browser profiles');
|
|
672
|
+
};
|
|
673
|
+
export const createTempUserDataDir = async () => {
|
|
674
|
+
const baseTempDir = await resolveWritableBaseTempDir();
|
|
319
675
|
const tempDirPrefix = path.join(baseTempDir, 'browser_');
|
|
320
676
|
const userDataDir = await fs.mkdtemp(tempDirPrefix);
|
|
321
677
|
log.debug(`Created browser profile directory: ${userDataDir}`);
|
|
@@ -326,7 +682,7 @@ export const launchChromeBrowserWithNewProfile = async (options = {}) => {
|
|
|
326
682
|
// CDPBrowserPlugin._launch() (resolveAntiDetectBrowserPath throws there if the
|
|
327
683
|
// binary is missing). Direct callers of this function without native stealth
|
|
328
684
|
// still fall through to the system Chrome lookup.
|
|
329
|
-
let executablePath = options
|
|
685
|
+
let { executablePath } = options;
|
|
330
686
|
if (!executablePath) {
|
|
331
687
|
const wantsNativeStealth = options.useNativeStealth || options.fingerprintOptions?.useNativeStealth;
|
|
332
688
|
if (wantsNativeStealth) {
|