arn-browser 0.1.18 → 0.1.20
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/playwright/pwLaunch.d.ts +37 -8
- package/src/utility/playwright/pwLaunch.js +78 -30
- package/src/utility/playwright/routes/pwRoute.d.ts +6 -0
- package/src/utility/playwright/routes/pwRoute.js +59 -5
- package/src/utility/proxy-utility/proxy-chain.d.ts +11 -0
- package/src/utility/proxy-utility/proxy-chain.js +95 -94
- 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 +6 -0
- package/src/utility/puppeteer/routes/ppRoute.js +59 -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.20",
|
|
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",
|
|
@@ -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,12 @@ 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: true) */
|
|
36
|
+
stripGotHeaders?: boolean;
|
|
37
|
+
|
|
38
|
+
/** Log stripped headers to console for debugging (default: false) */
|
|
39
|
+
stripGotLogger?: boolean;
|
|
40
|
+
|
|
35
41
|
/**
|
|
36
42
|
* Proxy for custom fetch requests (only used when useGot is true).
|
|
37
43
|
* String: "http://host:port", "socks5://user:pass@host:port"
|
|
@@ -71,6 +71,48 @@ 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, logger, url) {
|
|
79
|
+
const cleaned = { ...headers };
|
|
80
|
+
const stripped = [];
|
|
81
|
+
// Remove known auth headers
|
|
82
|
+
if (cleaned["cookie"]) { stripped.push("cookie"); delete cleaned["cookie"]; }
|
|
83
|
+
if (cleaned["authorization"]) { stripped.push("authorization"); delete cleaned["authorization"]; }
|
|
84
|
+
// Remove any header containing auth/token/csrf keywords
|
|
85
|
+
for (const key of Object.keys(cleaned)) {
|
|
86
|
+
const lower = key.toLowerCase();
|
|
87
|
+
if (lower.includes("token") || lower.includes("csrf") || lower.includes("auth")) {
|
|
88
|
+
stripped.push(key);
|
|
89
|
+
delete cleaned[key];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (logger && stripped.length) console.log(`[stripGot] Request stripped: [${stripped.join(", ")}] → ${url}`);
|
|
93
|
+
return cleaned;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Sanitizes response headers for safe caching across origins.
|
|
98
|
+
* - Replaces access-control-allow-origin with * (if present)
|
|
99
|
+
* - Removes access-control-allow-credentials, set-cookie, CSP, HSTS
|
|
100
|
+
*/
|
|
101
|
+
function sanitizeResponseHeaders(headers, logger, url) {
|
|
102
|
+
const cleaned = { ...headers };
|
|
103
|
+
const changes = [];
|
|
104
|
+
if (cleaned["access-control-allow-origin"]) {
|
|
105
|
+
changes.push(`access-control-allow-origin: ${cleaned["access-control-allow-origin"]} → *`);
|
|
106
|
+
cleaned["access-control-allow-origin"] = "*";
|
|
107
|
+
}
|
|
108
|
+
if (cleaned["access-control-allow-credentials"]) { changes.push("access-control-allow-credentials"); delete cleaned["access-control-allow-credentials"]; }
|
|
109
|
+
if (cleaned["set-cookie"]) { changes.push("set-cookie"); delete cleaned["set-cookie"]; }
|
|
110
|
+
if (cleaned["content-security-policy"]) { changes.push("content-security-policy"); delete cleaned["content-security-policy"]; }
|
|
111
|
+
if (cleaned["strict-transport-security"]) { changes.push("strict-transport-security"); delete cleaned["strict-transport-security"]; }
|
|
112
|
+
if (logger && changes.length) console.log(`[stripGot] Response stripped: [${changes.join(", ")}] → ${url}`);
|
|
113
|
+
return cleaned;
|
|
114
|
+
}
|
|
115
|
+
|
|
74
116
|
/**
|
|
75
117
|
* Function to fetch resources using Superagent library with optional caching.
|
|
76
118
|
* This mimics the browser's request but handles it in Node.js to allow caching or header manipulation.
|
|
@@ -81,9 +123,11 @@ function createProxyAgent(proxyUrl) {
|
|
|
81
123
|
* @param {boolean} useFullUrl - Whether to use the full URL as cache key or just origin+path
|
|
82
124
|
* @param {string|false} logger - Log level: "info" (success+error), "error" (errors only), false (no logs)
|
|
83
125
|
* @param {Object|null} proxyAgent - Proxy agent to use for the request
|
|
126
|
+
* @param {boolean} stripHeaders - Whether to sanitize request/response headers
|
|
127
|
+
* @param {boolean} stripLogger - Whether to log stripped headers
|
|
84
128
|
* @returns {Promise<Object>} - The response object containing status, headers, and body
|
|
85
129
|
*/
|
|
86
|
-
async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger, proxyAgent) {
|
|
130
|
+
async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger, proxyAgent, stripHeaders, stripLogger) {
|
|
87
131
|
// Determine the cache key based on configuration
|
|
88
132
|
let mainUrl = new URL(url).origin + new URL(url).pathname;
|
|
89
133
|
if (useFullUrl) {
|
|
@@ -102,9 +146,12 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
|
|
|
102
146
|
}
|
|
103
147
|
|
|
104
148
|
try {
|
|
149
|
+
// Sanitize outgoing request headers if stripHeaders is enabled
|
|
150
|
+
const finalHeaders = stripHeaders ? sanitizeRequestHeaders(requestHeaders, stripLogger, url) : requestHeaders;
|
|
151
|
+
|
|
105
152
|
// Fetch the resource using superagent
|
|
106
153
|
// buffer(true) ensures we get the raw binary data (essential for images/fonts)
|
|
107
|
-
let request = superagent(method, url).set(
|
|
154
|
+
let request = superagent(method, url).set(finalHeaders).buffer(true);
|
|
108
155
|
|
|
109
156
|
// Apply proxy agent if provided
|
|
110
157
|
if (proxyAgent) {
|
|
@@ -116,11 +163,14 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
|
|
|
116
163
|
// Determine the correct body type (Buffer for binary, text for others)
|
|
117
164
|
const responseBody = response.body instanceof Buffer ? response.body : response.text;
|
|
118
165
|
|
|
166
|
+
// Sanitize response headers if stripHeaders is enabled
|
|
167
|
+
const finalResponseHeaders = stripHeaders ? sanitizeResponseHeaders(response.headers, stripLogger, url) : response.headers;
|
|
168
|
+
|
|
119
169
|
// Save to cache only when caching is enabled
|
|
120
170
|
if (useCache) {
|
|
121
171
|
globalCache.set(mainUrl, {
|
|
122
172
|
status: response.status,
|
|
123
|
-
headers:
|
|
173
|
+
headers: finalResponseHeaders,
|
|
124
174
|
body: responseBody,
|
|
125
175
|
});
|
|
126
176
|
if (logger === "info") console.log(`Success (cached${viaProxy}): ${mainUrl}`);
|
|
@@ -130,7 +180,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
|
|
|
130
180
|
|
|
131
181
|
return {
|
|
132
182
|
status: response.status,
|
|
133
|
-
headers:
|
|
183
|
+
headers: finalResponseHeaders,
|
|
134
184
|
body: responseBody,
|
|
135
185
|
};
|
|
136
186
|
} catch (error) {
|
|
@@ -165,6 +215,8 @@ export async function pwRoute({
|
|
|
165
215
|
useGot = false,
|
|
166
216
|
useFullUrl = true,
|
|
167
217
|
useCache = true,
|
|
218
|
+
stripGotHeaders = true,
|
|
219
|
+
stripGotLogger = false,
|
|
168
220
|
proxy = null,
|
|
169
221
|
m4w_send_on_post = null,
|
|
170
222
|
m4w_send_on_message = null,
|
|
@@ -357,7 +409,9 @@ export async function pwRoute({
|
|
|
357
409
|
requestMethod,
|
|
358
410
|
useFullUrl,
|
|
359
411
|
logger,
|
|
360
|
-
proxyAgent
|
|
412
|
+
proxyAgent,
|
|
413
|
+
stripGotHeaders,
|
|
414
|
+
stripGotLogger
|
|
361
415
|
);
|
|
362
416
|
|
|
363
417
|
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
|
|
@@ -224,10 +235,10 @@ export async function startProxyServer({
|
|
|
224
235
|
|
|
225
236
|
// 5. Stats
|
|
226
237
|
const stats = {
|
|
227
|
-
DEFAULT_PROXY: { request: 0,
|
|
228
|
-
NO_PROXY: { request: 0,
|
|
229
|
-
PROXY_1: { request: 0,
|
|
230
|
-
PROXY_2: { request: 0,
|
|
238
|
+
DEFAULT_PROXY: { request: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 },
|
|
239
|
+
NO_PROXY: { request: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 },
|
|
240
|
+
PROXY_1: { request: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 },
|
|
241
|
+
PROXY_2: { request: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 },
|
|
231
242
|
};
|
|
232
243
|
// hostStatsMap is now categorized by proxy type
|
|
233
244
|
const hostStatsMap = {
|
|
@@ -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,
|
|
@@ -293,7 +301,7 @@ export async function startProxyServer({
|
|
|
293
301
|
// Ensure the type exists in map (it should, but safety first)
|
|
294
302
|
if (!hostStatsMap[proxyType]) hostStatsMap[proxyType] = {};
|
|
295
303
|
if (!hostStatsMap[proxyType][hostname]) {
|
|
296
|
-
hostStatsMap[proxyType][hostname] = { req: 0,
|
|
304
|
+
hostStatsMap[proxyType][hostname] = { req: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 };
|
|
297
305
|
}
|
|
298
306
|
hostStatsMap[proxyType][hostname].req++;
|
|
299
307
|
}
|
|
@@ -312,17 +320,26 @@ export async function startProxyServer({
|
|
|
312
320
|
|
|
313
321
|
server.on("connectionClosed", ({ connectionId, stats: connStats }) => {
|
|
314
322
|
const connectionInfo = connectionMap[connectionId];
|
|
315
|
-
if (connectionInfo) {
|
|
323
|
+
if (connectionInfo && connStats) {
|
|
316
324
|
const { type, hostname } = connectionInfo;
|
|
325
|
+
const srcTx = connStats.srcTxBytes || 0;
|
|
326
|
+
const srcRx = connStats.srcRxBytes || 0;
|
|
327
|
+
const trgTx = connStats.trgTxBytes || 0;
|
|
328
|
+
const trgRx = connStats.trgRxBytes || 0;
|
|
329
|
+
|
|
317
330
|
if (proxy_stats && stats[type]) {
|
|
318
331
|
stats[type].request++;
|
|
319
|
-
stats[type].
|
|
320
|
-
stats[type].
|
|
332
|
+
stats[type].srcTx += srcTx;
|
|
333
|
+
stats[type].srcRx += srcRx;
|
|
334
|
+
stats[type].trgTx += trgTx;
|
|
335
|
+
stats[type].trgRx += trgRx;
|
|
321
336
|
}
|
|
322
|
-
// Update host stats
|
|
337
|
+
// Update host stats
|
|
323
338
|
if (host_stats && hostname && hostStatsMap[type] && hostStatsMap[type][hostname]) {
|
|
324
|
-
hostStatsMap[type][hostname].
|
|
325
|
-
hostStatsMap[type][hostname].
|
|
339
|
+
hostStatsMap[type][hostname].srcTx += srcTx;
|
|
340
|
+
hostStatsMap[type][hostname].srcRx += srcRx;
|
|
341
|
+
hostStatsMap[type][hostname].trgTx += trgTx;
|
|
342
|
+
hostStatsMap[type][hostname].trgRx += trgRx;
|
|
326
343
|
}
|
|
327
344
|
}
|
|
328
345
|
delete connectionMap[connectionId];
|
|
@@ -331,38 +348,27 @@ export async function startProxyServer({
|
|
|
331
348
|
try {
|
|
332
349
|
await server.listen();
|
|
333
350
|
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
351
|
console.log(`░░ Local Proxy Started: http://127.0.0.1:${selectedPort}`);
|
|
352
352
|
} catch (err) {
|
|
353
353
|
console.error("░░ Failed to start proxy server:", err);
|
|
354
354
|
return null;
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
const formatBytes = (bytes) =>
|
|
357
|
+
const formatBytes = (bytes) => {
|
|
358
|
+
if (bytes < 1024) return bytes + " B";
|
|
359
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
|
|
360
|
+
return (bytes / (1024 * 1024)).toFixed(3) + " MB";
|
|
361
|
+
};
|
|
358
362
|
|
|
359
363
|
const getProxyStatsFormatted = () => {
|
|
360
364
|
const formatted = {};
|
|
361
365
|
for (const [key, val] of Object.entries(stats)) {
|
|
362
366
|
formatted[key] = {
|
|
363
367
|
req: val.request,
|
|
364
|
-
|
|
365
|
-
|
|
368
|
+
srcTx: formatBytes(val.srcTx),
|
|
369
|
+
srcRx: formatBytes(val.srcRx),
|
|
370
|
+
trgTx: formatBytes(val.trgTx),
|
|
371
|
+
trgRx: formatBytes(val.trgRx),
|
|
366
372
|
};
|
|
367
373
|
}
|
|
368
374
|
return formatted;
|
|
@@ -370,20 +376,20 @@ export async function startProxyServer({
|
|
|
370
376
|
|
|
371
377
|
const getHostStatsFormatted = () => {
|
|
372
378
|
const result = {};
|
|
373
|
-
// Iterate over each proxy category
|
|
374
379
|
for (const [type, hosts] of Object.entries(hostStatsMap)) {
|
|
375
380
|
const sortedHosts = Object.entries(hosts)
|
|
376
|
-
.sort((a, b) => b[1].req - a[1].req)
|
|
381
|
+
.sort((a, b) => b[1].req - a[1].req)
|
|
377
382
|
.reduce((acc, [host, hostData]) => {
|
|
378
383
|
acc[host] = {
|
|
379
384
|
req: hostData.req,
|
|
380
|
-
|
|
381
|
-
|
|
385
|
+
srcTx: formatBytes(hostData.srcTx),
|
|
386
|
+
srcRx: formatBytes(hostData.srcRx),
|
|
387
|
+
trgTx: formatBytes(hostData.trgTx),
|
|
388
|
+
trgRx: formatBytes(hostData.trgRx),
|
|
382
389
|
};
|
|
383
390
|
return acc;
|
|
384
391
|
}, {});
|
|
385
392
|
|
|
386
|
-
// Only include categories that have traffic
|
|
387
393
|
if (Object.keys(sortedHosts).length > 0) {
|
|
388
394
|
result[type] = sortedHosts;
|
|
389
395
|
}
|
|
@@ -415,16 +421,11 @@ export async function startProxyServer({
|
|
|
415
421
|
isServerRunning: () => serverRunning,
|
|
416
422
|
|
|
417
423
|
closeServer: async () => {
|
|
424
|
+
// Close the server — triggers connectionClosed events which accumulate byte stats
|
|
418
425
|
await server.close(true);
|
|
419
426
|
serverRunning = false;
|
|
420
427
|
|
|
421
|
-
//
|
|
422
|
-
for (const socket of activeSockets) {
|
|
423
|
-
socket.destroy();
|
|
424
|
-
}
|
|
425
|
-
activeSockets.clear();
|
|
426
|
-
|
|
427
|
-
// Auto console.log stats on close
|
|
428
|
+
// Log stats AFTER close so all connectionClosed events have fired
|
|
428
429
|
if (proxy_stats) {
|
|
429
430
|
console.log("░░ Proxy Stats:", getProxyStatsFormatted());
|
|
430
431
|
}
|
|
@@ -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,12 @@ 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: true) */
|
|
33
|
+
stripGotHeaders?: boolean;
|
|
34
|
+
|
|
35
|
+
/** Log stripped headers to console for debugging (default: false) */
|
|
36
|
+
stripGotLogger?: boolean;
|
|
37
|
+
|
|
32
38
|
/**
|
|
33
39
|
* Proxy for custom fetch requests (only used when useGot is true).
|
|
34
40
|
* String: "http://host:port", "socks5://user:pass@host:port"
|
|
@@ -71,6 +71,48 @@ 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, logger, url) {
|
|
79
|
+
const cleaned = { ...headers };
|
|
80
|
+
const stripped = [];
|
|
81
|
+
// Remove known auth headers
|
|
82
|
+
if (cleaned["cookie"]) { stripped.push("cookie"); delete cleaned["cookie"]; }
|
|
83
|
+
if (cleaned["authorization"]) { stripped.push("authorization"); delete cleaned["authorization"]; }
|
|
84
|
+
// Remove any header containing auth/token/csrf keywords
|
|
85
|
+
for (const key of Object.keys(cleaned)) {
|
|
86
|
+
const lower = key.toLowerCase();
|
|
87
|
+
if (lower.includes("token") || lower.includes("csrf") || lower.includes("auth")) {
|
|
88
|
+
stripped.push(key);
|
|
89
|
+
delete cleaned[key];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (logger && stripped.length) console.log(`[stripGot] Request stripped: [${stripped.join(", ")}] → ${url}`);
|
|
93
|
+
return cleaned;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Sanitizes response headers for safe caching across origins.
|
|
98
|
+
* - Replaces access-control-allow-origin with * (if present)
|
|
99
|
+
* - Removes access-control-allow-credentials, set-cookie, CSP, HSTS
|
|
100
|
+
*/
|
|
101
|
+
function sanitizeResponseHeaders(headers, logger, url) {
|
|
102
|
+
const cleaned = { ...headers };
|
|
103
|
+
const changes = [];
|
|
104
|
+
if (cleaned["access-control-allow-origin"]) {
|
|
105
|
+
changes.push(`access-control-allow-origin: ${cleaned["access-control-allow-origin"]} → *`);
|
|
106
|
+
cleaned["access-control-allow-origin"] = "*";
|
|
107
|
+
}
|
|
108
|
+
if (cleaned["access-control-allow-credentials"]) { changes.push("access-control-allow-credentials"); delete cleaned["access-control-allow-credentials"]; }
|
|
109
|
+
if (cleaned["set-cookie"]) { changes.push("set-cookie"); delete cleaned["set-cookie"]; }
|
|
110
|
+
if (cleaned["content-security-policy"]) { changes.push("content-security-policy"); delete cleaned["content-security-policy"]; }
|
|
111
|
+
if (cleaned["strict-transport-security"]) { changes.push("strict-transport-security"); delete cleaned["strict-transport-security"]; }
|
|
112
|
+
if (logger && changes.length) console.log(`[stripGot] Response stripped: [${changes.join(", ")}] → ${url}`);
|
|
113
|
+
return cleaned;
|
|
114
|
+
}
|
|
115
|
+
|
|
74
116
|
/**
|
|
75
117
|
* Function to fetch resources using Superagent library with optional caching.
|
|
76
118
|
* This mimics the browser's request but handles it in Node.js to allow caching or header manipulation.
|
|
@@ -81,9 +123,11 @@ function createProxyAgent(proxyUrl) {
|
|
|
81
123
|
* @param {boolean} useFullUrl - Whether to use the full URL as cache key or just origin+path
|
|
82
124
|
* @param {string|false} logger - Log level: "info" (success+error), "error" (errors only), false (no logs)
|
|
83
125
|
* @param {Object|null} proxyAgent - Proxy agent to use for the request
|
|
126
|
+
* @param {boolean} stripHeaders - Whether to sanitize request/response headers
|
|
127
|
+
* @param {boolean} stripLogger - Whether to log stripped headers
|
|
84
128
|
* @returns {Promise<Object>} - The response object containing status, headers, and body
|
|
85
129
|
*/
|
|
86
|
-
async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger, proxyAgent) {
|
|
130
|
+
async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger, proxyAgent, stripHeaders, stripLogger) {
|
|
87
131
|
// Determine the cache key based on configuration
|
|
88
132
|
let mainUrl = new URL(url).origin + new URL(url).pathname;
|
|
89
133
|
if (useFullUrl) {
|
|
@@ -102,9 +146,12 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
|
|
|
102
146
|
}
|
|
103
147
|
|
|
104
148
|
try {
|
|
149
|
+
// Sanitize outgoing request headers if stripHeaders is enabled
|
|
150
|
+
const finalHeaders = stripHeaders ? sanitizeRequestHeaders(requestHeaders, stripLogger, url) : requestHeaders;
|
|
151
|
+
|
|
105
152
|
// Fetch the resource using superagent
|
|
106
153
|
// buffer(true) ensures we get the raw binary data (essential for images/fonts)
|
|
107
|
-
let request = superagent(method, url).set(
|
|
154
|
+
let request = superagent(method, url).set(finalHeaders).buffer(true);
|
|
108
155
|
|
|
109
156
|
// Apply proxy agent if provided
|
|
110
157
|
if (proxyAgent) {
|
|
@@ -116,11 +163,14 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
|
|
|
116
163
|
// Determine the correct body type (Buffer for binary, text for others)
|
|
117
164
|
const responseBody = response.body instanceof Buffer ? response.body : response.text;
|
|
118
165
|
|
|
166
|
+
// Sanitize response headers if stripHeaders is enabled
|
|
167
|
+
const finalResponseHeaders = stripHeaders ? sanitizeResponseHeaders(response.headers, stripLogger, url) : response.headers;
|
|
168
|
+
|
|
119
169
|
// Save to cache only when caching is enabled
|
|
120
170
|
if (useCache) {
|
|
121
171
|
globalCache.set(mainUrl, {
|
|
122
172
|
status: response.status,
|
|
123
|
-
headers:
|
|
173
|
+
headers: finalResponseHeaders,
|
|
124
174
|
body: responseBody,
|
|
125
175
|
});
|
|
126
176
|
if (logger === "info") console.log(`Success (cached${viaProxy}): ${mainUrl}`);
|
|
@@ -130,7 +180,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
|
|
|
130
180
|
|
|
131
181
|
return {
|
|
132
182
|
status: response.status,
|
|
133
|
-
headers:
|
|
183
|
+
headers: finalResponseHeaders,
|
|
134
184
|
body: responseBody,
|
|
135
185
|
};
|
|
136
186
|
} catch (error) {
|
|
@@ -163,6 +213,8 @@ export async function ppRoute({
|
|
|
163
213
|
useGot = false,
|
|
164
214
|
useFullUrl = true,
|
|
165
215
|
useCache = true,
|
|
216
|
+
stripGotHeaders = true,
|
|
217
|
+
stripGotLogger = false,
|
|
166
218
|
proxy = null,
|
|
167
219
|
m4w_send_on_post = null,
|
|
168
220
|
m4w_send_on_message = null,
|
|
@@ -360,7 +412,9 @@ export async function ppRoute({
|
|
|
360
412
|
requestMethod,
|
|
361
413
|
useFullUrl,
|
|
362
414
|
logger,
|
|
363
|
-
proxyAgent
|
|
415
|
+
proxyAgent,
|
|
416
|
+
stripGotHeaders,
|
|
417
|
+
stripGotLogger
|
|
364
418
|
);
|
|
365
419
|
|
|
366
420
|
if (response) {
|