arn-browser 0.1.17 → 0.1.19
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/install.js +4 -4
- package/package.json +8 -7
- package/src/utility/mlx_token.js +11 -35
- package/src/utility/playwright/pwLaunch.d.ts +37 -8
- package/src/utility/playwright/pwLaunch.js +78 -30
- package/src/utility/playwright/routes/pwRoute.d.ts +3 -0
- package/src/utility/playwright/routes/pwRoute.js +42 -5
- package/src/utility/proxy-utility/proxy-chain.d.ts +11 -0
- package/src/utility/proxy-utility/proxy-chain.js +61 -75
- package/src/utility/puppeteer/ppLaunch.d.ts +42 -2
- package/src/utility/puppeteer/ppLaunch.js +68 -16
- package/src/utility/puppeteer/routes/ppRoute.d.ts +3 -0
- package/src/utility/puppeteer/routes/ppRoute.js +42 -5
package/bin/install.js
CHANGED
|
@@ -29,14 +29,14 @@ import { createWriteStream } from "fs";
|
|
|
29
29
|
|
|
30
30
|
const BRAVE_URLS = {
|
|
31
31
|
linux: {
|
|
32
|
-
x64: "https://github.com/brave/brave-browser/releases/download/v1.
|
|
33
|
-
arm64: "https://github.com/brave/brave-browser/releases/download/v1.
|
|
32
|
+
x64: "https://github.com/brave/brave-browser/releases/download/v1.88.134/brave-browser-1.88.134-linux-amd64.zip",
|
|
33
|
+
arm64: "https://github.com/brave/brave-browser/releases/download/v1.88.134/brave-browser-1.88.134-linux-arm64.zip",
|
|
34
34
|
},
|
|
35
35
|
darwin: {
|
|
36
|
-
arm64: "https://github.com/brave/brave-browser/releases/download/v1.
|
|
36
|
+
arm64: "https://github.com/brave/brave-browser/releases/download/v1.88.134/Brave-Browser-arm64.dmg",
|
|
37
37
|
},
|
|
38
38
|
win32: {
|
|
39
|
-
x64: "https://github.com/brave/brave-browser/releases/download/v1.
|
|
39
|
+
x64: "https://github.com/brave/brave-browser/releases/download/v1.88.134/brave-v1.88.134-win32-x64.zip",
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
42
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arn-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "A lightweight, browser autmation helper.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -12,21 +12,21 @@
|
|
|
12
12
|
"test": "test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@aws-sdk/client-ec2": "^3.
|
|
15
|
+
"@aws-sdk/client-ec2": "^3.1015.0",
|
|
16
16
|
"@ghostery/adblocker": "^2.13.0",
|
|
17
17
|
"arn-knexjs": "^0.0.3",
|
|
18
|
-
"camoufox-js": "^0.9.
|
|
18
|
+
"camoufox-js": "^0.9.3",
|
|
19
19
|
"dotenv": "^17.2.3",
|
|
20
20
|
"fingerprint-generator": "^2.1.78",
|
|
21
21
|
"fingerprint-injector": "^2.1.78",
|
|
22
|
-
"https-proxy-agent": "^
|
|
22
|
+
"https-proxy-agent": "^8.0.0",
|
|
23
23
|
"node-cache": "^5.1.2",
|
|
24
24
|
"node-fetch": "^3.3.2",
|
|
25
25
|
"playwright-core": "1.42.1",
|
|
26
26
|
"proxy-chain": "^2.6.0",
|
|
27
|
-
"puppeteer-core": "^24.
|
|
27
|
+
"puppeteer-core": "^24.40.0",
|
|
28
28
|
"randomstring": "^1.3.1",
|
|
29
|
-
"socks-proxy-agent": "^
|
|
29
|
+
"socks-proxy-agent": "^9.0.0",
|
|
30
30
|
"speakeasy": "^2.0.0",
|
|
31
31
|
"superagent": "^10.2.3"
|
|
32
32
|
},
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"arn-browser": "bin/cli.js"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
37
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
38
|
+
"postinstall": "npm install playwright-core@1.42.1 --no-save"
|
|
38
39
|
},
|
|
39
40
|
"keywords": [
|
|
40
41
|
"browser",
|
package/src/utility/mlx_token.js
CHANGED
|
@@ -3,18 +3,12 @@
|
|
|
3
3
|
import crypto from "crypto";
|
|
4
4
|
import { arn, query } from "arn-knexjs";
|
|
5
5
|
|
|
6
|
-
// Module-level cache for credentials (fetched once per process)
|
|
7
|
-
let _credentials = null;
|
|
8
|
-
|
|
9
6
|
/**
|
|
10
7
|
* Loads Multilogin credentials from the database.
|
|
11
|
-
*
|
|
12
|
-
* Results are cached in-memory so the DB is only hit once per process.
|
|
8
|
+
* Always reads fresh from DB (no in-memory cache) to support multiple processes.
|
|
13
9
|
* @returns {Promise<{id: string, email: string, password: string, workspace_id: string, data: object}>}
|
|
14
10
|
*/
|
|
15
11
|
async function loadCredentials() {
|
|
16
|
-
if (_credentials) return _credentials;
|
|
17
|
-
|
|
18
12
|
const { data: [row] = [], error } = await arn.single(
|
|
19
13
|
query("api_multilogin_token").select("*").where({ status: 1 }).orderByRaw("random()").limit(1)
|
|
20
14
|
);
|
|
@@ -23,13 +17,10 @@ async function loadCredentials() {
|
|
|
23
17
|
throw new Error("[TokenManager] No active Multilogin credentials found (status = 1).");
|
|
24
18
|
}
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
return _credentials;
|
|
20
|
+
return row;
|
|
28
21
|
}
|
|
29
22
|
|
|
30
|
-
async function saveTokens(tokens) {
|
|
31
|
-
const creds = await loadCredentials();
|
|
32
|
-
|
|
23
|
+
async function saveTokens(creds, tokens) {
|
|
33
24
|
await arn.single(
|
|
34
25
|
query("api_multilogin_token")
|
|
35
26
|
.update({
|
|
@@ -39,18 +30,6 @@ async function saveTokens(tokens) {
|
|
|
39
30
|
);
|
|
40
31
|
}
|
|
41
32
|
|
|
42
|
-
async function loadTokens() {
|
|
43
|
-
try {
|
|
44
|
-
const creds = await loadCredentials();
|
|
45
|
-
if (!creds.data || Object.keys(creds.data).length === 0) {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
return creds.data;
|
|
49
|
-
} catch (err) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
33
|
function isJwtExpired(token) {
|
|
55
34
|
if (!token) return true;
|
|
56
35
|
try {
|
|
@@ -62,9 +41,7 @@ function isJwtExpired(token) {
|
|
|
62
41
|
}
|
|
63
42
|
}
|
|
64
43
|
|
|
65
|
-
async function loginAndSaveTokens() {
|
|
66
|
-
const creds = await loadCredentials();
|
|
67
|
-
|
|
44
|
+
async function loginAndSaveTokens(creds) {
|
|
68
45
|
const passwordHash = crypto.createHash("md5").update(creds.password).digest("hex");
|
|
69
46
|
const data = {
|
|
70
47
|
email: creds.email,
|
|
@@ -85,7 +62,7 @@ async function loginAndSaveTokens() {
|
|
|
85
62
|
const json = await res.json();
|
|
86
63
|
const { token, refresh_token } = json.data;
|
|
87
64
|
|
|
88
|
-
await saveTokens({
|
|
65
|
+
await saveTokens(creds, {
|
|
89
66
|
token,
|
|
90
67
|
refresh_token,
|
|
91
68
|
email: creds.email,
|
|
@@ -96,9 +73,7 @@ async function loginAndSaveTokens() {
|
|
|
96
73
|
return { token, refresh_token };
|
|
97
74
|
}
|
|
98
75
|
|
|
99
|
-
async function refreshAndSaveTokens(refresh_token) {
|
|
100
|
-
const creds = await loadCredentials();
|
|
101
|
-
|
|
76
|
+
async function refreshAndSaveTokens(creds, refresh_token) {
|
|
102
77
|
const data = {
|
|
103
78
|
email: creds.email,
|
|
104
79
|
refresh_token: refresh_token,
|
|
@@ -119,7 +94,7 @@ async function refreshAndSaveTokens(refresh_token) {
|
|
|
119
94
|
const json = await res.json();
|
|
120
95
|
const { token, refresh_token: new_refresh } = json.data;
|
|
121
96
|
|
|
122
|
-
await saveTokens({
|
|
97
|
+
await saveTokens(creds, {
|
|
123
98
|
token,
|
|
124
99
|
refresh_token: new_refresh,
|
|
125
100
|
email: creds.email,
|
|
@@ -131,7 +106,8 @@ async function refreshAndSaveTokens(refresh_token) {
|
|
|
131
106
|
}
|
|
132
107
|
|
|
133
108
|
async function getMultiloginToken() {
|
|
134
|
-
|
|
109
|
+
const creds = await loadCredentials();
|
|
110
|
+
const tokens = creds.data && Object.keys(creds.data).length > 0 ? creds.data : null;
|
|
135
111
|
|
|
136
112
|
if (tokens && tokens.token && !isJwtExpired(tokens.token)) {
|
|
137
113
|
return tokens.token;
|
|
@@ -139,14 +115,14 @@ async function getMultiloginToken() {
|
|
|
139
115
|
|
|
140
116
|
if (tokens && tokens.refresh_token) {
|
|
141
117
|
try {
|
|
142
|
-
const { token } = await refreshAndSaveTokens(tokens.refresh_token);
|
|
118
|
+
const { token } = await refreshAndSaveTokens(creds, tokens.refresh_token);
|
|
143
119
|
return token;
|
|
144
120
|
} catch (err) {
|
|
145
121
|
console.warn("[REFRESH FAILED] Will login again:", err.message);
|
|
146
122
|
}
|
|
147
123
|
}
|
|
148
124
|
|
|
149
|
-
const { token } = await loginAndSaveTokens();
|
|
125
|
+
const { token } = await loginAndSaveTokens(creds);
|
|
150
126
|
return token;
|
|
151
127
|
}
|
|
152
128
|
|
|
@@ -110,10 +110,12 @@ export interface MultiloginOptions {
|
|
|
110
110
|
audio_masking?: boolean;
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
* Multilogin flag:
|
|
114
|
-
*
|
|
113
|
+
* Multilogin flag: Screen resolution masking.
|
|
114
|
+
* true = randomize screen resolution ("mask")
|
|
115
|
+
* false = use real screen resolution ("natural")
|
|
116
|
+
* Default: true
|
|
115
117
|
*/
|
|
116
|
-
|
|
118
|
+
screen_masking?: boolean;
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
/**
|
|
@@ -192,9 +194,12 @@ export interface PwLaunchOptions {
|
|
|
192
194
|
* Configuration for browser fingerprint spoofing.
|
|
193
195
|
*
|
|
194
196
|
* - `false`: No fingerprint spoofing (default)
|
|
195
|
-
* - `true`: Spoof fingerprint with viewport: null
|
|
196
|
-
* - `
|
|
197
|
-
*
|
|
197
|
+
* - `true`: Spoof fingerprint with defaults (detected OS, auto device, viewport: null)
|
|
198
|
+
* - `object`: Spoof fingerprint with optional overrides
|
|
199
|
+
*
|
|
200
|
+
* Device type is auto-determined from OS:
|
|
201
|
+
* - `"windows"`, `"macos"`, `"linux"` → desktop
|
|
202
|
+
* - `"android"`, `"ios"` → mobile
|
|
198
203
|
*
|
|
199
204
|
* **For persistent Brave profiles:** The value is locked on first launch.
|
|
200
205
|
* Subsequent launches will use the saved value, ignoring the current parameter.
|
|
@@ -204,15 +209,39 @@ export interface PwLaunchOptions {
|
|
|
204
209
|
* spoof_fingerprint: true
|
|
205
210
|
*
|
|
206
211
|
* @example
|
|
207
|
-
* // Spoof
|
|
208
|
-
* spoof_fingerprint: {
|
|
212
|
+
* // Spoof as Windows
|
|
213
|
+
* spoof_fingerprint: { os_type: "windows" }
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* // Random OS per launch
|
|
217
|
+
* spoof_fingerprint: { os_type: ["windows", "macos"] }
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* // Mixed desktop + mobile
|
|
221
|
+
* spoof_fingerprint: { os_type: ["windows", "android"] }
|
|
209
222
|
*
|
|
210
223
|
* Default: false
|
|
211
224
|
*/
|
|
212
225
|
spoof_fingerprint?: boolean | {
|
|
226
|
+
/**
|
|
227
|
+
* Override the OS for fingerprint generation.
|
|
228
|
+
* Can be a single value or array (random pick per launch).
|
|
229
|
+
* Device type is auto-determined: windows/macos/linux → desktop, android/ios → mobile
|
|
230
|
+
* Default: detected OS of the machine
|
|
231
|
+
*/
|
|
232
|
+
os_type?: ("windows" | "macos" | "linux" | "android" | "ios") | ("windows" | "macos" | "linux" | "android" | "ios")[];
|
|
233
|
+
/**
|
|
234
|
+
* Minimum browser version for fingerprint generation.
|
|
235
|
+
* Default: 141
|
|
236
|
+
*/
|
|
237
|
+
minBrowserVersion?: number;
|
|
238
|
+
/** Minimum screen width constraint */
|
|
213
239
|
minWidth?: number;
|
|
240
|
+
/** Maximum screen width constraint */
|
|
214
241
|
maxWidth?: number;
|
|
242
|
+
/** Minimum screen height constraint */
|
|
215
243
|
minHeight?: number;
|
|
244
|
+
/** Maximum screen height constraint */
|
|
216
245
|
maxHeight?: number;
|
|
217
246
|
};
|
|
218
247
|
|
|
@@ -37,6 +37,10 @@ const osMap = {
|
|
|
37
37
|
};
|
|
38
38
|
const detectedOs = osMap[process.platform] || "windows";
|
|
39
39
|
|
|
40
|
+
// Default minimum browser versions for fingerprint generation
|
|
41
|
+
const DEFAULT_MIN_CHROME_VERSION = 141;
|
|
42
|
+
const DEFAULT_MIN_FIREFOX_VERSION = 141;
|
|
43
|
+
|
|
40
44
|
const MULTILOGIN_LAUNCHER_URL = "https://launcher.mlx.yt:45001";
|
|
41
45
|
const MULTILOGIN_FOLDER_ID = "bad9e7e1-cfab-4c8d-bd19-91aa82929711";
|
|
42
46
|
|
|
@@ -227,36 +231,80 @@ function cleanUpProfiles(skipPath) {
|
|
|
227
231
|
scanDir(TEMP_DIR);
|
|
228
232
|
}
|
|
229
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Maps OS to its device type.
|
|
236
|
+
*/
|
|
237
|
+
const OS_TO_DEVICE = {
|
|
238
|
+
windows: "desktop",
|
|
239
|
+
macos: "desktop",
|
|
240
|
+
linux: "desktop",
|
|
241
|
+
android: "mobile",
|
|
242
|
+
ios: "mobile",
|
|
243
|
+
};
|
|
244
|
+
|
|
230
245
|
/**
|
|
231
246
|
* Helper to generate consistent fingerprint options.
|
|
232
247
|
* @param {string} browserType - 'chromium' | 'firefox' | 'brave'
|
|
233
|
-
* @param {object|null}
|
|
248
|
+
* @param {object|null} spoofOptions - Options from spoof_fingerprint (null if boolean true)
|
|
249
|
+
* Supported properties:
|
|
250
|
+
* - os_type: string | string[] — e.g. "windows" or ["windows", "macos"]. Default: detectedOs
|
|
251
|
+
* - minBrowserVersion: number — minimum browser version for fingerprint. Default: 141
|
|
252
|
+
* - minWidth, maxWidth, minHeight, maxHeight: screen constraints
|
|
234
253
|
*/
|
|
235
|
-
function getFingerprintConfig(browserType,
|
|
254
|
+
function getFingerprintConfig(browserType, spoofOptions = null) {
|
|
255
|
+
let selectedOs = detectedOs;
|
|
256
|
+
let selectedDevice = OS_TO_DEVICE[detectedOs] || "desktop";
|
|
257
|
+
|
|
258
|
+
if (spoofOptions && typeof spoofOptions === 'object' && spoofOptions.os_type) {
|
|
259
|
+
const osList = Array.isArray(spoofOptions.os_type) ? spoofOptions.os_type : [spoofOptions.os_type];
|
|
260
|
+
|
|
261
|
+
if (osList.length === 0) {
|
|
262
|
+
throw new Error(`░░░░░ [Fingerprint] os_type array is empty.`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Validate all values
|
|
266
|
+
for (const os of osList) {
|
|
267
|
+
if (!OS_TO_DEVICE[os]) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`░░░░░ [Fingerprint] Unknown os_type: "${os}".\n` +
|
|
270
|
+
` Supported: "windows", "macos", "linux" (desktop), "android", "ios" (mobile)`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Random pick one
|
|
276
|
+
selectedOs = osList[Math.floor(Math.random() * osList.length)];
|
|
277
|
+
selectedDevice = OS_TO_DEVICE[selectedOs];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const minVersion = (spoofOptions && typeof spoofOptions === 'object' && spoofOptions.minBrowserVersion) || (browserType === "firefox" ? DEFAULT_MIN_FIREFOX_VERSION : DEFAULT_MIN_CHROME_VERSION);
|
|
281
|
+
|
|
236
282
|
const config = {
|
|
237
|
-
devices: [
|
|
238
|
-
operatingSystems: [
|
|
283
|
+
devices: [selectedDevice],
|
|
284
|
+
operatingSystems: [selectedOs],
|
|
239
285
|
locales: ["en-US"],
|
|
240
286
|
browsers: [],
|
|
241
287
|
screen: {},
|
|
242
288
|
};
|
|
243
289
|
|
|
244
290
|
if (browserType === "chromium") {
|
|
245
|
-
config.browsers.push({ name: "chrome", minVersion
|
|
291
|
+
config.browsers.push({ name: "chrome", minVersion });
|
|
246
292
|
} else if (browserType === "firefox") {
|
|
247
|
-
config.browsers.push({ name: "firefox", minVersion
|
|
293
|
+
config.browsers.push({ name: "firefox", minVersion });
|
|
248
294
|
} else if (browserType === "brave") {
|
|
249
|
-
config.browsers.push({ name: "chrome", minVersion
|
|
295
|
+
config.browsers.push({ name: "chrome", minVersion });
|
|
250
296
|
}
|
|
251
297
|
|
|
252
298
|
// Apply screen filter options if provided
|
|
253
|
-
if (
|
|
254
|
-
if (
|
|
255
|
-
if (
|
|
256
|
-
if (
|
|
257
|
-
if (
|
|
299
|
+
if (spoofOptions && typeof spoofOptions === 'object') {
|
|
300
|
+
if (spoofOptions.minWidth) config.screen.minWidth = spoofOptions.minWidth;
|
|
301
|
+
if (spoofOptions.maxWidth) config.screen.maxWidth = spoofOptions.maxWidth;
|
|
302
|
+
if (spoofOptions.minHeight) config.screen.minHeight = spoofOptions.minHeight;
|
|
303
|
+
if (spoofOptions.maxHeight) config.screen.maxHeight = spoofOptions.maxHeight;
|
|
258
304
|
}
|
|
259
305
|
|
|
306
|
+
if (_launchLogs) console.log(`░░░░░ Fingerprint config: OS=${selectedOs}, Device=${selectedDevice}, Browser=${browserType}`);
|
|
307
|
+
|
|
260
308
|
return config;
|
|
261
309
|
}
|
|
262
310
|
|
|
@@ -280,8 +328,11 @@ export async function pwLaunch({
|
|
|
280
328
|
|
|
281
329
|
// Spoof Fingerprint
|
|
282
330
|
// - false: No spoofing
|
|
283
|
-
// - true: Spoof fingerprint with viewport: null
|
|
284
|
-
// -
|
|
331
|
+
// - true: Spoof fingerprint with defaults (detectedOs, auto device, viewport: null)
|
|
332
|
+
// - object: Spoof with overrides:
|
|
333
|
+
// os_type?: string | string[] (e.g. "windows" or ["windows", "macos"]) — random pick if multiple
|
|
334
|
+
// minBrowserVersion?: number — minimum browser version (default: 141)
|
|
335
|
+
// minWidth?, maxWidth?, minHeight?, maxHeight?: screen constraints
|
|
285
336
|
spoof_fingerprint = false,
|
|
286
337
|
|
|
287
338
|
// Browser Specific Grouped Options
|
|
@@ -441,15 +492,13 @@ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, hum
|
|
|
441
492
|
const proxyObj = formatProxy(proxy);
|
|
442
493
|
const tz = timezoneId || undefined;
|
|
443
494
|
|
|
444
|
-
// Determine screen options from spoof_fingerprint if it's an object
|
|
445
|
-
const screenOptions = (spoof_fingerprint && typeof spoof_fingerprint === 'object') ? spoof_fingerprint : null;
|
|
446
|
-
|
|
447
495
|
// ==================================================================
|
|
448
496
|
// BRANCH A: TEMP PROFILE (launch + newInjectedContext)
|
|
449
497
|
// ==================================================================
|
|
450
498
|
if (!isPersistent) {
|
|
451
499
|
try {
|
|
452
|
-
const
|
|
500
|
+
const spoofOptions = (spoof_fingerprint && typeof spoof_fingerprint === 'object') ? spoof_fingerprint : null;
|
|
501
|
+
const fpConfig = getFingerprintConfig("chromium", spoofOptions);
|
|
453
502
|
const fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
|
|
454
503
|
|
|
455
504
|
// Launch standard browser (not persistent context)
|
|
@@ -558,9 +607,6 @@ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_option
|
|
|
558
607
|
const proxyObj = formatProxy(proxy);
|
|
559
608
|
const tz = timezoneId || undefined;
|
|
560
609
|
|
|
561
|
-
// Determine screen options from spoof_fingerprint if it's an object
|
|
562
|
-
const screenOptions = (spoof_fingerprint && typeof spoof_fingerprint === 'object') ? spoof_fingerprint : null;
|
|
563
|
-
|
|
564
610
|
// Firefox specific preferences
|
|
565
611
|
const firefoxUserPrefs = {
|
|
566
612
|
"dom.webdriver.enabled": false,
|
|
@@ -573,7 +619,8 @@ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_option
|
|
|
573
619
|
// ==================================================================
|
|
574
620
|
if (!isPersistent) {
|
|
575
621
|
try {
|
|
576
|
-
const
|
|
622
|
+
const spoofOptions = (spoof_fingerprint && typeof spoof_fingerprint === 'object') ? spoof_fingerprint : null;
|
|
623
|
+
const fpConfig = getFingerprintConfig("firefox", spoofOptions);
|
|
577
624
|
const fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
|
|
578
625
|
|
|
579
626
|
const browser = await firefox.launch({
|
|
@@ -673,13 +720,13 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
|
|
|
673
720
|
}
|
|
674
721
|
}
|
|
675
722
|
|
|
676
|
-
// Determine
|
|
677
|
-
const
|
|
723
|
+
// Determine spoof options from effectiveSpoofFingerprint if it's an object
|
|
724
|
+
const spoofOptions = (effectiveSpoofFingerprint && typeof effectiveSpoofFingerprint === 'object') ? effectiveSpoofFingerprint : null;
|
|
678
725
|
|
|
679
726
|
// Determine if we should use screen viewport or null
|
|
680
727
|
// - spoof_fingerprint = true (boolean) -> spoof with viewport: null
|
|
681
728
|
// - spoof_fingerprint = { minWidth, maxWidth, ... } (object) -> spoof with generated screen viewport
|
|
682
|
-
const useScreenViewport =
|
|
729
|
+
const useScreenViewport = spoofOptions !== null;
|
|
683
730
|
|
|
684
731
|
let fingerprintData;
|
|
685
732
|
const fingerprintFilePath = path.join(activePath, "fingerprint.json");
|
|
@@ -687,8 +734,8 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
|
|
|
687
734
|
if (fs.existsSync(fingerprintFilePath)) {
|
|
688
735
|
fingerprintData = JSON.parse(fs.readFileSync(fingerprintFilePath, "utf-8"));
|
|
689
736
|
} else {
|
|
690
|
-
// Generate fingerprint with
|
|
691
|
-
const fpConfig = getFingerprintConfig("brave",
|
|
737
|
+
// Generate fingerprint with spoof options (null if spoof_fingerprint is just true)
|
|
738
|
+
const fpConfig = getFingerprintConfig("brave", spoofOptions);
|
|
692
739
|
fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
|
|
693
740
|
|
|
694
741
|
if (isPersistent) {
|
|
@@ -937,7 +984,7 @@ async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_opt
|
|
|
937
984
|
canvas_noise = true,
|
|
938
985
|
media_masking = true,
|
|
939
986
|
audio_masking = true,
|
|
940
|
-
|
|
987
|
+
screen_masking = true,
|
|
941
988
|
} = multilogin_options;
|
|
942
989
|
|
|
943
990
|
if (profileId) {
|
|
@@ -949,6 +996,7 @@ async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_opt
|
|
|
949
996
|
canvas_noise,
|
|
950
997
|
media_masking,
|
|
951
998
|
audio_masking,
|
|
999
|
+
screen_masking,
|
|
952
1000
|
humanize_options,
|
|
953
1001
|
});
|
|
954
1002
|
}
|
|
@@ -1030,7 +1078,7 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
|
|
|
1030
1078
|
}
|
|
1031
1079
|
}
|
|
1032
1080
|
|
|
1033
|
-
async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking, humanize_options = null }) {
|
|
1081
|
+
async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking, screen_masking, humanize_options = null }) {
|
|
1034
1082
|
const createUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v3/profile/quick`;
|
|
1035
1083
|
let browser, context, page, profileId;
|
|
1036
1084
|
|
|
@@ -1043,7 +1091,7 @@ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, medi
|
|
|
1043
1091
|
flags: {
|
|
1044
1092
|
audio_masking: audio_masking ? "mask" : "natural",
|
|
1045
1093
|
media_devices_masking: media_masking ? "mask" : "natural",
|
|
1046
|
-
screen_masking: "natural",
|
|
1094
|
+
screen_masking: screen_masking ? "mask" : "natural",
|
|
1047
1095
|
canvas_noise: canvas_noise ? "mask" : "natural",
|
|
1048
1096
|
proxy_masking: proxy ? "custom" : "disabled",
|
|
1049
1097
|
},
|
|
@@ -32,6 +32,9 @@ export interface PwRouteOptions {
|
|
|
32
32
|
/** Enable caching for requests */
|
|
33
33
|
useCache?: boolean;
|
|
34
34
|
|
|
35
|
+
/** Strip cookies/auth from outgoing requests and sanitize CORS/CSP/set-cookie from responses (default: false) */
|
|
36
|
+
stripGotHeaders?: boolean;
|
|
37
|
+
|
|
35
38
|
/**
|
|
36
39
|
* Proxy for custom fetch requests (only used when useGot is true).
|
|
37
40
|
* String: "http://host:port", "socks5://user:pass@host:port"
|
|
@@ -71,6 +71,34 @@ function createProxyAgent(proxyUrl) {
|
|
|
71
71
|
return new HttpsProxyAgent(proxyUrl);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Strips sensitive headers from outgoing request headers.
|
|
76
|
+
* Removes cookie and authorization to prevent session/IP correlation.
|
|
77
|
+
*/
|
|
78
|
+
function sanitizeRequestHeaders(headers) {
|
|
79
|
+
const cleaned = { ...headers };
|
|
80
|
+
delete cleaned["cookie"];
|
|
81
|
+
delete cleaned["authorization"];
|
|
82
|
+
return cleaned;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sanitizes response headers for safe caching across origins.
|
|
87
|
+
* - Replaces access-control-allow-origin with * (if present)
|
|
88
|
+
* - Removes access-control-allow-credentials, set-cookie, CSP, HSTS
|
|
89
|
+
*/
|
|
90
|
+
function sanitizeResponseHeaders(headers) {
|
|
91
|
+
const cleaned = { ...headers };
|
|
92
|
+
if (cleaned["access-control-allow-origin"]) {
|
|
93
|
+
cleaned["access-control-allow-origin"] = "*";
|
|
94
|
+
}
|
|
95
|
+
delete cleaned["access-control-allow-credentials"];
|
|
96
|
+
delete cleaned["set-cookie"];
|
|
97
|
+
delete cleaned["content-security-policy"];
|
|
98
|
+
delete cleaned["strict-transport-security"];
|
|
99
|
+
return cleaned;
|
|
100
|
+
}
|
|
101
|
+
|
|
74
102
|
/**
|
|
75
103
|
* Function to fetch resources using Superagent library with optional caching.
|
|
76
104
|
* This mimics the browser's request but handles it in Node.js to allow caching or header manipulation.
|
|
@@ -81,9 +109,10 @@ function createProxyAgent(proxyUrl) {
|
|
|
81
109
|
* @param {boolean} useFullUrl - Whether to use the full URL as cache key or just origin+path
|
|
82
110
|
* @param {string|false} logger - Log level: "info" (success+error), "error" (errors only), false (no logs)
|
|
83
111
|
* @param {Object|null} proxyAgent - Proxy agent to use for the request
|
|
112
|
+
* @param {boolean} stripHeaders - Whether to sanitize request/response headers
|
|
84
113
|
* @returns {Promise<Object>} - The response object containing status, headers, and body
|
|
85
114
|
*/
|
|
86
|
-
async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger, proxyAgent) {
|
|
115
|
+
async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger, proxyAgent, stripHeaders) {
|
|
87
116
|
// Determine the cache key based on configuration
|
|
88
117
|
let mainUrl = new URL(url).origin + new URL(url).pathname;
|
|
89
118
|
if (useFullUrl) {
|
|
@@ -102,9 +131,12 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
|
|
|
102
131
|
}
|
|
103
132
|
|
|
104
133
|
try {
|
|
134
|
+
// Sanitize outgoing request headers if stripHeaders is enabled
|
|
135
|
+
const finalHeaders = stripHeaders ? sanitizeRequestHeaders(requestHeaders) : requestHeaders;
|
|
136
|
+
|
|
105
137
|
// Fetch the resource using superagent
|
|
106
138
|
// buffer(true) ensures we get the raw binary data (essential for images/fonts)
|
|
107
|
-
let request = superagent(method, url).set(
|
|
139
|
+
let request = superagent(method, url).set(finalHeaders).buffer(true);
|
|
108
140
|
|
|
109
141
|
// Apply proxy agent if provided
|
|
110
142
|
if (proxyAgent) {
|
|
@@ -116,11 +148,14 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
|
|
|
116
148
|
// Determine the correct body type (Buffer for binary, text for others)
|
|
117
149
|
const responseBody = response.body instanceof Buffer ? response.body : response.text;
|
|
118
150
|
|
|
151
|
+
// Sanitize response headers if stripHeaders is enabled
|
|
152
|
+
const finalResponseHeaders = stripHeaders ? sanitizeResponseHeaders(response.headers) : response.headers;
|
|
153
|
+
|
|
119
154
|
// Save to cache only when caching is enabled
|
|
120
155
|
if (useCache) {
|
|
121
156
|
globalCache.set(mainUrl, {
|
|
122
157
|
status: response.status,
|
|
123
|
-
headers:
|
|
158
|
+
headers: finalResponseHeaders,
|
|
124
159
|
body: responseBody,
|
|
125
160
|
});
|
|
126
161
|
if (logger === "info") console.log(`Success (cached${viaProxy}): ${mainUrl}`);
|
|
@@ -130,7 +165,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
|
|
|
130
165
|
|
|
131
166
|
return {
|
|
132
167
|
status: response.status,
|
|
133
|
-
headers:
|
|
168
|
+
headers: finalResponseHeaders,
|
|
134
169
|
body: responseBody,
|
|
135
170
|
};
|
|
136
171
|
} catch (error) {
|
|
@@ -165,6 +200,7 @@ export async function pwRoute({
|
|
|
165
200
|
useGot = false,
|
|
166
201
|
useFullUrl = true,
|
|
167
202
|
useCache = true,
|
|
203
|
+
stripGotHeaders = true,
|
|
168
204
|
proxy = null,
|
|
169
205
|
m4w_send_on_post = null,
|
|
170
206
|
m4w_send_on_message = null,
|
|
@@ -357,7 +393,8 @@ export async function pwRoute({
|
|
|
357
393
|
requestMethod,
|
|
358
394
|
useFullUrl,
|
|
359
395
|
logger,
|
|
360
|
-
proxyAgent
|
|
396
|
+
proxyAgent,
|
|
397
|
+
stripGotHeaders
|
|
361
398
|
);
|
|
362
399
|
|
|
363
400
|
if (response) {
|
|
@@ -60,6 +60,9 @@ export interface ProxyServerOptions {
|
|
|
60
60
|
*/
|
|
61
61
|
ip2LocationKey?: string;
|
|
62
62
|
|
|
63
|
+
/** Retry delay in milliseconds between IP lookup attempts (default: 1500) */
|
|
64
|
+
retryDelayMs?: number;
|
|
65
|
+
|
|
63
66
|
/** Enable verbose logging (default: false) */
|
|
64
67
|
debug?: boolean;
|
|
65
68
|
/** Track proxy usage statistics (default: true) */
|
|
@@ -129,3 +132,11 @@ export function startProxyServer(options: ProxyServerOptions): Promise<ProxyServ
|
|
|
129
132
|
* @returns A promise resolving to GeoDetails (including timezoneId) or null if unreachable.
|
|
130
133
|
*/
|
|
131
134
|
export function fetchPublicIP(proxyUrl: string | null): Promise<GeoDetails | null>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Fetches proxy details (IP, country, city, timezone) with retries.
|
|
138
|
+
* @param proxyUrl The proxy URL string. Pass `null` for local fallback.
|
|
139
|
+
* @param apiKey Optional IP2Location API key.
|
|
140
|
+
* @param retryDelayMs Delay between retries in ms (default: 1500).
|
|
141
|
+
*/
|
|
142
|
+
export function fetchProxyDetails(proxyUrl: string | null, apiKey?: string | null, retryDelayMs?: number): Promise<GeoDetails | null>;
|
|
@@ -2,6 +2,7 @@ import ProxyChain from "proxy-chain";
|
|
|
2
2
|
import net from "net";
|
|
3
3
|
import https from "https";
|
|
4
4
|
import http from "http";
|
|
5
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
5
6
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
6
7
|
import { SocksProxyAgent } from "socks-proxy-agent";
|
|
7
8
|
|
|
@@ -100,7 +101,7 @@ export async function fetchPublicIP(proxyUrl) {
|
|
|
100
101
|
* 1. If proxyUrl is provided -> Fetch Real Data (IP + Timezone).
|
|
101
102
|
* 2. If proxyUrl is NULL/Undefined -> Return "Local" Mock immediately.
|
|
102
103
|
*/
|
|
103
|
-
export async function fetchProxyDetails(proxyUrl, apiKey = null) {
|
|
104
|
+
export async function fetchProxyDetails(proxyUrl, apiKey = null, retryDelayMs = 1500) {
|
|
104
105
|
// --- AUTOMATIC LOCAL FALLBACK ---
|
|
105
106
|
if (!proxyUrl) {
|
|
106
107
|
return {
|
|
@@ -111,56 +112,65 @@ export async function fetchProxyDetails(proxyUrl, apiKey = null) {
|
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
// --- EXTERNAL LOOKUP
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const jsonStr = await nativeGet(apiUrl, agent, 6000);
|
|
135
|
-
const data = JSON.parse(jsonStr);
|
|
136
|
-
|
|
137
|
-
if (provider === "IP-API") {
|
|
138
|
-
if (data.status === "success") {
|
|
139
|
-
return {
|
|
140
|
-
ip: data.query,
|
|
141
|
-
country: data.country,
|
|
142
|
-
city: data.city,
|
|
143
|
-
timezoneId: data.timezone || null,
|
|
144
|
-
};
|
|
115
|
+
// --- EXTERNAL LOOKUP with retry (10 attempts) ---
|
|
116
|
+
const providers = ["IP-API", "IP2Location"];
|
|
117
|
+
const MAX_RETRIES = 10;
|
|
118
|
+
|
|
119
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
120
|
+
for (const provider of providers) {
|
|
121
|
+
try {
|
|
122
|
+
const isSocks = proxyUrl.startsWith("socks");
|
|
123
|
+
const agent = isSocks ? new SocksProxyAgent(proxyUrl) : new HttpsProxyAgent(proxyUrl);
|
|
124
|
+
|
|
125
|
+
let apiUrl;
|
|
126
|
+
|
|
127
|
+
if (provider === "IP-API") {
|
|
128
|
+
apiUrl = "http://ip-api.com/json/?fields=status,message,country,city,timezone,query";
|
|
129
|
+
} else {
|
|
130
|
+
const baseUrl = "https://api.ip2location.io/";
|
|
131
|
+
const fields = "ip,country_name,city_name,time_zone,time_zone_olson";
|
|
132
|
+
apiUrl = apiKey
|
|
133
|
+
? `${baseUrl}?key=${apiKey}&format=json&fields=${fields}`
|
|
134
|
+
: `${baseUrl}?format=json&fields=${fields}`;
|
|
145
135
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
136
|
+
|
|
137
|
+
const jsonStr = await nativeGet(apiUrl, agent, 6000);
|
|
138
|
+
const data = JSON.parse(jsonStr);
|
|
139
|
+
|
|
140
|
+
if (provider === "IP-API") {
|
|
141
|
+
if (data.status === "success") {
|
|
142
|
+
return {
|
|
143
|
+
ip: data.query,
|
|
144
|
+
country: data.country,
|
|
145
|
+
city: data.city,
|
|
146
|
+
timezoneId: data.timezone || null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
} else if (provider === "IP2Location") {
|
|
150
|
+
if (data && data.ip) {
|
|
151
|
+
const tz = data.time_zone_olson || null;
|
|
152
|
+
const isValidId = tz && !tz.startsWith("+") && !tz.startsWith("-");
|
|
153
|
+
return {
|
|
154
|
+
ip: data.ip,
|
|
155
|
+
country: data.country_name,
|
|
156
|
+
city: data.city_name,
|
|
157
|
+
timezoneId: isValidId ? tz : null,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
156
160
|
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
// Silently fail to next provider
|
|
157
163
|
}
|
|
158
|
-
}
|
|
159
|
-
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Both providers failed, wait 1s before retrying
|
|
167
|
+
if (attempt < MAX_RETRIES) {
|
|
168
|
+
console.warn(`░░ Proxy IP lookup failed (attempt ${attempt}/${MAX_RETRIES}), retrying in ${retryDelayMs}ms...`);
|
|
169
|
+
await sleep(retryDelayMs);
|
|
160
170
|
}
|
|
161
171
|
}
|
|
162
172
|
|
|
163
|
-
//
|
|
173
|
+
// All retries exhausted:
|
|
164
174
|
return null;
|
|
165
175
|
}
|
|
166
176
|
|
|
@@ -176,6 +186,7 @@ export async function startProxyServer({
|
|
|
176
186
|
PROXY_2_HOSTS = [],
|
|
177
187
|
NO_PROXY_HOSTS = [],
|
|
178
188
|
ip2LocationKey = null,
|
|
189
|
+
retryDelayMs = 1500,
|
|
179
190
|
debug = false,
|
|
180
191
|
proxy_stats = true,
|
|
181
192
|
host_stats = true,
|
|
@@ -212,9 +223,9 @@ export async function startProxyServer({
|
|
|
212
223
|
// 4. Fetch Details (Simplified Logic)
|
|
213
224
|
// We pass the URL (or null). The function handles the "Local" logic.
|
|
214
225
|
const [defaultDetails, p1Details, p2Details] = await Promise.all([
|
|
215
|
-
fetchProxyDetails(upstreamProxies.default, ip2LocationKey),
|
|
216
|
-
fetchProxyDetails(upstreamProxies.p1, ip2LocationKey),
|
|
217
|
-
fetchProxyDetails(upstreamProxies.p2, ip2LocationKey),
|
|
226
|
+
fetchProxyDetails(upstreamProxies.default, ip2LocationKey, retryDelayMs),
|
|
227
|
+
fetchProxyDetails(upstreamProxies.p1, ip2LocationKey, retryDelayMs),
|
|
228
|
+
fetchProxyDetails(upstreamProxies.p2, ip2LocationKey, retryDelayMs),
|
|
218
229
|
]);
|
|
219
230
|
|
|
220
231
|
// Return null if any configured proxy is unreachable
|
|
@@ -240,9 +251,6 @@ export async function startProxyServer({
|
|
|
240
251
|
const connectionMap = {}; // Maps connectionId -> { type: "..." }
|
|
241
252
|
let serverRunning = false;
|
|
242
253
|
|
|
243
|
-
// Track all open sockets so we can force-destroy them on close
|
|
244
|
-
const activeSockets = new Set();
|
|
245
|
-
|
|
246
254
|
// 6. Server
|
|
247
255
|
const server = new ProxyChain.Server({
|
|
248
256
|
port: selectedPort,
|
|
@@ -331,23 +339,6 @@ export async function startProxyServer({
|
|
|
331
339
|
try {
|
|
332
340
|
await server.listen();
|
|
333
341
|
serverRunning = true;
|
|
334
|
-
|
|
335
|
-
// Track sockets so closeServer() can destroy them all
|
|
336
|
-
server.server.on('connection', (socket) => {
|
|
337
|
-
activeSockets.add(socket);
|
|
338
|
-
socket.once('close', () => activeSockets.delete(socket));
|
|
339
|
-
|
|
340
|
-
// Intercept pipe to catch the upstream target sockets created by proxy-chain
|
|
341
|
-
const originalPipe = socket.pipe;
|
|
342
|
-
socket.pipe = function(destination, options) {
|
|
343
|
-
if (destination && typeof destination.destroy === 'function') {
|
|
344
|
-
activeSockets.add(destination);
|
|
345
|
-
destination.once('close', () => activeSockets.delete(destination));
|
|
346
|
-
}
|
|
347
|
-
return originalPipe.apply(this, arguments);
|
|
348
|
-
};
|
|
349
|
-
});
|
|
350
|
-
|
|
351
342
|
console.log(`░░ Local Proxy Started: http://127.0.0.1:${selectedPort}`);
|
|
352
343
|
} catch (err) {
|
|
353
344
|
console.error("░░ Failed to start proxy server:", err);
|
|
@@ -415,16 +406,11 @@ export async function startProxyServer({
|
|
|
415
406
|
isServerRunning: () => serverRunning,
|
|
416
407
|
|
|
417
408
|
closeServer: async () => {
|
|
409
|
+
// Close the server — triggers connectionClosed events which accumulate byte stats
|
|
418
410
|
await server.close(true);
|
|
419
411
|
serverRunning = false;
|
|
420
412
|
|
|
421
|
-
//
|
|
422
|
-
for (const socket of activeSockets) {
|
|
423
|
-
socket.destroy();
|
|
424
|
-
}
|
|
425
|
-
activeSockets.clear();
|
|
426
|
-
|
|
427
|
-
// Auto console.log stats on close
|
|
413
|
+
// Log stats AFTER close so all connectionClosed events have fired
|
|
428
414
|
if (proxy_stats) {
|
|
429
415
|
console.log("░░ Proxy Stats:", getProxyStatsFormatted());
|
|
430
416
|
}
|
|
@@ -33,6 +33,14 @@ export interface PpMultiloginOptions {
|
|
|
33
33
|
* Default: true
|
|
34
34
|
*/
|
|
35
35
|
audio_masking?: boolean;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Multilogin flag: Screen resolution masking.
|
|
39
|
+
* true = randomize screen resolution ("mask")
|
|
40
|
+
* false = use real screen resolution ("natural")
|
|
41
|
+
* Default: true
|
|
42
|
+
*/
|
|
43
|
+
screen_masking?: boolean;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
/**
|
|
@@ -116,15 +124,47 @@ export interface PpLaunchOptions {
|
|
|
116
124
|
* Configuration for browser fingerprint spoofing (per-page via fingerprint-injector).
|
|
117
125
|
*
|
|
118
126
|
* - `false`: No fingerprint spoofing (default)
|
|
119
|
-
* - `true`: Spoof fingerprint with default
|
|
120
|
-
* - `
|
|
127
|
+
* - `true`: Spoof fingerprint with default OS (detected) and auto device
|
|
128
|
+
* - `object`: Spoof fingerprint with optional overrides
|
|
129
|
+
*
|
|
130
|
+
* Device type is auto-determined from OS:
|
|
131
|
+
* - `"windows"`, `"macos"`, `"linux"` → desktop
|
|
132
|
+
* - `"android"`, `"ios"` → mobile
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* // Spoof with defaults
|
|
136
|
+
* spoof_fingerprint: true
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* // Spoof as Windows
|
|
140
|
+
* spoof_fingerprint: { os_type: "windows" }
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* // Random OS per launch (desktop + mobile mixed)
|
|
144
|
+
* spoof_fingerprint: { os_type: ["windows", "android"] }
|
|
121
145
|
*
|
|
122
146
|
* Default: false
|
|
123
147
|
*/
|
|
124
148
|
spoof_fingerprint?: boolean | {
|
|
149
|
+
/**
|
|
150
|
+
* Override the OS for fingerprint generation.
|
|
151
|
+
* Can be a single value or array (random pick per launch).
|
|
152
|
+
* Device type is auto-determined: windows/macos/linux → desktop, android/ios → mobile
|
|
153
|
+
* Default: detected OS of the machine
|
|
154
|
+
*/
|
|
155
|
+
os_type?: ("windows" | "macos" | "linux" | "android" | "ios") | ("windows" | "macos" | "linux" | "android" | "ios")[];
|
|
156
|
+
/**
|
|
157
|
+
* Minimum Chrome version for fingerprint generation.
|
|
158
|
+
* Default: 141
|
|
159
|
+
*/
|
|
160
|
+
minBrowserVersion?: number;
|
|
161
|
+
/** Minimum screen width constraint */
|
|
125
162
|
minWidth?: number;
|
|
163
|
+
/** Maximum screen width constraint */
|
|
126
164
|
maxWidth?: number;
|
|
165
|
+
/** Minimum screen height constraint */
|
|
127
166
|
minHeight?: number;
|
|
167
|
+
/** Maximum screen height constraint */
|
|
128
168
|
maxHeight?: number;
|
|
129
169
|
};
|
|
130
170
|
|
|
@@ -41,26 +41,73 @@ let _cleanupLogs = false;
|
|
|
41
41
|
// Detect OS for fingerprint generation
|
|
42
42
|
const detectedOs = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux";
|
|
43
43
|
|
|
44
|
+
// Default minimum browser version for fingerprint generation
|
|
45
|
+
const DEFAULT_MIN_CHROME_VERSION = 141;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Maps OS to its device type.
|
|
49
|
+
*/
|
|
50
|
+
const OS_TO_DEVICE = {
|
|
51
|
+
windows: "desktop",
|
|
52
|
+
macos: "desktop",
|
|
53
|
+
linux: "desktop",
|
|
54
|
+
android: "mobile",
|
|
55
|
+
ios: "mobile",
|
|
56
|
+
};
|
|
57
|
+
|
|
44
58
|
/**
|
|
45
59
|
* Helper to generate consistent fingerprint options for Puppeteer.
|
|
46
|
-
* @param {object|null}
|
|
60
|
+
* @param {object|null} spoofOptions - Options from spoof_fingerprint (null if boolean true)
|
|
61
|
+
* Supported properties:
|
|
62
|
+
* - os_type: string | string[] — e.g. "windows" or ["windows", "macos"]. Default: detectedOs
|
|
63
|
+
* - minBrowserVersion: number — minimum Chrome version for fingerprint. Default: 141
|
|
64
|
+
* - minWidth, maxWidth, minHeight, maxHeight: screen constraints
|
|
47
65
|
*/
|
|
48
|
-
function getFingerprintConfig(
|
|
66
|
+
function getFingerprintConfig(spoofOptions = null) {
|
|
67
|
+
let selectedOs = detectedOs;
|
|
68
|
+
let selectedDevice = OS_TO_DEVICE[detectedOs] || "desktop";
|
|
69
|
+
|
|
70
|
+
if (spoofOptions && typeof spoofOptions === 'object' && spoofOptions.os_type) {
|
|
71
|
+
const osList = Array.isArray(spoofOptions.os_type) ? spoofOptions.os_type : [spoofOptions.os_type];
|
|
72
|
+
|
|
73
|
+
if (osList.length === 0) {
|
|
74
|
+
throw new Error(`░░░░░ [Fingerprint] os_type array is empty.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate all values
|
|
78
|
+
for (const os of osList) {
|
|
79
|
+
if (!OS_TO_DEVICE[os]) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`░░░░░ [Fingerprint] Unknown os_type: "${os}".\n` +
|
|
82
|
+
` Supported: "windows", "macos", "linux" (desktop), "android", "ios" (mobile)`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Random pick one
|
|
88
|
+
selectedOs = osList[Math.floor(Math.random() * osList.length)];
|
|
89
|
+
selectedDevice = OS_TO_DEVICE[selectedOs];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const minVersion = (spoofOptions && typeof spoofOptions === 'object' && spoofOptions.minBrowserVersion) || DEFAULT_MIN_CHROME_VERSION;
|
|
93
|
+
|
|
49
94
|
const config = {
|
|
50
|
-
devices: [
|
|
51
|
-
operatingSystems: [
|
|
95
|
+
devices: [selectedDevice],
|
|
96
|
+
operatingSystems: [selectedOs],
|
|
52
97
|
locales: ["en-US"],
|
|
53
|
-
browsers: [{ name: "chrome", minVersion
|
|
98
|
+
browsers: [{ name: "chrome", minVersion }],
|
|
54
99
|
screen: {},
|
|
55
100
|
};
|
|
56
101
|
|
|
57
|
-
if (
|
|
58
|
-
if (
|
|
59
|
-
if (
|
|
60
|
-
if (
|
|
61
|
-
if (
|
|
102
|
+
if (spoofOptions && typeof spoofOptions === 'object') {
|
|
103
|
+
if (spoofOptions.minWidth) config.screen.minWidth = spoofOptions.minWidth;
|
|
104
|
+
if (spoofOptions.maxWidth) config.screen.maxWidth = spoofOptions.maxWidth;
|
|
105
|
+
if (spoofOptions.minHeight) config.screen.minHeight = spoofOptions.minHeight;
|
|
106
|
+
if (spoofOptions.maxHeight) config.screen.maxHeight = spoofOptions.maxHeight;
|
|
62
107
|
}
|
|
63
108
|
|
|
109
|
+
if (_launchLogs) console.log(`░░░░░ Fingerprint config: OS=${selectedOs}, Device=${selectedDevice}`);
|
|
110
|
+
|
|
64
111
|
return config;
|
|
65
112
|
}
|
|
66
113
|
|
|
@@ -230,8 +277,11 @@ export async function ppLaunch({
|
|
|
230
277
|
|
|
231
278
|
// Spoof Fingerprint
|
|
232
279
|
// - false: No spoofing
|
|
233
|
-
// - true: Spoof with
|
|
234
|
-
// -
|
|
280
|
+
// - true: Spoof with defaults (detectedOs, auto device)
|
|
281
|
+
// - object: Spoof with overrides:
|
|
282
|
+
// os_type?: string | string[] (e.g. "windows" or ["windows", "macos"]) — random pick if multiple
|
|
283
|
+
// minBrowserVersion?: number — minimum Chrome version (default: 141)
|
|
284
|
+
// minWidth?, maxWidth?, minHeight?, maxHeight?: screen constraints
|
|
235
285
|
spoof_fingerprint = false,
|
|
236
286
|
|
|
237
287
|
// Multilogin
|
|
@@ -542,8 +592,8 @@ async function spawnAndConnect({ binaryPath, profilePath, isPersistent, proxy, e
|
|
|
542
592
|
if (_launchLogs) console.log(`░░░░░ Loaded saved fingerprint for ${browserLabel}`);
|
|
543
593
|
} else {
|
|
544
594
|
// Generate new fingerprint
|
|
545
|
-
const
|
|
546
|
-
const fpConfig = getFingerprintConfig(
|
|
595
|
+
const spoofOptions = (typeof spoof_fingerprint === 'object') ? spoof_fingerprint : null;
|
|
596
|
+
const fpConfig = getFingerprintConfig(spoofOptions);
|
|
547
597
|
fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
|
|
548
598
|
|
|
549
599
|
// Save for persistent profiles
|
|
@@ -593,6 +643,7 @@ async function multiloginLauncher({ proxy, multilogin_options = {} }) {
|
|
|
593
643
|
canvas_noise = true,
|
|
594
644
|
media_masking = true,
|
|
595
645
|
audio_masking = true,
|
|
646
|
+
screen_masking = true,
|
|
596
647
|
} = multilogin_options;
|
|
597
648
|
|
|
598
649
|
if (profileId) {
|
|
@@ -604,6 +655,7 @@ async function multiloginLauncher({ proxy, multilogin_options = {} }) {
|
|
|
604
655
|
canvas_noise,
|
|
605
656
|
media_masking,
|
|
606
657
|
audio_masking,
|
|
658
|
+
screen_masking,
|
|
607
659
|
});
|
|
608
660
|
}
|
|
609
661
|
}
|
|
@@ -683,7 +735,7 @@ async function launchExistingMultiloginProfile(profileId) {
|
|
|
683
735
|
}
|
|
684
736
|
}
|
|
685
737
|
|
|
686
|
-
async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking }) {
|
|
738
|
+
async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking, screen_masking }) {
|
|
687
739
|
const createUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v3/profile/quick`;
|
|
688
740
|
let browser, profileId;
|
|
689
741
|
|
|
@@ -696,7 +748,7 @@ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, medi
|
|
|
696
748
|
flags: {
|
|
697
749
|
audio_masking: audio_masking ? "mask" : "natural",
|
|
698
750
|
media_devices_masking: media_masking ? "mask" : "natural",
|
|
699
|
-
screen_masking: "natural",
|
|
751
|
+
screen_masking: screen_masking ? "mask" : "natural",
|
|
700
752
|
canvas_noise: canvas_noise ? "mask" : "natural",
|
|
701
753
|
proxy_masking: proxy ? "custom" : "disabled",
|
|
702
754
|
},
|
|
@@ -29,6 +29,9 @@ export interface PpRouteOptions {
|
|
|
29
29
|
/** Enable caching for requests */
|
|
30
30
|
useCache?: boolean;
|
|
31
31
|
|
|
32
|
+
/** Strip cookies/auth from outgoing requests and sanitize CORS/CSP/set-cookie from responses (default: false) */
|
|
33
|
+
stripGotHeaders?: boolean;
|
|
34
|
+
|
|
32
35
|
/**
|
|
33
36
|
* Proxy for custom fetch requests (only used when useGot is true).
|
|
34
37
|
* String: "http://host:port", "socks5://user:pass@host:port"
|
|
@@ -71,6 +71,34 @@ function createProxyAgent(proxyUrl) {
|
|
|
71
71
|
return new HttpsProxyAgent(proxyUrl);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Strips sensitive headers from outgoing request headers.
|
|
76
|
+
* Removes cookie and authorization to prevent session/IP correlation.
|
|
77
|
+
*/
|
|
78
|
+
function sanitizeRequestHeaders(headers) {
|
|
79
|
+
const cleaned = { ...headers };
|
|
80
|
+
delete cleaned["cookie"];
|
|
81
|
+
delete cleaned["authorization"];
|
|
82
|
+
return cleaned;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sanitizes response headers for safe caching across origins.
|
|
87
|
+
* - Replaces access-control-allow-origin with * (if present)
|
|
88
|
+
* - Removes access-control-allow-credentials, set-cookie, CSP, HSTS
|
|
89
|
+
*/
|
|
90
|
+
function sanitizeResponseHeaders(headers) {
|
|
91
|
+
const cleaned = { ...headers };
|
|
92
|
+
if (cleaned["access-control-allow-origin"]) {
|
|
93
|
+
cleaned["access-control-allow-origin"] = "*";
|
|
94
|
+
}
|
|
95
|
+
delete cleaned["access-control-allow-credentials"];
|
|
96
|
+
delete cleaned["set-cookie"];
|
|
97
|
+
delete cleaned["content-security-policy"];
|
|
98
|
+
delete cleaned["strict-transport-security"];
|
|
99
|
+
return cleaned;
|
|
100
|
+
}
|
|
101
|
+
|
|
74
102
|
/**
|
|
75
103
|
* Function to fetch resources using Superagent library with optional caching.
|
|
76
104
|
* This mimics the browser's request but handles it in Node.js to allow caching or header manipulation.
|
|
@@ -81,9 +109,10 @@ function createProxyAgent(proxyUrl) {
|
|
|
81
109
|
* @param {boolean} useFullUrl - Whether to use the full URL as cache key or just origin+path
|
|
82
110
|
* @param {string|false} logger - Log level: "info" (success+error), "error" (errors only), false (no logs)
|
|
83
111
|
* @param {Object|null} proxyAgent - Proxy agent to use for the request
|
|
112
|
+
* @param {boolean} stripHeaders - Whether to sanitize request/response headers
|
|
84
113
|
* @returns {Promise<Object>} - The response object containing status, headers, and body
|
|
85
114
|
*/
|
|
86
|
-
async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger, proxyAgent) {
|
|
115
|
+
async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger, proxyAgent, stripHeaders) {
|
|
87
116
|
// Determine the cache key based on configuration
|
|
88
117
|
let mainUrl = new URL(url).origin + new URL(url).pathname;
|
|
89
118
|
if (useFullUrl) {
|
|
@@ -102,9 +131,12 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
|
|
|
102
131
|
}
|
|
103
132
|
|
|
104
133
|
try {
|
|
134
|
+
// Sanitize outgoing request headers if stripHeaders is enabled
|
|
135
|
+
const finalHeaders = stripHeaders ? sanitizeRequestHeaders(requestHeaders) : requestHeaders;
|
|
136
|
+
|
|
105
137
|
// Fetch the resource using superagent
|
|
106
138
|
// buffer(true) ensures we get the raw binary data (essential for images/fonts)
|
|
107
|
-
let request = superagent(method, url).set(
|
|
139
|
+
let request = superagent(method, url).set(finalHeaders).buffer(true);
|
|
108
140
|
|
|
109
141
|
// Apply proxy agent if provided
|
|
110
142
|
if (proxyAgent) {
|
|
@@ -116,11 +148,14 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
|
|
|
116
148
|
// Determine the correct body type (Buffer for binary, text for others)
|
|
117
149
|
const responseBody = response.body instanceof Buffer ? response.body : response.text;
|
|
118
150
|
|
|
151
|
+
// Sanitize response headers if stripHeaders is enabled
|
|
152
|
+
const finalResponseHeaders = stripHeaders ? sanitizeResponseHeaders(response.headers) : response.headers;
|
|
153
|
+
|
|
119
154
|
// Save to cache only when caching is enabled
|
|
120
155
|
if (useCache) {
|
|
121
156
|
globalCache.set(mainUrl, {
|
|
122
157
|
status: response.status,
|
|
123
|
-
headers:
|
|
158
|
+
headers: finalResponseHeaders,
|
|
124
159
|
body: responseBody,
|
|
125
160
|
});
|
|
126
161
|
if (logger === "info") console.log(`Success (cached${viaProxy}): ${mainUrl}`);
|
|
@@ -130,7 +165,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
|
|
|
130
165
|
|
|
131
166
|
return {
|
|
132
167
|
status: response.status,
|
|
133
|
-
headers:
|
|
168
|
+
headers: finalResponseHeaders,
|
|
134
169
|
body: responseBody,
|
|
135
170
|
};
|
|
136
171
|
} catch (error) {
|
|
@@ -163,6 +198,7 @@ export async function ppRoute({
|
|
|
163
198
|
useGot = false,
|
|
164
199
|
useFullUrl = true,
|
|
165
200
|
useCache = true,
|
|
201
|
+
stripGotHeaders = true,
|
|
166
202
|
proxy = null,
|
|
167
203
|
m4w_send_on_post = null,
|
|
168
204
|
m4w_send_on_message = null,
|
|
@@ -360,7 +396,8 @@ export async function ppRoute({
|
|
|
360
396
|
requestMethod,
|
|
361
397
|
useFullUrl,
|
|
362
398
|
logger,
|
|
363
|
-
proxyAgent
|
|
399
|
+
proxyAgent,
|
|
400
|
+
stripGotHeaders
|
|
364
401
|
);
|
|
365
402
|
|
|
366
403
|
if (response) {
|