arn-browser 0.1.6 → 0.1.8
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/bin/cli.js +43 -0
- package/bin/install.js +420 -0
- package/package.json +8 -3
- package/src/index.d.ts +16 -6
- package/src/index.js +7 -4
- package/src/utility/{multilogin_token_manager.js → mlx_token.js} +32 -43
- package/src/utility/{launchBrowser.d.ts → playwright/pwLaunch.d.ts} +15 -7
- package/src/utility/{launchBrowser.js → playwright/pwLaunch.js} +61 -30
- package/src/{all_routes/routeWithSuperagent.d.ts → utility/playwright/routes/pwRoute.d.ts} +4 -4
- package/src/{all_routes/routeWithSuperagent.js → utility/playwright/routes/pwRoute.js} +2 -2
- package/src/utility/proxy-utility/proxy-chain.js +4 -3
- package/src/utility/proxy-utility/proxy-helper.js +1 -1
- package/src/utility/puppeteer/ppLaunch.d.ts +199 -0
- package/src/utility/puppeteer/ppLaunch.js +723 -0
- package/src/utility/puppeteer/routes/ppRoute.d.ts +64 -0
- package/src/utility/puppeteer/routes/ppRoute.js +326 -0
- /package/src/{human-cursor → utility/playwright/human-cursor}/HumanCursor.js +0 -0
- /package/src/{human-cursor → utility/playwright/human-cursor}/bezier.js +0 -0
- /package/src/{human-cursor → utility/playwright/human-cursor}/index.d.ts +0 -0
- /package/src/{human-cursor → utility/playwright/human-cursor}/index.js +0 -0
- /package/src/{human-cursor → utility/playwright/human-cursor}/randomizer.js +0 -0
- /package/src/{human-cursor → utility/playwright/human-cursor}/tweening.js +0 -0
- /package/src/utility/{playwright-helper.d.ts → playwright/playwright-helper.d.ts} +0 -0
- /package/src/utility/{playwright-helper.js → playwright/playwright-helper.js} +0 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file launchPuppeteer.js
|
|
3
|
+
* @description Puppeteer CDP-only browser launcher.
|
|
4
|
+
* Supports: Chrome, Brave, and Multilogin (via CDP connect).
|
|
5
|
+
* Handles: Persistent profiles (with prefixes), temp profiles, and cleanup.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import crypto from "node:crypto";
|
|
12
|
+
import { spawn } from "child_process";
|
|
13
|
+
import puppeteer from "puppeteer-core";
|
|
14
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
15
|
+
import { FingerprintInjector } from "fingerprint-injector";
|
|
16
|
+
import { FingerprintGenerator } from "fingerprint-generator";
|
|
17
|
+
|
|
18
|
+
// Internal Utilities
|
|
19
|
+
import { getMultiloginToken } from "../mlx_token.js";
|
|
20
|
+
import { deleteDirectoryWithRetries } from "../deleteDirectory.js";
|
|
21
|
+
|
|
22
|
+
// ==========================================================================
|
|
23
|
+
// 1. CONFIGURATION & CONSTANTS
|
|
24
|
+
// ==========================================================================
|
|
25
|
+
|
|
26
|
+
const MULTILOGIN_LAUNCHER_URL = "https://launcher.mlx.yt:45001";
|
|
27
|
+
const MULTILOGIN_FOLDER_ID = "bad9e7e1-cfab-4c8d-bd19-91aa82929711";
|
|
28
|
+
|
|
29
|
+
const ARN_BROWSER_DIR = path.join(os.homedir(), ".arn-browser");
|
|
30
|
+
const BASE_PROFILE_DIR = path.join(ARN_BROWSER_DIR, "profiles", "pp");
|
|
31
|
+
const PERSISTENT_DIR = path.join(BASE_PROFILE_DIR, "persistent");
|
|
32
|
+
const TEMP_DIR = path.join(BASE_PROFILE_DIR, "temp");
|
|
33
|
+
|
|
34
|
+
if (!fs.existsSync(PERSISTENT_DIR)) fs.mkdirSync(PERSISTENT_DIR, { recursive: true });
|
|
35
|
+
if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
36
|
+
|
|
37
|
+
// Module-level log flags (set per-launch via launchPuppeteer options)
|
|
38
|
+
let _launchLogs = false;
|
|
39
|
+
let _cleanupLogs = false;
|
|
40
|
+
|
|
41
|
+
// Detect OS for fingerprint generation
|
|
42
|
+
const detectedOs = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Helper to generate consistent fingerprint options for Puppeteer.
|
|
46
|
+
* @param {object|null} screenOptions - Optional screen constraints { minWidth, maxWidth, minHeight, maxHeight }
|
|
47
|
+
*/
|
|
48
|
+
function getFingerprintConfig(screenOptions = null) {
|
|
49
|
+
const config = {
|
|
50
|
+
devices: ["desktop"],
|
|
51
|
+
operatingSystems: [detectedOs],
|
|
52
|
+
locales: ["en-US"],
|
|
53
|
+
browsers: [{ name: "chrome", minVersion: 141 }],
|
|
54
|
+
screen: {},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (screenOptions && typeof screenOptions === 'object') {
|
|
58
|
+
if (screenOptions.minWidth) config.screen.minWidth = screenOptions.minWidth;
|
|
59
|
+
if (screenOptions.maxWidth) config.screen.maxWidth = screenOptions.maxWidth;
|
|
60
|
+
if (screenOptions.minHeight) config.screen.minHeight = screenOptions.minHeight;
|
|
61
|
+
if (screenOptions.maxHeight) config.screen.maxHeight = screenOptions.maxHeight;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return config;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ==========================================================================
|
|
68
|
+
// 2. HELPER FUNCTIONS
|
|
69
|
+
// ==========================================================================
|
|
70
|
+
|
|
71
|
+
const PROFILE_META_FILE = "_profile_meta.json";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolves the persistent path with a browser-specific prefix.
|
|
75
|
+
*/
|
|
76
|
+
function resolveProfilePath(nameOrPath, browserName) {
|
|
77
|
+
if (!nameOrPath) return null;
|
|
78
|
+
|
|
79
|
+
if (path.isAbsolute(nameOrPath)) return nameOrPath;
|
|
80
|
+
|
|
81
|
+
let prefix = browserName.toLowerCase();
|
|
82
|
+
if (prefix.includes("brave")) prefix = "brave";
|
|
83
|
+
else if (prefix.includes("chrome") || prefix.includes("chromium")) prefix = "chrome";
|
|
84
|
+
|
|
85
|
+
const folderName = `${prefix}_${nameOrPath}`;
|
|
86
|
+
return path.join(PERSISTENT_DIR, folderName);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Locates binaries for Chrome and Brave from the installed browsers.
|
|
91
|
+
* Uses ~/.arn-browser/browsers/{browser}/{executable}
|
|
92
|
+
*/
|
|
93
|
+
function getBinaryPath(browserName) {
|
|
94
|
+
const isWindows = process.platform === "win32";
|
|
95
|
+
const browsersDir = path.join(ARN_BROWSER_DIR, "browsers");
|
|
96
|
+
|
|
97
|
+
const binaryMap = {
|
|
98
|
+
brave: isWindows
|
|
99
|
+
? path.join(browsersDir, "brave", "brave.exe")
|
|
100
|
+
: path.join(browsersDir, "brave", "brave"),
|
|
101
|
+
chrome: isWindows
|
|
102
|
+
? path.join(browsersDir, "chromium", "chrome.exe")
|
|
103
|
+
: path.join(browsersDir, "chromium", "chrome"),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const binaryPath = binaryMap[browserName];
|
|
107
|
+
if (!binaryPath) {
|
|
108
|
+
throw new Error(`░░░░░ [LaunchPuppeteer] Unknown browser: ${browserName}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(binaryPath)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`░░░░░ [LaunchPuppeteer] ${browserName} binary not found at: ${binaryPath}\n` +
|
|
114
|
+
` Run "node bin/cli.js install" to install browsers.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return binaryPath;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Writes or updates the profile metadata file.
|
|
123
|
+
*/
|
|
124
|
+
function writeProfileMeta(dirPath, type, cleanupMinutes) {
|
|
125
|
+
try {
|
|
126
|
+
const metaPath = path.join(dirPath, PROFILE_META_FILE);
|
|
127
|
+
const now = new Date().toISOString();
|
|
128
|
+
|
|
129
|
+
if (fs.existsSync(metaPath)) {
|
|
130
|
+
const existing = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
131
|
+
existing.last_launched_at = now;
|
|
132
|
+
fs.writeFileSync(metaPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
133
|
+
} else {
|
|
134
|
+
const meta = {
|
|
135
|
+
type,
|
|
136
|
+
created_at: now,
|
|
137
|
+
last_launched_at: now,
|
|
138
|
+
cleanup_minutes: cleanupMinutes,
|
|
139
|
+
};
|
|
140
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
// Non-critical
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Scans profile directories and deletes expired profiles.
|
|
149
|
+
*/
|
|
150
|
+
function cleanUpProfiles(skipPath) {
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
|
|
153
|
+
function scanDir(dir) {
|
|
154
|
+
if (!fs.existsSync(dir)) return;
|
|
155
|
+
try {
|
|
156
|
+
const folders = fs.readdirSync(dir);
|
|
157
|
+
for (const folder of folders) {
|
|
158
|
+
const folderPath = path.join(dir, folder);
|
|
159
|
+
try {
|
|
160
|
+
const stats = fs.statSync(folderPath);
|
|
161
|
+
if (!stats.isDirectory()) continue;
|
|
162
|
+
|
|
163
|
+
if (skipPath && path.resolve(folderPath) === path.resolve(skipPath)) continue;
|
|
164
|
+
|
|
165
|
+
const metaPath = path.join(folderPath, PROFILE_META_FILE);
|
|
166
|
+
if (!fs.existsSync(metaPath)) {
|
|
167
|
+
if (dir === TEMP_DIR && now - stats.mtimeMs > 15 * 60 * 1000) {
|
|
168
|
+
if (_cleanupLogs) console.log(`░░░░░ [Cleanup] Deleting legacy temp profile: ${folder}`);
|
|
169
|
+
deleteDirectoryWithRetries(folderPath);
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
175
|
+
const cleanupMs = meta.cleanup_minutes;
|
|
176
|
+
|
|
177
|
+
if (!cleanupMs || cleanupMs <= 0) continue;
|
|
178
|
+
|
|
179
|
+
const lastLaunch = new Date(meta.last_launched_at).getTime();
|
|
180
|
+
if (now - lastLaunch > cleanupMs * 60 * 1000) {
|
|
181
|
+
if (_cleanupLogs) console.log(`░░░░░ [Cleanup] Deleting expired ${meta.type} profile: ${folder} (last launched ${Math.round((now - lastLaunch) / 60000)} mins ago)`);
|
|
182
|
+
deleteDirectoryWithRetries(folderPath);
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
// Ignore per-folder errors
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error("Cleanup scan error:", err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (_cleanupLogs) console.log("░░░░░ [Cleanup] Scanning for expired profiles...");
|
|
194
|
+
scanDir(PERSISTENT_DIR);
|
|
195
|
+
scanDir(TEMP_DIR);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Formats proxy into standard object.
|
|
200
|
+
*/
|
|
201
|
+
function formatProxy(proxy) {
|
|
202
|
+
if (!proxy) return undefined;
|
|
203
|
+
if (typeof proxy === "string") return { server: proxy };
|
|
204
|
+
|
|
205
|
+
const p = { server: `${proxy.type || "http"}://${proxy.host}:${proxy.port}` };
|
|
206
|
+
if (proxy.user) {
|
|
207
|
+
p.username = proxy.user;
|
|
208
|
+
p.password = proxy.pass;
|
|
209
|
+
}
|
|
210
|
+
return p;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ==========================================================================
|
|
214
|
+
// 3. MAIN LAUNCHER ENTRY POINT
|
|
215
|
+
// ==========================================================================
|
|
216
|
+
|
|
217
|
+
export async function ppLaunch({
|
|
218
|
+
// Browser selection
|
|
219
|
+
which_browser = "chrome",
|
|
220
|
+
|
|
221
|
+
// Path & Storage
|
|
222
|
+
profile_path = null,
|
|
223
|
+
cleanupMinutes = 0,
|
|
224
|
+
|
|
225
|
+
// Network
|
|
226
|
+
proxy = null,
|
|
227
|
+
|
|
228
|
+
// Extra browser CLI args
|
|
229
|
+
extraArgs = [],
|
|
230
|
+
|
|
231
|
+
// Spoof Fingerprint
|
|
232
|
+
// - false: No spoofing
|
|
233
|
+
// - true: Spoof with default screen
|
|
234
|
+
// - { minWidth?, maxWidth?, minHeight?, maxHeight? }: Spoof with screen constraints
|
|
235
|
+
spoof_fingerprint = false,
|
|
236
|
+
|
|
237
|
+
// Multilogin
|
|
238
|
+
multilogin_options = {},
|
|
239
|
+
|
|
240
|
+
// Logging
|
|
241
|
+
launch_logs = false,
|
|
242
|
+
cleanup_logs = false,
|
|
243
|
+
} = {}) {
|
|
244
|
+
try {
|
|
245
|
+
// Set module-level log flags
|
|
246
|
+
_launchLogs = launch_logs;
|
|
247
|
+
_cleanupLogs = cleanup_logs;
|
|
248
|
+
|
|
249
|
+
// Resolve path using the browser type to ensure prefixes
|
|
250
|
+
const fullPath = resolveProfilePath(profile_path, which_browser);
|
|
251
|
+
|
|
252
|
+
// Calculate effective cleanup minutes
|
|
253
|
+
const isPersistent = !!profile_path;
|
|
254
|
+
const effectiveCleanupMinutes = cleanupMinutes > 0
|
|
255
|
+
? cleanupMinutes
|
|
256
|
+
: (isPersistent ? null : 15);
|
|
257
|
+
|
|
258
|
+
// Run cleanup scan
|
|
259
|
+
cleanUpProfiles(fullPath);
|
|
260
|
+
|
|
261
|
+
let result;
|
|
262
|
+
|
|
263
|
+
switch (which_browser) {
|
|
264
|
+
case "chrome":
|
|
265
|
+
case "chromium":
|
|
266
|
+
result = await chromeLauncher({
|
|
267
|
+
profilePath: fullPath,
|
|
268
|
+
proxy,
|
|
269
|
+
extraArgs,
|
|
270
|
+
spoof_fingerprint,
|
|
271
|
+
cleanupMinutes: effectiveCleanupMinutes,
|
|
272
|
+
});
|
|
273
|
+
break;
|
|
274
|
+
case "brave":
|
|
275
|
+
result = await braveLauncher({
|
|
276
|
+
profilePath: fullPath,
|
|
277
|
+
proxy,
|
|
278
|
+
extraArgs,
|
|
279
|
+
spoof_fingerprint,
|
|
280
|
+
cleanupMinutes: effectiveCleanupMinutes,
|
|
281
|
+
});
|
|
282
|
+
break;
|
|
283
|
+
case "multilogin":
|
|
284
|
+
result = await multiloginLauncher({
|
|
285
|
+
proxy,
|
|
286
|
+
multilogin_options,
|
|
287
|
+
});
|
|
288
|
+
break;
|
|
289
|
+
default:
|
|
290
|
+
throw new Error(`[LaunchPuppeteer] Unknown browser type: ${which_browser}. Supported: chrome, brave, multilogin`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return result;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error("░░░░░ [LaunchPuppeteer] Critical Error:", error.message || error);
|
|
296
|
+
return { browser: null, context: null, page: null, isBrowserRunning: () => false, closeBrowser: async () => false, launchError: error };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ==========================================================================
|
|
301
|
+
// 4. ENGINE: CHROME (CDP)
|
|
302
|
+
// ==========================================================================
|
|
303
|
+
|
|
304
|
+
async function chromeLauncher({ profilePath, proxy, extraArgs, spoof_fingerprint, cleanupMinutes }) {
|
|
305
|
+
const isPersistent = !!profilePath;
|
|
306
|
+
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
307
|
+
|
|
308
|
+
fs.mkdirSync(activePath, { recursive: true });
|
|
309
|
+
writeProfileMeta(activePath, isPersistent ? "persistent" : "temp", cleanupMinutes);
|
|
310
|
+
if (_launchLogs) console.log(`░░░░░ Starting Chrome [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
|
|
311
|
+
|
|
312
|
+
const binaryPath = getBinaryPath("chrome");
|
|
313
|
+
return await spawnAndConnect({
|
|
314
|
+
binaryPath,
|
|
315
|
+
profilePath: activePath,
|
|
316
|
+
isPersistent,
|
|
317
|
+
proxy,
|
|
318
|
+
extraArgs,
|
|
319
|
+
spoof_fingerprint,
|
|
320
|
+
browserLabel: "Chrome",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ==========================================================================
|
|
325
|
+
// 5. ENGINE: BRAVE (CDP)
|
|
326
|
+
// ==========================================================================
|
|
327
|
+
|
|
328
|
+
async function braveLauncher({ profilePath, proxy, extraArgs, spoof_fingerprint, cleanupMinutes }) {
|
|
329
|
+
const isPersistent = !!profilePath;
|
|
330
|
+
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
331
|
+
|
|
332
|
+
fs.mkdirSync(activePath, { recursive: true });
|
|
333
|
+
writeProfileMeta(activePath, isPersistent ? "persistent" : "temp", cleanupMinutes);
|
|
334
|
+
if (_launchLogs) console.log(`░░░░░ Starting Brave [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
|
|
335
|
+
|
|
336
|
+
// ======================================================
|
|
337
|
+
// Disable Brave Sidebar via Preferences
|
|
338
|
+
// ======================================================
|
|
339
|
+
const prefsFilePath = path.join(activePath, "Default", "Preferences");
|
|
340
|
+
const prefsDir = path.dirname(prefsFilePath);
|
|
341
|
+
if (!fs.existsSync(prefsDir)) fs.mkdirSync(prefsDir, { recursive: true });
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
let prefs = {};
|
|
345
|
+
if (fs.existsSync(prefsFilePath)) {
|
|
346
|
+
prefs = JSON.parse(fs.readFileSync(prefsFilePath, "utf-8"));
|
|
347
|
+
}
|
|
348
|
+
if (!prefs.brave) prefs.brave = {};
|
|
349
|
+
if (!prefs.brave.sidebar) prefs.brave.sidebar = {};
|
|
350
|
+
prefs.brave.sidebar.sidebar_show_option = 3;
|
|
351
|
+
fs.writeFileSync(prefsFilePath, JSON.stringify(prefs, null, 2), "utf-8");
|
|
352
|
+
} catch (e) {
|
|
353
|
+
console.warn("░░░░░ Could not modify Brave preferences:", e.message);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const binaryPath = getBinaryPath("brave");
|
|
357
|
+
const braveArgs = [
|
|
358
|
+
"--disable-features=Translate,BraveRewards,BraveWallet,BraveNews,Sidebar,SidePanel",
|
|
359
|
+
"--homepage=about:blank",
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
return await spawnAndConnect({
|
|
363
|
+
binaryPath,
|
|
364
|
+
profilePath: activePath,
|
|
365
|
+
isPersistent,
|
|
366
|
+
proxy,
|
|
367
|
+
extraArgs: [...braveArgs, ...extraArgs],
|
|
368
|
+
spoof_fingerprint,
|
|
369
|
+
browserLabel: "Brave",
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ==========================================================================
|
|
374
|
+
// 6. CDP SPAWN & CONNECT (Shared between Chrome & Brave)
|
|
375
|
+
// ==========================================================================
|
|
376
|
+
|
|
377
|
+
async function spawnAndConnect({ binaryPath, profilePath, isPersistent, proxy, extraArgs = [], spoof_fingerprint = false, browserLabel = "Browser" }) {
|
|
378
|
+
let browser;
|
|
379
|
+
let closing = false;
|
|
380
|
+
let signalHandler;
|
|
381
|
+
|
|
382
|
+
// Random 5-digit port (10000–65535)
|
|
383
|
+
const debugPort = Math.floor(Math.random() * 55535) + 10000;
|
|
384
|
+
|
|
385
|
+
const proxyObj = formatProxy(proxy);
|
|
386
|
+
|
|
387
|
+
const args = [
|
|
388
|
+
`--remote-debugging-port=${debugPort}`,
|
|
389
|
+
`--user-data-dir=${profilePath}`,
|
|
390
|
+
"--no-first-run",
|
|
391
|
+
"--no-default-browser-check",
|
|
392
|
+
"--start-maximized",
|
|
393
|
+
|
|
394
|
+
// --- Stealth & Anti-Detection ---
|
|
395
|
+
"--test-type",
|
|
396
|
+
"--disable-blink-features=AutomationControlled",
|
|
397
|
+
|
|
398
|
+
// --- PREVENT EXTRA TABS ---
|
|
399
|
+
"--disable-restore-session-state",
|
|
400
|
+
|
|
401
|
+
// --- Error Suppression ---
|
|
402
|
+
"--disable-session-crashed-bubble",
|
|
403
|
+
"--hide-crash-restore-bubble",
|
|
404
|
+
|
|
405
|
+
// --- Silence & Networking ---
|
|
406
|
+
"--disable-background-networking",
|
|
407
|
+
"--disable-background-timer-throttling",
|
|
408
|
+
"--disable-breakpad",
|
|
409
|
+
"--disable-crash-reporter",
|
|
410
|
+
"--disable-component-update",
|
|
411
|
+
"--disable-sync",
|
|
412
|
+
"--disable-domain-reliability",
|
|
413
|
+
"--disable-client-side-phishing-detection",
|
|
414
|
+
"--disable-infobars",
|
|
415
|
+
|
|
416
|
+
...extraArgs,
|
|
417
|
+
];
|
|
418
|
+
|
|
419
|
+
if (proxyObj) {
|
|
420
|
+
args.push(`--proxy-server=${proxyObj.server}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Spawn browser
|
|
424
|
+
const chrome = spawn(binaryPath, args, {
|
|
425
|
+
stdio: "ignore",
|
|
426
|
+
detached: true,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Unref so Node.js can exit independently
|
|
430
|
+
chrome.unref();
|
|
431
|
+
|
|
432
|
+
// Cleanup function
|
|
433
|
+
const cleanup = async () => {
|
|
434
|
+
if (closing) return false;
|
|
435
|
+
closing = true;
|
|
436
|
+
|
|
437
|
+
if (signalHandler) {
|
|
438
|
+
process.off("SIGINT", signalHandler);
|
|
439
|
+
process.off("SIGTERM", signalHandler);
|
|
440
|
+
process.off("SIGHUP", signalHandler);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
if (browser?.connected) await browser.close();
|
|
445
|
+
} catch { }
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
if (chrome.pid) process.kill(-chrome.pid, "SIGTERM");
|
|
449
|
+
} catch { }
|
|
450
|
+
|
|
451
|
+
// Delete temp profile
|
|
452
|
+
if (!isPersistent && profilePath) {
|
|
453
|
+
try {
|
|
454
|
+
if (_launchLogs) console.log(`░░░░░ Deleting temp profile: ${profilePath}`);
|
|
455
|
+
await deleteDirectoryWithRetries(profilePath);
|
|
456
|
+
} catch { }
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return true;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// Register signal handlers
|
|
463
|
+
signalHandler = async () => {
|
|
464
|
+
await cleanup();
|
|
465
|
+
process.exit(0);
|
|
466
|
+
};
|
|
467
|
+
process.once("SIGINT", signalHandler);
|
|
468
|
+
process.once("SIGTERM", signalHandler);
|
|
469
|
+
process.once("SIGHUP", signalHandler);
|
|
470
|
+
|
|
471
|
+
if (_launchLogs) console.log(`░░░░░ ${browserLabel} started on port ${debugPort}`);
|
|
472
|
+
await sleep(2000);
|
|
473
|
+
|
|
474
|
+
// Connect via CDP
|
|
475
|
+
try {
|
|
476
|
+
browser = await puppeteer.connect({
|
|
477
|
+
browserURL: `http://127.0.0.1:${debugPort}`,
|
|
478
|
+
defaultViewport: null,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
if (_launchLogs) console.log(`░░░░░ Connected to ${browserLabel} via CDP (port ${debugPort})`);
|
|
482
|
+
|
|
483
|
+
// Get page — apply fingerprint injection if spoof_fingerprint is enabled
|
|
484
|
+
const pages = await browser.pages();
|
|
485
|
+
const page = pages[0] ?? (await browser.newPage());
|
|
486
|
+
|
|
487
|
+
// Fingerprint injection logic:
|
|
488
|
+
// 1. If persistent profile has a saved fingerprint.json → ALWAYS use it (even if spoof_fingerprint is false)
|
|
489
|
+
// 2. If spoof_fingerprint is truthy → generate new fingerprint (save it for persistent profiles)
|
|
490
|
+
// 3. Otherwise → no fingerprint injection
|
|
491
|
+
const fingerprintFilePath = path.join(profilePath, "fingerprint.json");
|
|
492
|
+
const hasSavedFingerprint = isPersistent && fs.existsSync(fingerprintFilePath);
|
|
493
|
+
|
|
494
|
+
if (hasSavedFingerprint || spoof_fingerprint) {
|
|
495
|
+
let fingerprintData;
|
|
496
|
+
|
|
497
|
+
if (hasSavedFingerprint) {
|
|
498
|
+
// Persistent: reuse saved fingerprint (regardless of spoof_fingerprint setting)
|
|
499
|
+
fingerprintData = JSON.parse(fs.readFileSync(fingerprintFilePath, "utf-8"));
|
|
500
|
+
if (_launchLogs) console.log(`░░░░░ Loaded saved fingerprint for ${browserLabel}`);
|
|
501
|
+
} else {
|
|
502
|
+
// Generate new fingerprint
|
|
503
|
+
const screenOptions = (typeof spoof_fingerprint === 'object') ? spoof_fingerprint : null;
|
|
504
|
+
const fpConfig = getFingerprintConfig(screenOptions);
|
|
505
|
+
fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
|
|
506
|
+
|
|
507
|
+
// Save for persistent profiles
|
|
508
|
+
if (isPersistent) {
|
|
509
|
+
fs.writeFileSync(fingerprintFilePath, JSON.stringify(fingerprintData, null, 2), "utf-8");
|
|
510
|
+
if (_launchLogs) console.log(`░░░░░ Generated and saved new fingerprint for ${browserLabel}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Inject onto existing page
|
|
515
|
+
const injector = new FingerprintInjector();
|
|
516
|
+
await injector.attachFingerprintToPuppeteer(page, fingerprintData);
|
|
517
|
+
|
|
518
|
+
if (_launchLogs) console.log(`░░░░░ Fingerprint injected into ${browserLabel} page`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
browser,
|
|
523
|
+
context: browser,
|
|
524
|
+
page,
|
|
525
|
+
isBrowserRunning: () => !!browser?.connected,
|
|
526
|
+
closeBrowser: cleanup,
|
|
527
|
+
launchError: null,
|
|
528
|
+
};
|
|
529
|
+
} catch (error) {
|
|
530
|
+
console.error(`░░░░░ Failed to connect to ${browserLabel} on port ${debugPort}:`, error.message);
|
|
531
|
+
await cleanup();
|
|
532
|
+
return {
|
|
533
|
+
browser: null,
|
|
534
|
+
context: null,
|
|
535
|
+
page: null,
|
|
536
|
+
isBrowserRunning: () => false,
|
|
537
|
+
closeBrowser: async () => false,
|
|
538
|
+
launchError: error,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ==========================================================================
|
|
544
|
+
// 7. ENGINE: MULTILOGIN (CDP via Puppeteer)
|
|
545
|
+
// ==========================================================================
|
|
546
|
+
|
|
547
|
+
async function multiloginLauncher({ proxy, multilogin_options = {} }) {
|
|
548
|
+
const {
|
|
549
|
+
profileId = null,
|
|
550
|
+
os_type = (process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux"),
|
|
551
|
+
canvas_noise = true,
|
|
552
|
+
media_masking = true,
|
|
553
|
+
audio_masking = true,
|
|
554
|
+
} = multilogin_options;
|
|
555
|
+
|
|
556
|
+
if (profileId) {
|
|
557
|
+
return await launchExistingMultiloginProfile(profileId);
|
|
558
|
+
} else {
|
|
559
|
+
return await launchQuickMultiloginProfile({
|
|
560
|
+
os_type,
|
|
561
|
+
proxy,
|
|
562
|
+
canvas_noise,
|
|
563
|
+
media_masking,
|
|
564
|
+
audio_masking,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function launchExistingMultiloginProfile(profileId) {
|
|
570
|
+
const startUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v2/profile/f/${MULTILOGIN_FOLDER_ID}/p/${profileId}/start?automation_type=playwright&headless_mode=false`;
|
|
571
|
+
let browser;
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const token = await getMultiloginToken();
|
|
575
|
+
let response = await fetch(startUrl, {
|
|
576
|
+
method: "GET",
|
|
577
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json", "X-Strict-Mode": "true" },
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
if (response.status === 400) {
|
|
581
|
+
const errorJson = await response.json().catch(() => null);
|
|
582
|
+
if (errorJson?.status?.error_code === "PROFILE_ALREADY_RUNNING") {
|
|
583
|
+
if (_launchLogs) console.log(`░░░░░ Profile ${profileId} is running. Restarting...`);
|
|
584
|
+
await stopMultiloginProfile(profileId);
|
|
585
|
+
await sleep(5000);
|
|
586
|
+
response = await fetch(startUrl, {
|
|
587
|
+
method: "GET",
|
|
588
|
+
headers: {
|
|
589
|
+
Authorization: `Bearer ${await getMultiloginToken()}`,
|
|
590
|
+
Accept: "application/json",
|
|
591
|
+
"X-Strict-Mode": "true",
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
} else {
|
|
595
|
+
throw new Error(`Multilogin 400 Error: ${JSON.stringify(errorJson)}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (!response.ok) throw new Error(`Failed to start profile: ${await response.text()}`);
|
|
600
|
+
|
|
601
|
+
const data = await response.json();
|
|
602
|
+
const port = data.data.port;
|
|
603
|
+
if (_launchLogs) console.log(`░░░░░ Multilogin Profile ${profileId} started on port ${port}`);
|
|
604
|
+
|
|
605
|
+
browser = await puppeteer.connect({
|
|
606
|
+
browserURL: `http://127.0.0.1:${port}`,
|
|
607
|
+
defaultViewport: null,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const pages = await browser.pages();
|
|
611
|
+
const page = pages[0] ?? (await browser.newPage());
|
|
612
|
+
|
|
613
|
+
const closeBrowser = async () => {
|
|
614
|
+
try {
|
|
615
|
+
if (browser?.connected) await browser.close();
|
|
616
|
+
} catch { }
|
|
617
|
+
return await stopMultiloginProfile(profileId);
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
return { browser, context: browser, page, isBrowserRunning: () => !!browser?.connected, closeBrowser, launchError: null };
|
|
621
|
+
} catch (error) {
|
|
622
|
+
console.error("Multilogin Launch Error:", error.message);
|
|
623
|
+
try { await stopMultiloginProfile(profileId); } catch { }
|
|
624
|
+
return { browser: null, context: null, page: null, isBrowserRunning: () => false, closeBrowser: async () => false, launchError: error };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking }) {
|
|
629
|
+
const createUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v3/profile/quick`;
|
|
630
|
+
let browser, profileId;
|
|
631
|
+
|
|
632
|
+
const requestBody = {
|
|
633
|
+
browser_type: "mimic",
|
|
634
|
+
os_type,
|
|
635
|
+
automation: "playwright",
|
|
636
|
+
is_headless: false,
|
|
637
|
+
parameters: {
|
|
638
|
+
flags: {
|
|
639
|
+
audio_masking: audio_masking ? "mask" : "natural",
|
|
640
|
+
media_devices_masking: media_masking ? "mask" : "natural",
|
|
641
|
+
screen_masking: "natural",
|
|
642
|
+
canvas_noise: canvas_noise ? "mask" : "natural",
|
|
643
|
+
proxy_masking: proxy ? "custom" : "disabled",
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
quickProfilesCount: 1,
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
if (proxy) {
|
|
650
|
+
const pObj = formatProxy(proxy);
|
|
651
|
+
if (pObj) {
|
|
652
|
+
const u = new URL(pObj.server);
|
|
653
|
+
requestBody.parameters.proxy = {
|
|
654
|
+
type: "http",
|
|
655
|
+
host: u.hostname,
|
|
656
|
+
port: Number(u.port),
|
|
657
|
+
username: pObj.username || "",
|
|
658
|
+
password: pObj.password || "",
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
try {
|
|
664
|
+
const token = await getMultiloginToken();
|
|
665
|
+
const response = await fetch(createUrl, {
|
|
666
|
+
method: "POST",
|
|
667
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
668
|
+
body: JSON.stringify(requestBody),
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
if (!response.ok) throw new Error(await response.text());
|
|
672
|
+
const json = await response.json();
|
|
673
|
+
|
|
674
|
+
profileId = json.data.id;
|
|
675
|
+
const port = json.data.port;
|
|
676
|
+
if (_launchLogs) console.log(`░░░░░ Quick Profile Created: ${profileId} on port ${port}`);
|
|
677
|
+
|
|
678
|
+
browser = await puppeteer.connect({
|
|
679
|
+
browserURL: `http://127.0.0.1:${port}`,
|
|
680
|
+
defaultViewport: null,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const pages = await browser.pages();
|
|
684
|
+
const page = pages[0] ?? (await browser.newPage());
|
|
685
|
+
|
|
686
|
+
const closeBrowser = async () => {
|
|
687
|
+
try {
|
|
688
|
+
if (browser?.connected) await browser.close();
|
|
689
|
+
} catch { }
|
|
690
|
+
return await stopMultiloginProfile(profileId);
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
return { browser, context: browser, page, isBrowserRunning: () => !!browser?.connected, closeBrowser, launchError: null };
|
|
694
|
+
} catch (error) {
|
|
695
|
+
console.error("Quick Profile Error:", error);
|
|
696
|
+
if (profileId) await stopMultiloginProfile(profileId);
|
|
697
|
+
return { browser: null, context: null, page: null, isBrowserRunning: () => false, closeBrowser: async () => false, launchError: error };
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function stopMultiloginProfile(profileId) {
|
|
702
|
+
const stopUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v1/profile/stop/p/${profileId}`;
|
|
703
|
+
try {
|
|
704
|
+
const token = await getMultiloginToken();
|
|
705
|
+
const response = await fetch(stopUrl, {
|
|
706
|
+
method: "GET",
|
|
707
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
if (!response.ok) {
|
|
711
|
+
const txt = await response.text();
|
|
712
|
+
if (response.status === 500 && txt.includes("profile already stopped")) {
|
|
713
|
+
return { error: null };
|
|
714
|
+
}
|
|
715
|
+
throw new Error(txt);
|
|
716
|
+
}
|
|
717
|
+
if (_launchLogs) console.log(`░░░░░ Profile ${profileId} stopped.`);
|
|
718
|
+
return { error: null };
|
|
719
|
+
} catch (err) {
|
|
720
|
+
console.error(`Error stopping ${profileId}:`, err);
|
|
721
|
+
return { error: err };
|
|
722
|
+
}
|
|
723
|
+
}
|