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
package/dist/browser-plugin.js
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import CDP from 'chrome-remote-interface';
|
|
2
5
|
import { BrowserPlugin, log } from 'crawlee';
|
|
3
6
|
import CDPBrowserController from './browser-controller.js';
|
|
4
7
|
import { launchChromeBrowserWithNewProfile } from './browser-process/browser.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
+
import { prepareForLaunch } from './browser-process/fingerprint-config.js';
|
|
9
|
+
import { resolveAntiDetectBrowserPath } from './browser-process/get-chrome-executable.js';
|
|
10
|
+
import { PortAllocator } from './browser-process/port-allocator.js';
|
|
11
|
+
import { ensureWidevineCdm } from './browser-process/widevine-cdm.js';
|
|
8
12
|
import { fetchProxyCountry, resolveLocale } from './fingerprinting/locale-resolver.js';
|
|
9
|
-
import { CdpLauncher } from './launcher-wrap.js';
|
|
10
13
|
import { anonymizeProxySugar } from './proxy/anonymize-proxy.js';
|
|
14
|
+
import { untarUserDataDir } from './session/tar-data-dir.js';
|
|
15
|
+
import { assertValidBundle } from './session/types.js';
|
|
16
|
+
import { resolveUseNativeStealth } from './session-profile.js';
|
|
17
|
+
/**
|
|
18
|
+
* Type-only marker satisfying Crawlee's BrowserPlugin generic. CDP has no
|
|
19
|
+
* Puppeteer-style library object - the connection happens directly in
|
|
20
|
+
* CDPBrowserPlugin._openCdpConnection, so `launch` is never invoked at runtime;
|
|
21
|
+
* the declaration exists purely so Crawlee can infer LibraryOptions /
|
|
22
|
+
* LaunchResult from the generic. Constructable (`new CdpLauncher()`) because the
|
|
23
|
+
* Crawler and the e2e harness pass an instance to the plugin constructor; the
|
|
24
|
+
* body is intentionally empty rather than the former throw-only stub.
|
|
25
|
+
*/
|
|
26
|
+
export class CdpLauncher {
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Overall budget for the browser to print its "DevTools listening on ws://…"
|
|
30
|
+
* line and become connectable. Covers a slow cold start (first launch of the
|
|
31
|
+
* anti-detect binary, cold page cache) while still failing in bounded time if
|
|
32
|
+
* the process is wedged without having exited. The process-exit rejection is
|
|
33
|
+
* handled separately by the endpoint promise itself, so this only fires for a
|
|
34
|
+
* genuine hang.
|
|
35
|
+
*/
|
|
36
|
+
const CDP_READINESS_TIMEOUT_MS = 30000;
|
|
11
37
|
class CDPBrowserPlugin extends BrowserPlugin {
|
|
12
38
|
/**
|
|
13
39
|
* Accept flat LaunchBrowserOptions directly (not nested inside BrowserPluginOptions).
|
|
@@ -17,117 +43,20 @@ class CDPBrowserPlugin extends BrowserPlugin {
|
|
|
17
43
|
*/
|
|
18
44
|
constructor(library, launchOptions) {
|
|
19
45
|
super(library, { launchOptions: (launchOptions ?? {}) });
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Increment the last assigned port, wrapping around if necessary
|
|
33
|
-
CDPBrowserPlugin.lastAssignedPort++;
|
|
34
|
-
if (CDPBrowserPlugin.lastAssignedPort > CDPBrowserPlugin.PORT_RANGE.max) {
|
|
35
|
-
CDPBrowserPlugin.lastAssignedPort = CDPBrowserPlugin.PORT_RANGE.min;
|
|
36
|
-
log.debug('Port range wrapped around, starting from beginning');
|
|
37
|
-
}
|
|
38
|
-
const candidatePort = CDPBrowserPlugin.lastAssignedPort;
|
|
39
|
-
// Skip if port is already tracked as in use
|
|
40
|
-
if (CDPBrowserPlugin.usedPorts.has(candidatePort)) {
|
|
41
|
-
attempts++;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
// Check if port is actually available
|
|
45
|
-
if (await this._isPortAvailable(candidatePort)) {
|
|
46
|
-
CDPBrowserPlugin.usedPorts.add(candidatePort);
|
|
47
|
-
log.debug(`Reserved port ${candidatePort} for browser instance (attempt ${attempts + 1})`);
|
|
48
|
-
return candidatePort;
|
|
49
|
-
}
|
|
50
|
-
// Port is not available, mark it as used temporarily to avoid immediate retry
|
|
51
|
-
CDPBrowserPlugin.usedPorts.add(candidatePort);
|
|
52
|
-
setTimeout(() => {
|
|
53
|
-
CDPBrowserPlugin.usedPorts.delete(candidatePort);
|
|
54
|
-
}, 5000); // Remove from used ports after 5 seconds
|
|
55
|
-
}
|
|
56
|
-
finally {
|
|
57
|
-
// Always release the lock
|
|
58
|
-
CDPBrowserPlugin.portLock = false;
|
|
59
|
-
}
|
|
60
|
-
attempts++;
|
|
61
|
-
}
|
|
62
|
-
// If we've tried all ports and none are available, try to clean up stale ports
|
|
63
|
-
log.warning(`All ports in range ${CDPBrowserPlugin.PORT_RANGE.min}-${CDPBrowserPlugin.PORT_RANGE.max} are in use. Attempting cleanup.`);
|
|
64
|
-
await this._cleanupStalePorts();
|
|
65
|
-
// Try once more after cleanup
|
|
66
|
-
for (let port = CDPBrowserPlugin.PORT_RANGE.min; port <= CDPBrowserPlugin.PORT_RANGE.max; port++) {
|
|
67
|
-
if (!CDPBrowserPlugin.usedPorts.has(port) && await this._isPortAvailable(port)) {
|
|
68
|
-
CDPBrowserPlugin.usedPorts.add(port);
|
|
69
|
-
log.debug(`Reserved port ${port} after cleanup`);
|
|
70
|
-
return port;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
throw new Error(`No available ports found in the configured range ${CDPBrowserPlugin.PORT_RANGE.min}-${CDPBrowserPlugin.PORT_RANGE.max} after maximum attempts and cleanup`);
|
|
74
|
-
}
|
|
75
|
-
async _cleanupStalePorts() {
|
|
76
|
-
log.debug('Cleaning up stale ports');
|
|
77
|
-
const portsToCheck = Array.from(CDPBrowserPlugin.usedPorts);
|
|
78
|
-
for (const port of portsToCheck) {
|
|
79
|
-
if (await this._isPortAvailable(port)) {
|
|
80
|
-
CDPBrowserPlugin.usedPorts.delete(port);
|
|
81
|
-
log.debug(`Cleaned up stale port ${port}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
async _isPortAvailable(port) {
|
|
86
|
-
return new Promise((resolve) => {
|
|
87
|
-
const server = net.createServer();
|
|
88
|
-
let resolved = false;
|
|
89
|
-
const cleanup = () => {
|
|
90
|
-
if (!resolved) {
|
|
91
|
-
resolved = true;
|
|
92
|
-
try {
|
|
93
|
-
server.close();
|
|
94
|
-
}
|
|
95
|
-
catch (e) {
|
|
96
|
-
// Ignore errors during cleanup
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
server.once('error', () => {
|
|
101
|
-
cleanup();
|
|
102
|
-
resolve(false);
|
|
103
|
-
});
|
|
104
|
-
server.once('listening', () => {
|
|
105
|
-
cleanup();
|
|
106
|
-
resolve(true);
|
|
107
|
-
});
|
|
108
|
-
// Add timeout to prevent hanging
|
|
109
|
-
setTimeout(() => {
|
|
110
|
-
cleanup();
|
|
111
|
-
resolve(false);
|
|
112
|
-
}, 2000); // Reduced timeout
|
|
113
|
-
try {
|
|
114
|
-
server.listen(port, '127.0.0.1');
|
|
115
|
-
}
|
|
116
|
-
catch (e) {
|
|
117
|
-
cleanup();
|
|
118
|
-
resolve(false);
|
|
119
|
-
}
|
|
46
|
+
/**
|
|
47
|
+
* Debug-port pool: acquisition serialization, wraparound scan, probe-busy
|
|
48
|
+
* quarantine, and stale-port sweep all live in PortAllocator. The plugin
|
|
49
|
+
* only acquires on launch and releases from the browser event handlers.
|
|
50
|
+
* One pool per process (matching the former static state): two crawlers in
|
|
51
|
+
* one process - e.g. a session-bundle restore - must not race the same range.
|
|
52
|
+
*/
|
|
53
|
+
Object.defineProperty(this, "portAllocator", {
|
|
54
|
+
enumerable: true,
|
|
55
|
+
configurable: true,
|
|
56
|
+
writable: true,
|
|
57
|
+
value: CDPBrowserPlugin.sharedPortAllocator
|
|
120
58
|
});
|
|
121
59
|
}
|
|
122
|
-
releasePort(port) {
|
|
123
|
-
if (CDPBrowserPlugin.usedPorts.has(port)) {
|
|
124
|
-
CDPBrowserPlugin.usedPorts.delete(port);
|
|
125
|
-
log.debug(`Released port ${port} back to pool. Used ports: ${CDPBrowserPlugin.usedPorts.size}/${CDPBrowserPlugin.PORT_RANGE.max - CDPBrowserPlugin.PORT_RANGE.min + 1}`);
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
log.debug(`Attempted to release port ${port} that was not in use`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
60
|
/**
|
|
132
61
|
*/
|
|
133
62
|
async _addProxyToLaunchOptions() {
|
|
@@ -148,211 +77,149 @@ class CDPBrowserPlugin extends BrowserPlugin {
|
|
|
148
77
|
* @private
|
|
149
78
|
*/
|
|
150
79
|
async _launch(launchContext) {
|
|
151
|
-
const debuggingPort = await this.
|
|
80
|
+
const debuggingPort = await this.portAllocator.acquire();
|
|
152
81
|
let browserProcess;
|
|
82
|
+
// Idempotent teardown for the anonymize proxy server. Reassigned the
|
|
83
|
+
// moment the anonymize promise exists; a no-op until then so the outer
|
|
84
|
+
// catch can always call it even when we fail before proxy setup.
|
|
85
|
+
let closeAnonymizedProxyOnce = async () => { };
|
|
153
86
|
try {
|
|
154
|
-
const { launchOptions, useIncognitoPages, proxyUrl
|
|
155
|
-
|
|
156
|
-
launchOptions.browserType = "chrome";
|
|
157
|
-
}
|
|
158
|
-
// useNativeStealth may be set in three places (canonical, nested, or at
|
|
159
|
-
// launchContext root) — normalize to launchOptions.useNativeStealth so the
|
|
160
|
-
// rest of the pipeline reads one source of truth.
|
|
161
|
-
const nativeStealthRequested = !!launchOptions.useNativeStealth ||
|
|
162
|
-
!!launchOptions.fingerprintOptions?.useNativeStealth ||
|
|
163
|
-
!!launchContext?.useNativeStealth;
|
|
164
|
-
if (nativeStealthRequested) {
|
|
165
|
-
launchOptions.useNativeStealth = true;
|
|
166
|
-
}
|
|
167
|
-
// Auto-resolve the anti-detect binary so consumers can simply pass
|
|
168
|
-
// `useNativeStealth: true` without having to walk node_modules to find it.
|
|
169
|
-
// If the user pinned an explicit executablePath, respect it.
|
|
170
|
-
if (launchOptions.useNativeStealth && !launchOptions.executablePath) {
|
|
171
|
-
launchOptions.executablePath = resolveAntiDetectBrowserPath();
|
|
172
|
-
log.info('[AntiDetect] Auto-resolved anti-detect binary', {
|
|
173
|
-
executablePath: launchOptions.executablePath,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
// Extract screen dimensions from fingerprint if available and not already set
|
|
177
|
-
if (fingerprint?.fingerprint?.screen && !launchOptions.windowWidth && !launchOptions.windowHeight) {
|
|
178
|
-
const screen = fingerprint.fingerprint.screen;
|
|
179
|
-
launchOptions.windowWidth = screen.width || 1920;
|
|
180
|
-
launchOptions.windowHeight = screen.height || 1080;
|
|
181
|
-
log.info('Using fingerprint screen dimensions for browser window', {
|
|
182
|
-
width: launchOptions.windowWidth,
|
|
183
|
-
height: launchOptions.windowHeight,
|
|
184
|
-
devicePixelRatio: screen.devicePixelRatio,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
else if (launchOptions.windowWidth && launchOptions.windowHeight) {
|
|
188
|
-
log.info('Using explicitly set window dimensions', {
|
|
189
|
-
width: launchOptions.windowWidth,
|
|
190
|
-
height: launchOptions.windowHeight,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
log.warning('No fingerprint screen dimensions and no explicit dimensions - will use defaults or maximize');
|
|
195
|
-
}
|
|
87
|
+
const { launchOptions, useIncognitoPages, proxyUrl } = launchContext;
|
|
88
|
+
this._normalizeLaunchIdentity(launchContext);
|
|
196
89
|
let browser;
|
|
197
90
|
launchOptions.useIncognitoPages = useIncognitoPages;
|
|
198
91
|
log.debug("Launch options", { ...launchOptions, debuggingPort });
|
|
199
|
-
|
|
92
|
+
// If the caller handed us a previously-captured session bundle, hydrate
|
|
93
|
+
// launch state from it BEFORE any locale resolution / fingerprint
|
|
94
|
+
// generation runs - so the downstream code paths see the bundle's values
|
|
95
|
+
// as if the user had set them by hand and we never call the generators.
|
|
96
|
+
// This stays serial and ahead of the parallel block below because hydration
|
|
97
|
+
// may pin resolvedLocale / userDataDir that _ensureResolvedLocale then reads.
|
|
98
|
+
const { sessionBundle } = launchOptions;
|
|
99
|
+
if (sessionBundle) {
|
|
100
|
+
await this._hydrateFromSessionBundle(launchContext, sessionBundle);
|
|
101
|
+
}
|
|
102
|
+
// Proxy anonymization and locale resolution are independent launch I/O:
|
|
103
|
+
// both only READ the original proxyUrl and write to disjoint targets
|
|
104
|
+
// (anonymize returns [url, close]; ensureLocale writes launchOptions.resolvedLocale).
|
|
105
|
+
// Run them concurrently to cut one round-trip off the launch path.
|
|
106
|
+
//
|
|
107
|
+
// The anonymize close handle must be reachable from every cleanup path
|
|
108
|
+
// even if a *later* await in this method throws before we destructure
|
|
109
|
+
// the result - including the race where locale resolution rejects
|
|
110
|
+
// before anonymize has resolved. So we hold the anonymize promise in
|
|
111
|
+
// _launch scope and wrap its close in a single idempotent guard that
|
|
112
|
+
// awaits the promise itself: whoever fires first (inner catch, outer
|
|
113
|
+
// catch, or a browser close/error/disconnect handler) closes the
|
|
114
|
+
// server exactly once, and a proxy that resolves only after the
|
|
115
|
+
// failure is still torn down rather than leaked.
|
|
116
|
+
const anonymizePromise = anonymizeProxySugar(proxyUrl);
|
|
117
|
+
let proxyClosed = false;
|
|
118
|
+
closeAnonymizedProxyOnce = async () => {
|
|
119
|
+
if (proxyClosed)
|
|
120
|
+
return;
|
|
121
|
+
proxyClosed = true;
|
|
122
|
+
try {
|
|
123
|
+
const [, close] = await anonymizePromise;
|
|
124
|
+
await close();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// anonymizeProxySugar itself rejected - nothing was started,
|
|
128
|
+
// so there is nothing to close.
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const [[anonymizedProxyUrl]] = await Promise.all([
|
|
132
|
+
anonymizePromise,
|
|
133
|
+
this._ensureResolvedLocale(launchOptions, proxyUrl, launchContext.fingerprint),
|
|
134
|
+
]);
|
|
200
135
|
if (anonymizedProxyUrl) {
|
|
201
136
|
launchOptions.proxyUrl = anonymizedProxyUrl;
|
|
202
137
|
}
|
|
203
|
-
// Resolve the locale triple once per launch so native-stealth args, CDP overrides,
|
|
204
|
-
// and the FingerprintConfigJson all read the same { locale, languages, timezone }.
|
|
205
|
-
// Priority: explicit fingerprintOptions.locale → proxy country (via ipinfo.io) →
|
|
206
|
-
// fingerprint-generator language → 'en-US'.
|
|
207
|
-
{
|
|
208
|
-
const fpOpts = launchOptions.fingerprintOptions;
|
|
209
|
-
const fingerprintLanguage = fingerprint?.headers
|
|
210
|
-
? (fingerprint.headers['accept-language'] ?? fingerprint.headers['Accept-Language'])
|
|
211
|
-
: undefined;
|
|
212
|
-
// Only pay the ipinfo.io roundtrip if we actually need it — skip when the user
|
|
213
|
-
// has already pinned an explicit locale.
|
|
214
|
-
const proxyCountry = fpOpts?.locale ? undefined : await fetchProxyCountry(proxyUrl);
|
|
215
|
-
launchOptions.resolvedLocale = resolveLocale({
|
|
216
|
-
locale: fpOpts?.locale,
|
|
217
|
-
timezone: fpOpts?.timezone,
|
|
218
|
-
languages: fpOpts?.languages,
|
|
219
|
-
proxyCountry,
|
|
220
|
-
fingerprintLanguage: fingerprintLanguage?.split(',')[0]?.trim(),
|
|
221
|
-
});
|
|
222
|
-
log.debug('[Locale] Resolved locale triple', {
|
|
223
|
-
locale: launchOptions.resolvedLocale.locale,
|
|
224
|
-
timezone: launchOptions.resolvedLocale.timezone,
|
|
225
|
-
languages: launchOptions.resolvedLocale.languages,
|
|
226
|
-
source: fpOpts?.locale ? 'user' : proxyCountry ? 'proxy' : 'fingerprint',
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
138
|
// Pre-generate a per-session C++ fingerprint config for the anti-detect browser.
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const _fpOpts = launchOptions.fingerprintOptions;
|
|
235
|
-
const proxySeedKey = proxyUrl
|
|
236
|
-
? (proxyUrl.match(/session-([^,&]+)/i)?.[1] ?? null)
|
|
237
|
-
: null;
|
|
238
|
-
// If non-Apify real-device profiles are requested, generate the profile now
|
|
239
|
-
// so C++ gets the same profile that the fingerprint injector will use.
|
|
240
|
-
let fpForConfig = fingerprint;
|
|
241
|
-
if (_fpOpts?.useNonApifyFingerprints) {
|
|
242
|
-
// User-supplied fingerprintOptions.platform wins; falls back to
|
|
243
|
-
// whatever the existing Crawlee-generated fingerprint says.
|
|
244
|
-
const userPlatform = _fpOpts?.platform;
|
|
245
|
-
const osHint = (() => {
|
|
246
|
-
const p = (fingerprint?.fingerprint?.navigator?.platform || '').toLowerCase();
|
|
247
|
-
if (p.includes('mac'))
|
|
248
|
-
return 'macos';
|
|
249
|
-
if (p.includes('linux'))
|
|
250
|
-
return 'linux';
|
|
251
|
-
return 'windows';
|
|
252
|
-
})();
|
|
253
|
-
const nonApifyFp = await generateNonApifyFingerprint({
|
|
254
|
-
platform: userPlatform,
|
|
255
|
-
os: osHint,
|
|
256
|
-
proxySeedKey,
|
|
257
|
-
});
|
|
258
|
-
fpForConfig = nonApifyFp;
|
|
259
|
-
// Store for browser-controller._newPage() — it will reuse this instead of
|
|
260
|
-
// generating a different profile (session consistency).
|
|
261
|
-
launchContext.fingerprint = fpForConfig;
|
|
262
|
-
}
|
|
263
|
-
if (fpForConfig) {
|
|
264
|
-
try {
|
|
265
|
-
const cfg = generateFingerprintConfig(fpForConfig, proxySeedKey ?? undefined, launchOptions.resolvedLocale);
|
|
266
|
-
// Align UA version with actual binary before writing the config.
|
|
267
|
-
const binaryPath = getAntiDetectBrowserPath() ?? launchOptions.executablePath;
|
|
268
|
-
if (binaryPath) {
|
|
269
|
-
const binaryVersion = getChromeBinaryVersion(binaryPath);
|
|
270
|
-
if (binaryVersion) {
|
|
271
|
-
const major = binaryVersion.split('.')[0];
|
|
272
|
-
if (cfg.navigator.userAgent) {
|
|
273
|
-
cfg.navigator.userAgent = cfg.navigator.userAgent.replace(/Chrome\/\d+\.\d+\.\d+\.\d+/, `Chrome/${major}.0.0.0`);
|
|
274
|
-
}
|
|
275
|
-
if (cfg.navigator.userAgentData) {
|
|
276
|
-
cfg.navigator.userAgentData.uaFullVersion = binaryVersion;
|
|
277
|
-
cfg.navigator.userAgentData.brands = cfg.navigator.userAgentData.brands.map(b => b.brand === 'Not_A Brand' ? b : { ...b, version: major, full_version: binaryVersion });
|
|
278
|
-
cfg.navigator.userAgentData.fullVersionList = cfg.navigator.userAgentData.brands;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
launchOptions.fingerprintConfig = cfg;
|
|
283
|
-
log.debug('[AntiDetect] Pre-generated per-session fingerprint config', {
|
|
284
|
-
platform: cfg.navigator.platform,
|
|
285
|
-
gpu: cfg.webgl.renderer.substring(0, 60),
|
|
286
|
-
screen: `${cfg.screen.width}x${cfg.screen.height}`,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
catch (cfgErr) {
|
|
290
|
-
log.debug('[AntiDetect] Could not pre-generate fingerprint config:', { error: cfgErr });
|
|
291
|
-
}
|
|
292
|
-
}
|
|
139
|
+
// Skipped when a sessionBundle pre-supplied the config - that is the whole point
|
|
140
|
+
// of byte-for-byte replay.
|
|
141
|
+
if (launchOptions.useNativeStealth && !sessionBundle) {
|
|
142
|
+
await this._prepareNativeFingerprintConfig(launchContext);
|
|
293
143
|
}
|
|
294
144
|
browserProcess = await this._launchBrowser(debuggingPort, launchOptions);
|
|
295
145
|
log.debug("Browser process started", { pid: browserProcess.pid, debuggingPort });
|
|
296
146
|
try {
|
|
297
|
-
browser = await this.
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
// Add error handler to ensure cleanup
|
|
314
|
-
browser.on('error', async (error) => {
|
|
147
|
+
browser = await this._connectViaWsEndpoint(browserProcess, debuggingPort);
|
|
148
|
+
// Single once-guarded teardown wired to every terminal browser
|
|
149
|
+
// event (close / error / disconnect). Any of them can fire - and
|
|
150
|
+
// more than one can fire for the same instance - so the guard
|
|
151
|
+
// ensures the port is released and the anonymize proxy closed
|
|
152
|
+
// exactly once regardless of which event(s) arrive. Both
|
|
153
|
+
// portAllocator.release and closeAnonymizedProxyOnce are already
|
|
154
|
+
// idempotent; the guard additionally avoids duplicate log lines
|
|
155
|
+
// and redundant work.
|
|
156
|
+
let teardownDone = false;
|
|
157
|
+
const teardown = async (reason) => {
|
|
158
|
+
if (teardownDone)
|
|
159
|
+
return;
|
|
160
|
+
teardownDone = true;
|
|
315
161
|
try {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
await close();
|
|
320
|
-
}
|
|
162
|
+
this.portAllocator.release(debuggingPort);
|
|
163
|
+
await closeAnonymizedProxyOnce();
|
|
164
|
+
log.debug(`Browser on port ${debuggingPort} cleaned up (${reason})`);
|
|
321
165
|
}
|
|
322
166
|
catch (cleanupError) {
|
|
323
|
-
log.
|
|
324
|
-
// Still release the port even if other cleanup fails
|
|
325
|
-
this.
|
|
167
|
+
log.warning(`Error during browser ${reason} cleanup on port ${debuggingPort}:`, { error: cleanupError });
|
|
168
|
+
// Still release the port even if other cleanup fails.
|
|
169
|
+
this.portAllocator.release(debuggingPort);
|
|
326
170
|
}
|
|
171
|
+
};
|
|
172
|
+
browser.on('close', async () => teardown('close'));
|
|
173
|
+
browser.on('error', async (error) => {
|
|
174
|
+
log.warning(`Browser error on port ${debuggingPort}:`, error);
|
|
175
|
+
return teardown('error');
|
|
327
176
|
});
|
|
328
|
-
// Add disconnect handler for CDP connection issues
|
|
329
177
|
browser.on('disconnect', async () => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
178
|
+
log.warning(`Browser disconnected on port ${debuggingPort}, cleaning up`);
|
|
179
|
+
return teardown('disconnect');
|
|
180
|
+
});
|
|
181
|
+
// Widevine, before the first navigation rather than after it. Real
|
|
182
|
+
// Chrome bundles the CDM and reports support immediately; ours has to
|
|
183
|
+
// fetch it, and only a FOREGROUND component check is served (#141).
|
|
184
|
+
// Doing it here means a page never observes the pre-CDM state where
|
|
185
|
+
// `requestMediaKeySystemAccess('com.widevine.alpha')` rejects - a
|
|
186
|
+
// divergence a detector needs one line to find. Cold cache costs this
|
|
187
|
+
// once per host; after that the profile is seeded before launch.
|
|
188
|
+
if (launchOptions.useNativeStealth) {
|
|
189
|
+
const widevine = await ensureWidevineCdm({
|
|
190
|
+
session: browser,
|
|
191
|
+
userDataDir: browserProcess.getUserDataDir(),
|
|
192
|
+
});
|
|
193
|
+
const detail = { ...widevine, port: debuggingPort };
|
|
194
|
+
if (widevine.usableThisSession) {
|
|
195
|
+
log.debug('[AntiDetect] Widevine CDM ready before first navigation', detail);
|
|
336
196
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
//
|
|
340
|
-
|
|
197
|
+
else if (widevine.outcome === 'installed-pending-restart') {
|
|
198
|
+
// Linux binds the CDM at startup, so this session still reports
|
|
199
|
+
// no Widevine; the profile and the host cache now have it, so
|
|
200
|
+
// every later launch is correct from the first navigation.
|
|
201
|
+
log.debug('[AntiDetect] Widevine CDM fetched; active from the next launch (Linux startup binding)', detail);
|
|
341
202
|
}
|
|
342
|
-
|
|
203
|
+
else {
|
|
204
|
+
log.debug('[AntiDetect] Widevine CDM unavailable this session', detail);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
343
207
|
launchContext.debuggingPort = debuggingPort;
|
|
344
208
|
launchContext.process = browserProcess;
|
|
345
209
|
return browser;
|
|
346
210
|
}
|
|
347
211
|
catch (error) {
|
|
348
|
-
|
|
349
|
-
await close();
|
|
350
|
-
}
|
|
212
|
+
await closeAnonymizedProxyOnce();
|
|
351
213
|
throw error;
|
|
352
214
|
}
|
|
353
215
|
}
|
|
354
216
|
catch (error) {
|
|
355
|
-
this.
|
|
217
|
+
this.portAllocator.release(debuggingPort);
|
|
218
|
+
// Close the anonymize proxy on any failure before the browser's own
|
|
219
|
+
// handlers were wired - covers a locale-resolution / fingerprint /
|
|
220
|
+
// launch failure that rejected the outer try before we ever connected.
|
|
221
|
+
// Idempotent, so it is a no-op if the inner catch already ran it.
|
|
222
|
+
await closeAnonymizedProxyOnce();
|
|
356
223
|
// Ensure browser process is killed if it was started
|
|
357
224
|
if (browserProcess) {
|
|
358
225
|
try {
|
|
@@ -365,6 +232,155 @@ class CDPBrowserPlugin extends BrowserPlugin {
|
|
|
365
232
|
throw error;
|
|
366
233
|
}
|
|
367
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Phase 1 of _launch - normalize the launch identity in place:
|
|
237
|
+
* default browserType, normalize useNativeStealth from its three legacy
|
|
238
|
+
* locations, auto-resolve the anti-detect binary, and adopt the
|
|
239
|
+
* fingerprint's screen dimensions when the caller didn't pin any.
|
|
240
|
+
*/
|
|
241
|
+
_normalizeLaunchIdentity(launchContext) {
|
|
242
|
+
const { launchOptions, fingerprint } = launchContext;
|
|
243
|
+
if (!launchOptions.browserType) {
|
|
244
|
+
launchOptions.browserType = "chrome";
|
|
245
|
+
}
|
|
246
|
+
// Default to the non-Apify real-device fingerprint pool. Real Chrome
|
|
247
|
+
// device profiles unblock materially better than the synthetic Apify
|
|
248
|
+
// pool, so `useNonApifyFingerprints` defaults ON. Only fill it when the
|
|
249
|
+
// caller left it unset (explicit `false` is honored), and never for a
|
|
250
|
+
// session-bundle restore - that path pins its own captured fingerprint
|
|
251
|
+
// and must not re-roll from any pool. Clone the nested bag so we default
|
|
252
|
+
// on our per-launch copy, never the caller's object (issue #80).
|
|
253
|
+
if (!launchOptions.sessionBundle) {
|
|
254
|
+
const fpOpts = { ...(launchOptions.fingerprintOptions ?? {}) };
|
|
255
|
+
if (fpOpts.useNonApifyFingerprints === undefined) {
|
|
256
|
+
fpOpts.useNonApifyFingerprints = true;
|
|
257
|
+
}
|
|
258
|
+
launchOptions.fingerprintOptions = fpOpts;
|
|
259
|
+
}
|
|
260
|
+
// useNativeStealth may be set in three places (canonical, nested, or at
|
|
261
|
+
// launchContext root) - normalize to launchOptions.useNativeStealth so the
|
|
262
|
+
// rest of the pipeline reads one source of truth.
|
|
263
|
+
const nativeStealthRequested = resolveUseNativeStealth(launchOptions, launchContext);
|
|
264
|
+
if (nativeStealthRequested) {
|
|
265
|
+
launchOptions.useNativeStealth = true;
|
|
266
|
+
}
|
|
267
|
+
// Auto-resolve the anti-detect binary so consumers can simply pass
|
|
268
|
+
// `useNativeStealth: true` without having to walk node_modules to find it.
|
|
269
|
+
// If the user pinned an explicit executablePath, respect it.
|
|
270
|
+
if (launchOptions.useNativeStealth && !launchOptions.executablePath) {
|
|
271
|
+
launchOptions.executablePath = resolveAntiDetectBrowserPath();
|
|
272
|
+
log.debug('[AntiDetect] Auto-resolved anti-detect binary', {
|
|
273
|
+
executablePath: launchOptions.executablePath,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
// Extract screen dimensions from fingerprint if available and not already set
|
|
277
|
+
if (fingerprint?.fingerprint?.screen && !launchOptions.windowWidth && !launchOptions.windowHeight) {
|
|
278
|
+
const { screen } = fingerprint.fingerprint;
|
|
279
|
+
launchOptions.windowWidth = screen.width || 1920;
|
|
280
|
+
launchOptions.windowHeight = screen.height || 1080;
|
|
281
|
+
log.info('Using fingerprint screen dimensions for browser window', {
|
|
282
|
+
width: launchOptions.windowWidth,
|
|
283
|
+
height: launchOptions.windowHeight,
|
|
284
|
+
devicePixelRatio: screen.devicePixelRatio,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
else if (launchOptions.windowWidth && launchOptions.windowHeight) {
|
|
288
|
+
log.debug('Using explicitly set window dimensions', {
|
|
289
|
+
width: launchOptions.windowWidth,
|
|
290
|
+
height: launchOptions.windowHeight,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
log.warning('No fingerprint screen dimensions and no explicit dimensions - will use defaults or maximize');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Phase 2 of _launch - hydrate launch state from a previously-captured
|
|
299
|
+
* session bundle: untar the user-data-dir, pin the resolved locale +
|
|
300
|
+
* fingerprintConfig verbatim, and replace the fingerprint so no generator
|
|
301
|
+
* runs (byte-for-byte replay).
|
|
302
|
+
*/
|
|
303
|
+
async _hydrateFromSessionBundle(launchContext, sessionBundle) {
|
|
304
|
+
const { launchOptions } = launchContext;
|
|
305
|
+
assertValidBundle(sessionBundle);
|
|
306
|
+
if (sessionBundle.userDataDir && !launchOptions.userDataDir) {
|
|
307
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdp-crawler-session-'));
|
|
308
|
+
await untarUserDataDir(sessionBundle.userDataDir, tempDir);
|
|
309
|
+
launchOptions.userDataDir = tempDir;
|
|
310
|
+
log.info('[session] Restored user-data-dir from bundle', {
|
|
311
|
+
tempDir,
|
|
312
|
+
sizeBytes: sessionBundle.userDataDir.sizeBytes,
|
|
313
|
+
capturedFiles: sessionBundle.userDataDir.capturedFiles,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
launchOptions.resolvedLocale = sessionBundle.resolvedLocale;
|
|
317
|
+
launchOptions.fingerprintConfig = sessionBundle.fingerprintConfig;
|
|
318
|
+
launchOptions.fingerprintOptions = {
|
|
319
|
+
...(launchOptions.fingerprintOptions ?? {}),
|
|
320
|
+
...sessionBundle.fingerprintInput,
|
|
321
|
+
useNonApifyFingerprints: false,
|
|
322
|
+
};
|
|
323
|
+
launchContext.fingerprint = sessionBundle.fingerprint;
|
|
324
|
+
log.info('[session] Hydrated launchContext from sessionBundle', {
|
|
325
|
+
cookies: sessionBundle.cookies.length,
|
|
326
|
+
localStorageOrigins: Object.keys(sessionBundle.localStorage).length,
|
|
327
|
+
sessionStorageOrigins: Object.keys(sessionBundle.sessionStorage).length,
|
|
328
|
+
locale: sessionBundle.resolvedLocale.locale,
|
|
329
|
+
timezone: sessionBundle.resolvedLocale.timezone,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Phase 3 of _launch - resolve the locale triple once per launch so
|
|
334
|
+
* native-stealth args, CDP overrides, and the FingerprintConfigJson all
|
|
335
|
+
* read the same { locale, languages, timezone }. No-op when a session
|
|
336
|
+
* bundle (or an earlier caller) already pinned the triple.
|
|
337
|
+
*/
|
|
338
|
+
async _ensureResolvedLocale(launchOptions, proxyUrl, fingerprint) {
|
|
339
|
+
// Resolve the locale triple once per launch so native-stealth args, CDP overrides,
|
|
340
|
+
// and the FingerprintConfigJson all read the same { locale, languages, timezone }.
|
|
341
|
+
// Priority: explicit fingerprintOptions.locale → proxy country (via ipinfo.io) →
|
|
342
|
+
// fingerprint-generator language → 'en-US'. Skipped when a sessionBundle already
|
|
343
|
+
// pinned the triple above.
|
|
344
|
+
if (!launchOptions.resolvedLocale) {
|
|
345
|
+
const fpOpts = launchOptions.fingerprintOptions;
|
|
346
|
+
const fingerprintLanguage = fingerprint?.headers
|
|
347
|
+
? (fingerprint.headers['accept-language'] ?? fingerprint.headers['Accept-Language'])
|
|
348
|
+
: undefined;
|
|
349
|
+
// Only pay the ipinfo.io roundtrip if we actually need it - skip when the user
|
|
350
|
+
// has already pinned an explicit locale.
|
|
351
|
+
const proxyCountry = fpOpts?.locale ? undefined : await fetchProxyCountry(proxyUrl);
|
|
352
|
+
launchOptions.resolvedLocale = resolveLocale({
|
|
353
|
+
locale: fpOpts?.locale,
|
|
354
|
+
timezone: fpOpts?.timezone,
|
|
355
|
+
languages: fpOpts?.languages,
|
|
356
|
+
proxyCountry,
|
|
357
|
+
fingerprintLanguage: fingerprintLanguage?.split(',')[0]?.trim(),
|
|
358
|
+
});
|
|
359
|
+
let localeSource;
|
|
360
|
+
if (fpOpts?.locale)
|
|
361
|
+
localeSource = 'user';
|
|
362
|
+
else if (proxyCountry)
|
|
363
|
+
localeSource = 'proxy';
|
|
364
|
+
else
|
|
365
|
+
localeSource = 'fingerprint';
|
|
366
|
+
log.debug('[Locale] Resolved locale triple', {
|
|
367
|
+
locale: launchOptions.resolvedLocale.locale,
|
|
368
|
+
timezone: launchOptions.resolvedLocale.timezone,
|
|
369
|
+
languages: launchOptions.resolvedLocale.languages,
|
|
370
|
+
source: localeSource,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Phase 4 of _launch - thin delegate to the FingerprintConfig module's
|
|
376
|
+
* pre-launch path. It selects a clean (uncontaminated, OS-matching) profile,
|
|
377
|
+
* generates + aligns the FingerprintConfigJson, stamps the config onto
|
|
378
|
+
* launchOptions for browser-process to write, and records the selection on
|
|
379
|
+
* the launch context for the post-launch path to reuse.
|
|
380
|
+
*/
|
|
381
|
+
async _prepareNativeFingerprintConfig(launchContext) {
|
|
382
|
+
await prepareForLaunch(launchContext);
|
|
383
|
+
}
|
|
368
384
|
/**
|
|
369
385
|
* Creates a new browser controller.
|
|
370
386
|
*
|
|
@@ -383,11 +399,27 @@ class CDPBrowserPlugin extends BrowserPlugin {
|
|
|
383
399
|
async _launchBrowser(debuggingPort, launchOptions) {
|
|
384
400
|
const { proxyUrl, browserType, ignoreHttpsErrors = true, headless, executablePath, useNativeStealth, useExperimentalFingerprints, fingerprintOptions, fingerprintConfig, maximizeWindow, windowWidth, windowHeight, userDataDir, keepUserDataDir, dnsOverHttpsServer, secureDnsMode, webrtcIpHandlingPolicy, } = launchOptions;
|
|
385
401
|
if (executablePath) {
|
|
386
|
-
log.
|
|
402
|
+
log.debug(`[AntiDetect] Launching browser binary: ${executablePath}`);
|
|
387
403
|
}
|
|
388
404
|
let browserProcess;
|
|
389
405
|
switch (browserType) {
|
|
390
406
|
case "chrome": {
|
|
407
|
+
// Anti-Detect 2026-05-18: when native stealth is active and the
|
|
408
|
+
// caller didn't pin an explicit policy, default to
|
|
409
|
+
// `disable_non_proxied_udp` - the strictest setting, which
|
|
410
|
+
// tells Chromium's WebRTC stack to refuse UDP traffic unless
|
|
411
|
+
// it's tunneled through the proxy. This prevents host/srflx
|
|
412
|
+
// ICE candidate GATHERING at the network layer (which would
|
|
413
|
+
// otherwise add the real LAN/public IP to
|
|
414
|
+
// peerConnection.localDescription.sdp regardless of any
|
|
415
|
+
// JS-visible event filter). Pairs with the renderer-side
|
|
416
|
+
// candidate filter in rtc_peer_connection.cc::
|
|
417
|
+
// DidGenerateICECandidate that catches anything the policy
|
|
418
|
+
// misses on the trickle-ICE event path. Detection vectors
|
|
419
|
+
// covered: browserleaks.com/webrtc, ipleak.net,
|
|
420
|
+
// FingerprintJS WebRTC panel.
|
|
421
|
+
const effectiveWebrtcPolicy = webrtcIpHandlingPolicy
|
|
422
|
+
?? (useNativeStealth ? 'disable_non_proxied_udp' : undefined);
|
|
391
423
|
browserProcess = await launchChromeBrowserWithNewProfile({
|
|
392
424
|
debuggingPort,
|
|
393
425
|
anonymizedProxyUrl: proxyUrl,
|
|
@@ -405,7 +437,7 @@ class CDPBrowserPlugin extends BrowserPlugin {
|
|
|
405
437
|
keepUserDataDir,
|
|
406
438
|
dnsOverHttpsServer,
|
|
407
439
|
secureDnsMode,
|
|
408
|
-
webrtcIpHandlingPolicy,
|
|
440
|
+
webrtcIpHandlingPolicy: effectiveWebrtcPolicy,
|
|
409
441
|
});
|
|
410
442
|
break;
|
|
411
443
|
}
|
|
@@ -413,108 +445,73 @@ class CDPBrowserPlugin extends BrowserPlugin {
|
|
|
413
445
|
throw new Error(`No browser found with this type: ${browserType}!`);
|
|
414
446
|
}
|
|
415
447
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
resolve(browserProcess);
|
|
421
|
-
})
|
|
422
|
-
.catch((err) => {
|
|
423
|
-
log.error(`Failed to connect to browser on port: ${debuggingPort}`, err);
|
|
424
|
-
reject(new Error(`Failed to connect to browser on port: ${debuggingPort}`));
|
|
425
|
-
});
|
|
426
|
-
});
|
|
448
|
+
// Readiness is now driven off the browser's stderr (the "DevTools
|
|
449
|
+
// listening on ws://…" line), awaited by _connectViaWsEndpoint. No TCP
|
|
450
|
+
// port poll here - just hand back the live process.
|
|
451
|
+
return browserProcess;
|
|
427
452
|
}
|
|
428
453
|
/**
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
* @
|
|
454
|
+
* Open the raw CDP connection. The newPage guard is part of the public
|
|
455
|
+
* contract: pages are created through CDPBrowserController, never here.
|
|
456
|
+
* @protected for test seams.
|
|
432
457
|
*/
|
|
433
|
-
async
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
socket.on('connect', () => {
|
|
439
|
-
socket.end();
|
|
440
|
-
resolve(true);
|
|
441
|
-
}).on('error', (_err) => {
|
|
442
|
-
if (Date.now() - startTime > timeout) {
|
|
443
|
-
reject(new Error(`Timeout waiting for port ${port}`));
|
|
444
|
-
}
|
|
445
|
-
else {
|
|
446
|
-
setTimeout(tryConnect, retryDelay); // Retry after a delay
|
|
447
|
-
}
|
|
448
|
-
});
|
|
449
|
-
}());
|
|
450
|
-
});
|
|
458
|
+
async _openCdpConnection(options) {
|
|
459
|
+
const cdp = await CDP(options);
|
|
460
|
+
// @ts-expect-error newPage is not part of the CDP client type
|
|
461
|
+
cdp.newPage = () => { throw new Error("Use BrowserController class for creating new page!"); };
|
|
462
|
+
return cdp;
|
|
451
463
|
}
|
|
452
464
|
/**
|
|
453
|
-
*
|
|
465
|
+
* Readiness + connect, Puppeteer-style. Await the browser-level CDP
|
|
466
|
+
* WebSocket endpoint the launcher parsed from Chromium's stderr, then open
|
|
467
|
+
* the CDP connection straight to that ws URL as the target. The endpoint
|
|
468
|
+
* promise already rejects fast (with the stderr tail) if the process dies,
|
|
469
|
+
* so a genuinely dead binary fails immediately instead of spinning a retry
|
|
470
|
+
* loop; a slow cold start still wins as long as it comes up within
|
|
471
|
+
* CDP_READINESS_TIMEOUT_MS. The overall timeout only guards a wedged process
|
|
472
|
+
* that never prints the line and never exits.
|
|
454
473
|
*
|
|
455
474
|
* @private
|
|
456
475
|
*/
|
|
457
|
-
async
|
|
458
|
-
let
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
476
|
+
async _connectViaWsEndpoint(browserProcess, debuggingPort) {
|
|
477
|
+
let timeoutHandle;
|
|
478
|
+
const timeout = new Promise((_, reject) => {
|
|
479
|
+
timeoutHandle = setTimeout(() => reject(new Error(`Timed out after ${CDP_READINESS_TIMEOUT_MS}ms waiting for the CDP WebSocket endpoint on port ${debuggingPort}`)), CDP_READINESS_TIMEOUT_MS);
|
|
480
|
+
});
|
|
481
|
+
try {
|
|
482
|
+
// Race the timeout against the WHOLE readiness+connect sequence, not
|
|
483
|
+
// just the ws-endpoint wait: chrome-remote-interface's protocol fetch
|
|
484
|
+
// and ws handshake have no built-in timeout, so a browser that prints
|
|
485
|
+
// the DevTools banner and then stalls the handshake (wedged renderer /
|
|
486
|
+
// resource exhaustion under high concurrency) would otherwise hang
|
|
487
|
+
// _launch forever and leak the port + Chromium process.
|
|
488
|
+
return await Promise.race([
|
|
489
|
+
(async () => {
|
|
490
|
+
const wsUrl = await browserProcess.wsEndpoint();
|
|
491
|
+
log.debug(`Browser CDP endpoint ready on port ${debuggingPort}`, { wsUrl });
|
|
492
|
+
// `local: true` makes chrome-remote-interface use its bundled
|
|
493
|
+
// protocol instead of fetching it over the DevTools HTTP
|
|
494
|
+
// endpoint. Without it the connect opens a second TCP socket
|
|
495
|
+
// (an HTTP /json/protocol GET kept alive) alongside the ws -
|
|
496
|
+
// an /json round-trip ADR 0003 set out to remove, and a
|
|
497
|
+
// second live socket on the single-socket transport. The
|
|
498
|
+
// reconnect path (browser-controller) already connects this way.
|
|
499
|
+
return this._openCdpConnection({ target: wsUrl, local: true });
|
|
500
|
+
})(),
|
|
501
|
+
timeout,
|
|
502
|
+
]);
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
if (timeoutHandle)
|
|
506
|
+
clearTimeout(timeoutHandle);
|
|
487
507
|
}
|
|
488
|
-
// Always throw the wrapped error with our standard message
|
|
489
|
-
throw new Error(`Failed to connect to browser after ${maxRetries} attempts.`);
|
|
490
508
|
}
|
|
491
509
|
}
|
|
492
|
-
Object.defineProperty(CDPBrowserPlugin, "
|
|
493
|
-
enumerable: true,
|
|
494
|
-
configurable: true,
|
|
495
|
-
writable: true,
|
|
496
|
-
value: {
|
|
497
|
-
min: 9200,
|
|
498
|
-
max: 9600, // Give a range of 400 ports instead of 200
|
|
499
|
-
}
|
|
500
|
-
});
|
|
501
|
-
Object.defineProperty(CDPBrowserPlugin, "usedPorts", {
|
|
502
|
-
enumerable: true,
|
|
503
|
-
configurable: true,
|
|
504
|
-
writable: true,
|
|
505
|
-
value: new Set()
|
|
506
|
-
});
|
|
507
|
-
Object.defineProperty(CDPBrowserPlugin, "lastAssignedPort", {
|
|
508
|
-
enumerable: true,
|
|
509
|
-
configurable: true,
|
|
510
|
-
writable: true,
|
|
511
|
-
value: CDPBrowserPlugin.PORT_RANGE.min - 1
|
|
512
|
-
});
|
|
513
|
-
Object.defineProperty(CDPBrowserPlugin, "portLock", {
|
|
510
|
+
Object.defineProperty(CDPBrowserPlugin, "sharedPortAllocator", {
|
|
514
511
|
enumerable: true,
|
|
515
512
|
configurable: true,
|
|
516
513
|
writable: true,
|
|
517
|
-
value:
|
|
514
|
+
value: new PortAllocator({ min: 9200, max: 9600 })
|
|
518
515
|
});
|
|
519
516
|
export default CDPBrowserPlugin;
|
|
520
517
|
//# sourceMappingURL=browser-plugin.js.map
|