arn-browser 0.0.2 → 0.0.4
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 +3 -2
- package/package.json +28 -48
- package/rowser_automation_env.js +32 -0
- package/src/all_routes/routeWithSuperagent.d.ts +67 -0
- package/src/all_routes/routeWithSuperagent.js +322 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +15 -0
- package/src/others/totp-generator.d.ts +15 -0
- package/src/others/totp-generator.js +86 -0
- package/src/utility/deleteDirectory.js +105 -0
- package/src/utility/launchBrowser.d.ts +221 -0
- package/src/utility/launchBrowser.js +868 -0
- package/src/utility/multilogin_token_manager.js +186 -0
- package/src/utility/playwright-helper.d.ts +61 -0
- package/src/utility/playwright-helper.js +129 -0
- package/src/utility/proxy-utility/custom-proxy.d.ts +93 -0
- package/src/utility/proxy-utility/custom-proxy.js +669 -0
- package/src/utility/proxy-utility/proxy-chain.d.ts +123 -0
- package/src/utility/proxy-utility/proxy-chain.js +337 -0
- package/src/utility/proxy-utility/proxy-helper.d.ts +91 -0
- package/src/utility/proxy-utility/proxy-helper.js +245 -0
- /package/{LICENSE.md → LICENSE} +0 -0
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file launchBrowser.js
|
|
3
|
+
* @description Main entry point for launching various browser engines.
|
|
4
|
+
* Supports: Chromium, Firefox (Playwright), Brave, Camoufox, and Multilogin.
|
|
5
|
+
* Handles: Persistent profiles (with prefixes), temp profiles, and tab 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 { chromium, firefox } from "playwright";
|
|
13
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
14
|
+
|
|
15
|
+
// Fingerprint management
|
|
16
|
+
import { newInjectedContext, FingerprintInjector } from "fingerprint-injector";
|
|
17
|
+
import { FingerprintGenerator } from "fingerprint-generator";
|
|
18
|
+
|
|
19
|
+
// Internal Utilities
|
|
20
|
+
import { getMultiloginToken } from "./multilogin_token_manager.js";
|
|
21
|
+
import { deleteDirectoryWithRetries } from "./deleteDirectory.js";
|
|
22
|
+
|
|
23
|
+
// Camoufox Special
|
|
24
|
+
import { launchOptions } from "camoufox-js";
|
|
25
|
+
|
|
26
|
+
// ==========================================================================
|
|
27
|
+
// 1. CONFIGURATION & CONSTANTS
|
|
28
|
+
// ==========================================================================
|
|
29
|
+
|
|
30
|
+
const osMap = {
|
|
31
|
+
win32: "windows",
|
|
32
|
+
darwin: "macos",
|
|
33
|
+
linux: "linux",
|
|
34
|
+
};
|
|
35
|
+
const detectedOs = osMap[process.platform] || "windows";
|
|
36
|
+
|
|
37
|
+
const MULTILOGIN_LAUNCHER_URL = "https://launcher.mlx.yt:45001";
|
|
38
|
+
const MULTILOGIN_FOLDER_ID = "bad9e7e1-cfab-4c8d-bd19-91aa82929711";
|
|
39
|
+
|
|
40
|
+
const PROJECT_ROOT = process.cwd();
|
|
41
|
+
const BASE_PROFILE_DIR = path.join(PROJECT_ROOT, ".data", "browser_profiles");
|
|
42
|
+
const PERSISTENT_DIR = path.join(BASE_PROFILE_DIR, "persistent");
|
|
43
|
+
const TEMP_DIR = path.join(BASE_PROFILE_DIR, "temp");
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(PERSISTENT_DIR)) fs.mkdirSync(PERSISTENT_DIR, { recursive: true });
|
|
46
|
+
if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
47
|
+
|
|
48
|
+
// ==========================================================================
|
|
49
|
+
// 2. HELPER FUNCTIONS
|
|
50
|
+
// ==========================================================================
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolves the persistent path with a browser-specific prefix.
|
|
54
|
+
*/
|
|
55
|
+
function resolveProfilePath(nameOrPath, browserName) {
|
|
56
|
+
if (!nameOrPath) return null;
|
|
57
|
+
|
|
58
|
+
if (path.isAbsolute(nameOrPath)) return nameOrPath;
|
|
59
|
+
|
|
60
|
+
let prefix = browserName.toLowerCase();
|
|
61
|
+
if (prefix.includes("brave")) prefix = "brave";
|
|
62
|
+
else if (prefix.includes("chrome") || prefix.includes("chromium")) prefix = "chromium";
|
|
63
|
+
else if (prefix.includes("firefox")) prefix = "firefox";
|
|
64
|
+
else if (prefix.includes("camoufox")) prefix = "camoufox";
|
|
65
|
+
|
|
66
|
+
const folderName = `${prefix}_${nameOrPath}`;
|
|
67
|
+
return path.join(PERSISTENT_DIR, folderName);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Locates binaries for manual browsers (Brave).
|
|
72
|
+
*/
|
|
73
|
+
function getBinaryPath(browserName) {
|
|
74
|
+
const isWindows = process.platform === "win32";
|
|
75
|
+
const homeDir = os.homedir();
|
|
76
|
+
|
|
77
|
+
let binaryPath = "";
|
|
78
|
+
|
|
79
|
+
if (browserName === "brave") {
|
|
80
|
+
if (isWindows) {
|
|
81
|
+
// Windows: Standard custom install path in Downloads
|
|
82
|
+
binaryPath = path.join(homeDir, "Downloads", "brave", "brave.exe");
|
|
83
|
+
} else {
|
|
84
|
+
// Linux: Primary check in .cache (common for script installs)
|
|
85
|
+
const cachePath = path.join(homeDir, ".cache", "brave", "brave");
|
|
86
|
+
const downloadsPath = path.join(homeDir, "Downloads", "brave", "brave");
|
|
87
|
+
|
|
88
|
+
if (fs.existsSync(cachePath)) {
|
|
89
|
+
binaryPath = cachePath;
|
|
90
|
+
} else if (fs.existsSync(downloadsPath)) {
|
|
91
|
+
binaryPath = downloadsPath;
|
|
92
|
+
} else {
|
|
93
|
+
// Default to cache path for the error message if neither is found
|
|
94
|
+
binaryPath = cachePath;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!binaryPath || !fs.existsSync(binaryPath)) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`❌ [LaunchBrowser] Binary not found for ${browserName} at: ${binaryPath}\n` +
|
|
102
|
+
` Linux checked: ~/.cache/brave/brave AND ~/Downloads/brave/brave`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return binaryPath;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function cleanUpTempProfiles(ageLimitMinutes) {
|
|
110
|
+
if (!ageLimitMinutes || ageLimitMinutes < 10) return;
|
|
111
|
+
|
|
112
|
+
console.log(`🧹 [Cleanup] Checking for temp profiles older than ${ageLimitMinutes} mins...`);
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
const limit = ageLimitMinutes * 60 * 1000;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const files = fs.readdirSync(TEMP_DIR);
|
|
118
|
+
files.forEach((file) => {
|
|
119
|
+
const curPath = path.join(TEMP_DIR, file);
|
|
120
|
+
try {
|
|
121
|
+
const stats = fs.statSync(curPath);
|
|
122
|
+
if (stats.isDirectory() && now - stats.mtimeMs > limit) {
|
|
123
|
+
console.log(` Deleting expired temp profile: ${file}`);
|
|
124
|
+
deleteDirectoryWithRetries(curPath);
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
// Ignore file lock errors
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error("Cleanup Error:", err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Helper to generate consistent fingerprint options.
|
|
137
|
+
*/
|
|
138
|
+
function getFingerprintConfig(browserType, maxWidth) {
|
|
139
|
+
const config = {
|
|
140
|
+
devices: ["desktop"],
|
|
141
|
+
operatingSystems: [detectedOs],
|
|
142
|
+
locales: ["en-US"],
|
|
143
|
+
browsers: [],
|
|
144
|
+
screen: {},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (browserType === "chromium") {
|
|
148
|
+
config.browsers.push({ name: "chrome", minVersion: 140 });
|
|
149
|
+
} else if (browserType === "firefox") {
|
|
150
|
+
config.browsers.push({ name: "firefox", minVersion: 140 });
|
|
151
|
+
} else if (browserType === "brave") {
|
|
152
|
+
config.browsers.push({ name: "chrome", minVersion: 140 });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (maxWidth) {
|
|
156
|
+
const widthVal = maxWidth <= 10 ? Math.round(maxWidth * 1920) : maxWidth;
|
|
157
|
+
config.screen.maxWidth = widthVal;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return config;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ==========================================================================
|
|
164
|
+
// 3. MAIN LAUNCHER ENTRY POINT
|
|
165
|
+
// ==========================================================================
|
|
166
|
+
|
|
167
|
+
export async function launchBrowser({
|
|
168
|
+
// Common
|
|
169
|
+
timezoneId = null,
|
|
170
|
+
proxy,
|
|
171
|
+
maxWidth = 1,
|
|
172
|
+
|
|
173
|
+
// Path & Storage
|
|
174
|
+
profile_path = null,
|
|
175
|
+
custom_profile_path = null, // Backward (Just for throwing error)
|
|
176
|
+
cleanupMinutes = 0,
|
|
177
|
+
|
|
178
|
+
which_browser = "chromium",
|
|
179
|
+
CapSolver = false,
|
|
180
|
+
|
|
181
|
+
// Browser Specific Grouped Options
|
|
182
|
+
camoufox_options = {}, // { geoip, humanize, ... }
|
|
183
|
+
multilogin_options = {}, // { profileId, os_type, canvas_noise, ... }
|
|
184
|
+
}) {
|
|
185
|
+
try {
|
|
186
|
+
if (custom_profile_path) throw new Error("Please use profile_path");
|
|
187
|
+
|
|
188
|
+
// Resolve path using the browser type to ensure prefixes
|
|
189
|
+
const fullPath = resolveProfilePath(profile_path, which_browser);
|
|
190
|
+
cleanUpTempProfiles(cleanupMinutes);
|
|
191
|
+
|
|
192
|
+
let browserInstance;
|
|
193
|
+
|
|
194
|
+
switch (which_browser) {
|
|
195
|
+
case "chromium":
|
|
196
|
+
case "chrome":
|
|
197
|
+
browserInstance = await chromiumLauncher({
|
|
198
|
+
profilePath: fullPath,
|
|
199
|
+
proxy,
|
|
200
|
+
timezoneId,
|
|
201
|
+
CapSolver,
|
|
202
|
+
maxWidth,
|
|
203
|
+
});
|
|
204
|
+
break;
|
|
205
|
+
case "firefox":
|
|
206
|
+
browserInstance = await firefoxLauncher({
|
|
207
|
+
profilePath: fullPath,
|
|
208
|
+
proxy,
|
|
209
|
+
timezoneId,
|
|
210
|
+
maxWidth,
|
|
211
|
+
});
|
|
212
|
+
break;
|
|
213
|
+
case "brave":
|
|
214
|
+
case "braveLauncher":
|
|
215
|
+
browserInstance = await braveLauncher({
|
|
216
|
+
profilePath: fullPath,
|
|
217
|
+
proxy,
|
|
218
|
+
timezoneId,
|
|
219
|
+
CapSolver,
|
|
220
|
+
maxWidth,
|
|
221
|
+
});
|
|
222
|
+
break;
|
|
223
|
+
case "camoufox":
|
|
224
|
+
browserInstance = await camoufoxLauncher({
|
|
225
|
+
profilePath: fullPath,
|
|
226
|
+
proxy,
|
|
227
|
+
timezoneId,
|
|
228
|
+
maxWidth,
|
|
229
|
+
camoufox_options,
|
|
230
|
+
});
|
|
231
|
+
break;
|
|
232
|
+
case "multilogin":
|
|
233
|
+
browserInstance = await multiloginLauncher({
|
|
234
|
+
proxy,
|
|
235
|
+
multilogin_options,
|
|
236
|
+
});
|
|
237
|
+
break;
|
|
238
|
+
default:
|
|
239
|
+
throw new Error(`Unknown browser type: ${which_browser}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return browserInstance;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error("❌ [LaunchBrowser] Critical Error:", error.message || error);
|
|
245
|
+
return { data: null, error: error, closeBrowser: async () => {} };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ==========================================================================
|
|
250
|
+
// 4. ENGINE: CHROMIUM
|
|
251
|
+
// ==========================================================================
|
|
252
|
+
async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, maxWidth }) {
|
|
253
|
+
const isPersistent = !!profilePath;
|
|
254
|
+
|
|
255
|
+
// 1. Determine Path (Temp needs it for fingerprint storage, Persistent needs it for data)
|
|
256
|
+
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
257
|
+
console.log(`🚀 Starting Chromium [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
|
|
258
|
+
|
|
259
|
+
// 2. Define Args
|
|
260
|
+
const args = [
|
|
261
|
+
"--test-type",
|
|
262
|
+
// --- Stealth & Anti-Detection ---
|
|
263
|
+
"--disable-blink-features=AutomationControlled",
|
|
264
|
+
// "--disable-features=UserAgentClientHint",
|
|
265
|
+
// --- PREVENT EXTRA TABS ---
|
|
266
|
+
// "--homepage=about:blank",
|
|
267
|
+
"--disable-restore-session-state",
|
|
268
|
+
// --- Error Suppression ---
|
|
269
|
+
"--disable-session-crashed-bubble",
|
|
270
|
+
"--hide-crash-restore-bubble",
|
|
271
|
+
// --- Silence & Networking ---
|
|
272
|
+
"--disable-background-networking",
|
|
273
|
+
"--disable-background-timer-throttling",
|
|
274
|
+
"--disable-breakpad",
|
|
275
|
+
"--disable-crash-reporter",
|
|
276
|
+
"--disable-component-update",
|
|
277
|
+
"--disable-sync",
|
|
278
|
+
"--no-default-browser-check",
|
|
279
|
+
"--no-first-run",
|
|
280
|
+
"--disable-domain-reliability",
|
|
281
|
+
"--disable-client-side-phishing-detection",
|
|
282
|
+
// --- UI ---
|
|
283
|
+
"--disable-infobars",
|
|
284
|
+
];
|
|
285
|
+
const ignoreDefaultArgs = ["--enable-automation", "--no-sandbox"];
|
|
286
|
+
|
|
287
|
+
if (CapSolver) {
|
|
288
|
+
const extPath = path.resolve(`./utility/browser-fingerprint/CapSolver`);
|
|
289
|
+
args.push(`--disable-extensions-except=${extPath}`, `--load-extension=${extPath}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const proxyObj = formatProxy(proxy);
|
|
293
|
+
const tz = timezoneId || undefined;
|
|
294
|
+
|
|
295
|
+
// ==================================================================
|
|
296
|
+
// BRANCH A: TEMP PROFILE (launch + newInjectedContext)
|
|
297
|
+
// ==================================================================
|
|
298
|
+
if (!isPersistent) {
|
|
299
|
+
try {
|
|
300
|
+
const fpConfig = getFingerprintConfig("chromium", maxWidth);
|
|
301
|
+
const fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
|
|
302
|
+
|
|
303
|
+
// Launch standard browser (not persistent context)
|
|
304
|
+
const browser = await chromium.launch({
|
|
305
|
+
headless: false,
|
|
306
|
+
proxy: proxyObj,
|
|
307
|
+
args: args,
|
|
308
|
+
ignoreDefaultArgs: ignoreDefaultArgs,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Inject the fingerprint
|
|
312
|
+
const context = await newInjectedContext(browser, {
|
|
313
|
+
fingerprint: fingerprintData,
|
|
314
|
+
timezoneId: tz,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const page = context.pages()[0] || (await context.newPage());
|
|
318
|
+
|
|
319
|
+
// Pass 'activePath' so the temp folder (containing fingerprint.json) gets deleted on close
|
|
320
|
+
return createBrowserController(browser, context, page, activePath);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
console.error("Chromium Temp Launch Error:", err);
|
|
323
|
+
throw err;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// ==================================================================
|
|
327
|
+
// BRANCH B: PERSISTENT PROFILE (launchPersistentContext + Manual Script)
|
|
328
|
+
// ==================================================================
|
|
329
|
+
else {
|
|
330
|
+
try {
|
|
331
|
+
if (!fs.existsSync(activePath)) fs.mkdirSync(activePath, { recursive: true });
|
|
332
|
+
|
|
333
|
+
// Logic: Native Persistent Launch. No fingerprint-injector.
|
|
334
|
+
const context = await chromium.launchPersistentContext(activePath, {
|
|
335
|
+
headless: false,
|
|
336
|
+
proxy: proxyObj,
|
|
337
|
+
args: args,
|
|
338
|
+
timezoneId: tz,
|
|
339
|
+
ignoreDefaultArgs: ignoreDefaultArgs,
|
|
340
|
+
viewport: null,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Manual Stealth Script
|
|
344
|
+
await addStealthScript(context);
|
|
345
|
+
|
|
346
|
+
const page = context.pages()[0];
|
|
347
|
+
|
|
348
|
+
return createBrowserController(context, context, page, null);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.error("Chromium Persistent Launch Error:", err);
|
|
351
|
+
throw err;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ==========================================================================
|
|
357
|
+
// 5. ENGINE: FIREFOX
|
|
358
|
+
// ==========================================================================
|
|
359
|
+
async function firefoxLauncher({ profilePath, proxy, timezoneId, maxWidth }) {
|
|
360
|
+
const isPersistent = !!profilePath;
|
|
361
|
+
|
|
362
|
+
// 1. Determine Path
|
|
363
|
+
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
364
|
+
console.log(`🚀 Starting Firefox [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
|
|
365
|
+
|
|
366
|
+
const proxyObj = formatProxy(proxy);
|
|
367
|
+
const tz = timezoneId || undefined;
|
|
368
|
+
|
|
369
|
+
// Firefox specific preferences
|
|
370
|
+
const firefoxUserPrefs = {
|
|
371
|
+
"dom.webdriver.enabled": false,
|
|
372
|
+
useAutomationExtension: false,
|
|
373
|
+
"media.peerconnection.enabled": false,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// ==================================================================
|
|
377
|
+
// BRANCH A: TEMP PROFILE (launch + newInjectedContext)
|
|
378
|
+
// ==================================================================
|
|
379
|
+
if (!isPersistent) {
|
|
380
|
+
try {
|
|
381
|
+
const fpConfig = getFingerprintConfig("firefox", maxWidth);
|
|
382
|
+
const fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
|
|
383
|
+
|
|
384
|
+
const browser = await firefox.launch({
|
|
385
|
+
headless: false,
|
|
386
|
+
proxy: proxyObj,
|
|
387
|
+
ignoreDefaultArgs: ["--enable-automation"],
|
|
388
|
+
firefoxUserPrefs: firefoxUserPrefs,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const context = await newInjectedContext(browser, {
|
|
392
|
+
fingerprint: fingerprintData,
|
|
393
|
+
timezoneId: tz,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const page = context.pages()[0] || (await context.newPage());
|
|
397
|
+
|
|
398
|
+
// Pass 'activePath' to delete temp folder later
|
|
399
|
+
return createBrowserController(browser, context, page, activePath);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
console.error("Firefox Temp Launch Error:", err);
|
|
402
|
+
throw err;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// ==================================================================
|
|
406
|
+
// BRANCH B: PERSISTENT PROFILE (launchPersistentContext + Manual Script)
|
|
407
|
+
// ==================================================================
|
|
408
|
+
else {
|
|
409
|
+
try {
|
|
410
|
+
if (!fs.existsSync(activePath)) fs.mkdirSync(activePath, { recursive: true });
|
|
411
|
+
|
|
412
|
+
const context = await firefox.launchPersistentContext(activePath, {
|
|
413
|
+
headless: false,
|
|
414
|
+
proxy: proxyObj,
|
|
415
|
+
timezoneId: tz,
|
|
416
|
+
ignoreDefaultArgs: ["--enable-automation"],
|
|
417
|
+
// firefoxUserPrefs: firefoxUserPrefs,
|
|
418
|
+
viewport: null,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Manual Stealth Script
|
|
422
|
+
await addStealthScript(context);
|
|
423
|
+
|
|
424
|
+
const page = context.pages()[0];
|
|
425
|
+
|
|
426
|
+
return createBrowserController(context, context, page, null);
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.error("Firefox Persistent Launch Error:", err);
|
|
429
|
+
throw err;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ==========================================================================
|
|
435
|
+
// 6. ENGINE: BRAVE
|
|
436
|
+
// ==========================================================================
|
|
437
|
+
async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, maxWidth }) {
|
|
438
|
+
const isPersistent = !!profilePath;
|
|
439
|
+
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
440
|
+
|
|
441
|
+
console.log(`🚀 Starting Brave: ${activePath}`);
|
|
442
|
+
fs.mkdirSync(activePath, { recursive: true });
|
|
443
|
+
|
|
444
|
+
let fingerprintData;
|
|
445
|
+
const fingerprintFilePath = path.join(activePath, "fingerprint.json");
|
|
446
|
+
|
|
447
|
+
if (fs.existsSync(fingerprintFilePath)) {
|
|
448
|
+
fingerprintData = JSON.parse(fs.readFileSync(fingerprintFilePath, "utf-8"));
|
|
449
|
+
} else {
|
|
450
|
+
const fpConfig = getFingerprintConfig("brave", maxWidth);
|
|
451
|
+
fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
|
|
452
|
+
|
|
453
|
+
if (isPersistent) {
|
|
454
|
+
fs.writeFileSync(fingerprintFilePath, JSON.stringify(fingerprintData, null, 2), "utf-8");
|
|
455
|
+
console.log("📝 Generated and saved new fingerprint for Brave");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const braveBin = getBinaryPath("brave");
|
|
460
|
+
|
|
461
|
+
const args = [
|
|
462
|
+
"--test-type",
|
|
463
|
+
// --- Stealth & Anti-Detection ---
|
|
464
|
+
"--disable-blink-features=AutomationControlled",
|
|
465
|
+
//"--disable-features=UserAgentClientHint",
|
|
466
|
+
|
|
467
|
+
// --- PREVENT EXTRA TABS (New Flags) ---
|
|
468
|
+
// "--no-startup-window",
|
|
469
|
+
"--homepage=about:blank",
|
|
470
|
+
"--disable-restore-session-state", // Prevents restoring old tabs
|
|
471
|
+
|
|
472
|
+
// --- Error Suppression ---
|
|
473
|
+
"--disable-session-crashed-bubble",
|
|
474
|
+
"--hide-crash-restore-bubble",
|
|
475
|
+
|
|
476
|
+
// --- Silence & Networking ---
|
|
477
|
+
"--disable-background-networking",
|
|
478
|
+
"--disable-background-timer-throttling",
|
|
479
|
+
"--disable-breakpad",
|
|
480
|
+
"--disable-crash-reporter",
|
|
481
|
+
"--disable-component-update",
|
|
482
|
+
"--disable-sync",
|
|
483
|
+
"--no-default-browser-check",
|
|
484
|
+
"--no-first-run",
|
|
485
|
+
"--disable-domain-reliability",
|
|
486
|
+
"--disable-client-side-phishing-detection",
|
|
487
|
+
|
|
488
|
+
// --- Brave Specific Silence ---
|
|
489
|
+
"--disable-features=Translate,BraveRewards,BraveWallet,BraveNews",
|
|
490
|
+
"--disable-infobars",
|
|
491
|
+
];
|
|
492
|
+
const ignoreDefaultArgs = ["--enable-automation", "--no-sandbox"];
|
|
493
|
+
if (CapSolver) {
|
|
494
|
+
const extPath = path.resolve(`./utility/browser-fingerprint/CapSolver`);
|
|
495
|
+
args.push(`--load-extension=${extPath}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const proxyObj = formatProxy(proxy);
|
|
499
|
+
const tz = timezoneId || undefined;
|
|
500
|
+
|
|
501
|
+
const context = await chromium.launchPersistentContext(activePath, {
|
|
502
|
+
headless: false,
|
|
503
|
+
executablePath: braveBin,
|
|
504
|
+
proxy: proxyObj,
|
|
505
|
+
timezoneId: tz,
|
|
506
|
+
ignoreDefaultArgs: ignoreDefaultArgs,
|
|
507
|
+
args: args,
|
|
508
|
+
userAgent: fingerprintData.fingerprint.navigator.userAgent,
|
|
509
|
+
viewport: fingerprintData.fingerprint.screen,
|
|
510
|
+
locale: fingerprintData.fingerprint.navigator.language,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
await new FingerprintInjector().attachFingerprintToPlaywright(context, fingerprintData);
|
|
514
|
+
|
|
515
|
+
// ======================================================
|
|
516
|
+
// 🛠️ FIX: Close Extra Tabs (Brave Dashboard / Restore)
|
|
517
|
+
// ======================================================
|
|
518
|
+
const pages = context.pages();
|
|
519
|
+
if (pages.length > 1) {
|
|
520
|
+
console.log(`🧹 Found ${pages.length} tabs open in Brave. Closing extras...`);
|
|
521
|
+
// Close all tabs except the last one (which is usually the fresh active one)
|
|
522
|
+
for (let i = 0; i < pages.length - 1; i++) {
|
|
523
|
+
await pages[i].close();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const page = context.pages()[0];
|
|
527
|
+
|
|
528
|
+
const dirToDelete = isPersistent ? null : activePath;
|
|
529
|
+
// Return the remaining open page (context.pages() might change, so we grab index 0 after cleanup)
|
|
530
|
+
return createBrowserController(context, context, page, dirToDelete);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ==========================================================================
|
|
534
|
+
// 7. ENGINE: CAMOUFOX
|
|
535
|
+
// ==========================================================================
|
|
536
|
+
async function camoufoxLauncher({ profilePath, proxy, timezoneId, maxWidth, camoufox_options = {} }) {
|
|
537
|
+
const isPersistent = !!profilePath;
|
|
538
|
+
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
539
|
+
|
|
540
|
+
console.log(`🚀 Starting camoufoxJs: ${activePath}`);
|
|
541
|
+
fs.mkdirSync(activePath, { recursive: true });
|
|
542
|
+
|
|
543
|
+
const proxyObj = formatProxy(proxy);
|
|
544
|
+
const tz = timezoneId || undefined;
|
|
545
|
+
|
|
546
|
+
// Check if GeoIP is enabled in options (Default to false if undefined)
|
|
547
|
+
const isGeoIpEnabled = camoufox_options.geoip === true;
|
|
548
|
+
|
|
549
|
+
let launchConfig;
|
|
550
|
+
const fingerprintFilePath = path.join(activePath, "fingerprint.json");
|
|
551
|
+
|
|
552
|
+
// 1. Load existing or Generate new config
|
|
553
|
+
if (fs.existsSync(fingerprintFilePath)) {
|
|
554
|
+
// Load the previously generated launch options
|
|
555
|
+
launchConfig = JSON.parse(fs.readFileSync(fingerprintFilePath, "utf-8"));
|
|
556
|
+
|
|
557
|
+
// IMPORTANT: For persistent profiles, if we are NOT using GeoIP (static behavior),
|
|
558
|
+
// we usually want to ensure the proxy for *this* run is applied, as it might have changed.
|
|
559
|
+
if (!isGeoIpEnabled && proxyObj) {
|
|
560
|
+
launchConfig.proxy = proxyObj;
|
|
561
|
+
}
|
|
562
|
+
} else {
|
|
563
|
+
function getRandomValidMajorVersion() {
|
|
564
|
+
// Valid Major Versions (135 - 145)
|
|
565
|
+
const validMajorVersions = [135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145];
|
|
566
|
+
const randomIndex = Math.floor(Math.random() * validMajorVersions.length);
|
|
567
|
+
return validMajorVersions[randomIndex];
|
|
568
|
+
}
|
|
569
|
+
const screenWidth = maxWidth ? (maxWidth <= 10 ? Math.round(maxWidth * 1920) : maxWidth) : 1920;
|
|
570
|
+
|
|
571
|
+
// Prepare the base options
|
|
572
|
+
const optionsGenerator = {
|
|
573
|
+
ff_version: camoufox_options.ff_version || getRandomValidMajorVersion(),
|
|
574
|
+
headless: camoufox_options.headless || false,
|
|
575
|
+
humanize: camoufox_options.humanize ?? true,
|
|
576
|
+
block_images: camoufox_options.block_images ?? false,
|
|
577
|
+
block_webrtc: camoufox_options.block_webrtc ?? false,
|
|
578
|
+
i_know_what_im_doing: true, // Suppress warnings if manual config intentional
|
|
579
|
+
geoip: isGeoIpEnabled,
|
|
580
|
+
|
|
581
|
+
// Apply the strictly validated OS
|
|
582
|
+
os: camoufox_options.os || detectedOs,
|
|
583
|
+
|
|
584
|
+
...camoufox_options, // Spread user overrides
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
if (camoufox_options.screen) {
|
|
588
|
+
optionsGenerator.screen = camoufox_options.screen;
|
|
589
|
+
} else {
|
|
590
|
+
optionsGenerator.screen = {
|
|
591
|
+
maxWidth: screenWidth,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
// If GeoIP is TRUE, pass proxy inside launchOptions
|
|
595
|
+
if (isGeoIpEnabled && proxyObj) {
|
|
596
|
+
optionsGenerator.proxy = proxyObj;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// If GeoIP is TRUE, pass proxy inside launchOptions
|
|
600
|
+
if (isGeoIpEnabled && proxyObj) {
|
|
601
|
+
optionsGenerator.proxy = proxyObj;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Generate full launch config via camoufox-js
|
|
605
|
+
launchConfig = await launchOptions(optionsGenerator);
|
|
606
|
+
|
|
607
|
+
// Save persistent config
|
|
608
|
+
if (isPersistent) {
|
|
609
|
+
fs.writeFileSync(fingerprintFilePath, JSON.stringify(launchConfig, null, 2), "utf-8");
|
|
610
|
+
console.log("📝 Generated and saved new launch config for Camoufox");
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Post-Generation Adjustments for Playwright Connection
|
|
615
|
+
if (!isGeoIpEnabled && proxyObj) {
|
|
616
|
+
launchConfig.proxy = proxyObj;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (!isGeoIpEnabled && tz) {
|
|
620
|
+
launchConfig.timezoneId = tz;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const context = await firefox.launchPersistentContext(activePath, {
|
|
624
|
+
...launchConfig,
|
|
625
|
+
// Removed viewport: null, handled by Camoufox args
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const dirToDelete = isPersistent ? null : activePath;
|
|
629
|
+
return createBrowserController(context, context, context.pages()[0], dirToDelete);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// ==========================================================================
|
|
633
|
+
// 8. ENGINE: MULTILOGIN
|
|
634
|
+
// ==========================================================================
|
|
635
|
+
async function multiloginLauncher({ proxy, multilogin_options = {} }) {
|
|
636
|
+
// Destructure defaults from multilogin_options
|
|
637
|
+
const {
|
|
638
|
+
profileId = null,
|
|
639
|
+
os_type = detectedOs,
|
|
640
|
+
canvas_noise = true,
|
|
641
|
+
media_masking = true,
|
|
642
|
+
audio_masking = true,
|
|
643
|
+
custom_screen = false,
|
|
644
|
+
} = multilogin_options;
|
|
645
|
+
|
|
646
|
+
if (profileId) {
|
|
647
|
+
return await launchExistingMultiloginProfile(profileId);
|
|
648
|
+
} else {
|
|
649
|
+
return await launchQuickMultiloginProfile({
|
|
650
|
+
os_type,
|
|
651
|
+
proxy,
|
|
652
|
+
canvas_noise,
|
|
653
|
+
media_masking,
|
|
654
|
+
audio_masking,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async function launchExistingMultiloginProfile(profileId) {
|
|
660
|
+
const startUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v2/profile/f/${MULTILOGIN_FOLDER_ID}/p/${profileId}/start?automation_type=playwright&headless_mode=false`;
|
|
661
|
+
let browser, context, page;
|
|
662
|
+
|
|
663
|
+
try {
|
|
664
|
+
const token = await getMultiloginToken();
|
|
665
|
+
let response = await fetch(startUrl, {
|
|
666
|
+
method: "GET",
|
|
667
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json", "X-Strict-Mode": "true" },
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
if (response.status === 400) {
|
|
671
|
+
const errorJson = await response.json().catch(() => null);
|
|
672
|
+
if (errorJson?.status?.error_code === "PROFILE_ALREADY_RUNNING") {
|
|
673
|
+
console.log(`⚠️ Profile ${profileId} is running. Restarting...`);
|
|
674
|
+
await stopMultiloginProfile(profileId);
|
|
675
|
+
await sleep(5000);
|
|
676
|
+
response = await fetch(startUrl, {
|
|
677
|
+
method: "GET",
|
|
678
|
+
headers: {
|
|
679
|
+
Authorization: `Bearer ${await getMultiloginToken()}`,
|
|
680
|
+
Accept: "application/json",
|
|
681
|
+
"X-Strict-Mode": "true",
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
} else {
|
|
685
|
+
throw new Error(`Multilogin 400 Error: ${JSON.stringify(errorJson)}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (!response.ok) throw new Error(`Failed to start profile: ${await response.text()}`);
|
|
690
|
+
|
|
691
|
+
const data = await response.json();
|
|
692
|
+
const port = data.data.port;
|
|
693
|
+
console.log(`✅ Multilogin Profile ${profileId} started on port ${port}`);
|
|
694
|
+
|
|
695
|
+
browser = await chromium.connectOverCDP(`http://localhost:${port}`);
|
|
696
|
+
context = browser.contexts()[0];
|
|
697
|
+
page = context.pages()[0];
|
|
698
|
+
|
|
699
|
+
const closeBrowser = async () => {
|
|
700
|
+
if (browser) await browser.close().catch(() => {});
|
|
701
|
+
return await stopMultiloginProfile(profileId);
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
return { browser, context, page, isBrowserRunning: () => !!browser, closeBrowser, launchError: null };
|
|
705
|
+
} catch (error) {
|
|
706
|
+
console.error("Multilogin Launch Error:", error.message);
|
|
707
|
+
try {
|
|
708
|
+
await stopMultiloginProfile(profileId);
|
|
709
|
+
} catch (e) {}
|
|
710
|
+
return { data: null, error, closeBrowser: async () => {} };
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking }) {
|
|
715
|
+
const createUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v3/profile/quick`;
|
|
716
|
+
let browser, context, page, profileId;
|
|
717
|
+
|
|
718
|
+
const requestBody = {
|
|
719
|
+
browser_type: "mimic",
|
|
720
|
+
os_type,
|
|
721
|
+
automation: "playwright",
|
|
722
|
+
is_headless: false,
|
|
723
|
+
parameters: {
|
|
724
|
+
flags: {
|
|
725
|
+
audio_masking: audio_masking ? "mask" : "natural",
|
|
726
|
+
media_devices_masking: media_masking ? "mask" : "natural",
|
|
727
|
+
screen_masking: "natural",
|
|
728
|
+
canvas_noise: canvas_noise ? "mask" : "natural",
|
|
729
|
+
proxy_masking: proxy ? "custom" : "disabled",
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
quickProfilesCount: 1,
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
if (proxy) {
|
|
736
|
+
const pObj = formatProxy(proxy);
|
|
737
|
+
if (pObj) {
|
|
738
|
+
const u = new URL(pObj.server);
|
|
739
|
+
requestBody.parameters.proxy = {
|
|
740
|
+
type: "http",
|
|
741
|
+
host: u.hostname,
|
|
742
|
+
port: Number(u.port),
|
|
743
|
+
username: pObj.username || "",
|
|
744
|
+
password: pObj.password || "",
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
const token = await getMultiloginToken();
|
|
751
|
+
const response = await fetch(createUrl, {
|
|
752
|
+
method: "POST",
|
|
753
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
754
|
+
body: JSON.stringify(requestBody),
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
if (!response.ok) throw new Error(await response.text());
|
|
758
|
+
const json = await response.json();
|
|
759
|
+
|
|
760
|
+
profileId = json.data.id;
|
|
761
|
+
const port = json.data.port;
|
|
762
|
+
console.log(`✅ Quick Profile Created: ${profileId} on port ${port}`);
|
|
763
|
+
|
|
764
|
+
browser = await chromium.connectOverCDP(`http://localhost:${port}`);
|
|
765
|
+
context = browser.contexts()[0];
|
|
766
|
+
page = context.pages()[0];
|
|
767
|
+
|
|
768
|
+
const closeBrowser = async () => {
|
|
769
|
+
if (browser) await browser.close().catch(() => {});
|
|
770
|
+
return await stopMultiloginProfile(profileId);
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
return { browser, context, page, isBrowserRunning: () => !!browser, closeBrowser, launchError: null };
|
|
774
|
+
} catch (error) {
|
|
775
|
+
console.error("Quick Profile Error:", error);
|
|
776
|
+
if (profileId) await stopMultiloginProfile(profileId);
|
|
777
|
+
return { data: null, error, closeBrowser: async () => {} };
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function stopMultiloginProfile(profileId) {
|
|
782
|
+
const stopUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v1/profile/stop/p/${profileId}`;
|
|
783
|
+
try {
|
|
784
|
+
const token = await getMultiloginToken();
|
|
785
|
+
const response = await fetch(stopUrl, {
|
|
786
|
+
method: "GET",
|
|
787
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
if (!response.ok) {
|
|
791
|
+
const txt = await response.text();
|
|
792
|
+
if (response.status === 500 && txt.includes("profile already stopped")) {
|
|
793
|
+
return { error: null };
|
|
794
|
+
}
|
|
795
|
+
throw new Error(txt);
|
|
796
|
+
}
|
|
797
|
+
console.log(`🛑 Profile ${profileId} stopped.`);
|
|
798
|
+
return { error: null };
|
|
799
|
+
} catch (err) {
|
|
800
|
+
console.error(`Error stopping ${profileId}:`, err);
|
|
801
|
+
return { error: err };
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
// ==========================================================================
|
|
805
|
+
// 10. SHARED UTILITIES
|
|
806
|
+
// ==========================================================================
|
|
807
|
+
|
|
808
|
+
async function addStealthScript(context) {
|
|
809
|
+
await context.addInitScript(() => {
|
|
810
|
+
// Keep these deletions, they are Playwright-specific bindings
|
|
811
|
+
delete window.__pwInitScripts;
|
|
812
|
+
// delete window.playwright;
|
|
813
|
+
delete window.__playwright__binding__;
|
|
814
|
+
delete window.__playwright__binding__controller__;
|
|
815
|
+
|
|
816
|
+
// Check if navigator.webdriver exists and is true (indicating automation)
|
|
817
|
+
if (navigator.webdriver) {
|
|
818
|
+
// console.log("33333333333333333333333 - Automation detected, patching to false");
|
|
819
|
+
|
|
820
|
+
// Define a new getter for the property on the prototype
|
|
821
|
+
// This overrides the native 'true' with 'false'
|
|
822
|
+
Object.defineProperty(Object.getPrototypeOf(navigator), "webdriver", {
|
|
823
|
+
get: () => false,
|
|
824
|
+
});
|
|
825
|
+
} else {
|
|
826
|
+
// If it's already false or undefined, we leave it alone to look natural
|
|
827
|
+
// console.log("Navigator.webdriver is already safe (" + navigator.webdriver + ")");
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
function formatProxy(proxy) {
|
|
832
|
+
if (!proxy) return undefined;
|
|
833
|
+
if (typeof proxy === "string") return { server: proxy };
|
|
834
|
+
|
|
835
|
+
const p = { server: `${proxy.type}://${proxy.host}:${proxy.port}` };
|
|
836
|
+
if (proxy.user) {
|
|
837
|
+
p.username = proxy.user;
|
|
838
|
+
p.password = proxy.pass;
|
|
839
|
+
}
|
|
840
|
+
return p;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function createBrowserController(browser, context, page, dirToDelete = null) {
|
|
844
|
+
const closeBrowser = async () => {
|
|
845
|
+
try {
|
|
846
|
+
console.log("🔒 Closing browser session...");
|
|
847
|
+
if (context) await context.close();
|
|
848
|
+
if (browser && typeof browser.close === "function" && browser !== context) {
|
|
849
|
+
try {
|
|
850
|
+
await browser.close();
|
|
851
|
+
} catch (e) {}
|
|
852
|
+
}
|
|
853
|
+
if (dirToDelete) {
|
|
854
|
+
if (dirToDelete.includes("persistent")) {
|
|
855
|
+
console.warn(`⚠️ Safety Block: Attempted to delete persistent profile: ${dirToDelete}`);
|
|
856
|
+
} else {
|
|
857
|
+
console.log(`🗑️ Deleting temp profile: ${dirToDelete}`);
|
|
858
|
+
await deleteDirectoryWithRetries(dirToDelete);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return true;
|
|
862
|
+
} catch (err) {
|
|
863
|
+
console.error("Error closing browser:", err);
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
return { browser, context, page, isBrowserRunning: () => !!context, closeBrowser, launchError: null };
|
|
868
|
+
}
|