arn-browser 0.1.29 → 0.1.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arn-browser",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "A lightweight, browser autmation helper.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -208,14 +208,7 @@ export async function startProxyServer({
208
208
  proxy2: createHostMatcher([...PROXY_2_HOSTS, "proxy.multilogin.com", "multilogin.com"]),
209
209
  };
210
210
 
211
- // 2. Port
212
- const selectedPort = await findAvailablePort(50001, 50010);
213
- if (!selectedPort) {
214
- console.error("░░ Critical Error: No available ports.");
215
- return null;
216
- }
217
-
218
- // 3. Build URLs
211
+ // 2. Build URLs
219
212
  const buildURL = (data) => {
220
213
  if (!data) return null; // Returns null if no config
221
214
  const { type = "http", host, port, user, pass } = data;
@@ -230,7 +223,7 @@ export async function startProxyServer({
230
223
  p2: buildURL(PROXY_2_DATA),
231
224
  };
232
225
 
233
- // 4. Fetch Details (Simplified Logic)
226
+ // 3. Fetch Details (Simplified Logic)
234
227
  // We pass the URL (or null). The function handles the "Local" logic.
235
228
  const [defaultDetails, p1Details, p2Details] = await Promise.all([
236
229
  fetchProxyDetails(upstreamProxies.default, ip2LocationKey, retryDelayMs),
@@ -243,7 +236,7 @@ export async function startProxyServer({
243
236
  if (upstreamProxies.p1 && !p1Details) { console.warn("░░ Warning: PROXY_1 configured but unreachable."); return null; }
244
237
  if (upstreamProxies.p2 && !p2Details) { console.warn("░░ Warning: PROXY_2 configured but unreachable."); return null; }
245
238
 
246
- // 5. Stats
239
+ // 4. Stats
247
240
  const stats = {
248
241
  DEFAULT_PROXY: { request: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 },
249
242
  NO_PROXY: { request: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 },
@@ -261,72 +254,105 @@ export async function startProxyServer({
261
254
  const connectionMap = {}; // Maps connectionId -> { type: "..." }
262
255
  let serverRunning = false;
263
256
 
264
- // 6. Server
265
- const server = new ProxyChain.Server({
266
- port: selectedPort,
267
- host: "127.0.0.1",
268
- verbose: debug,
269
- prepareRequestFunction: ({ hostname, connectionId }) => {
270
- let proxyType = "DEFAULT_PROXY";
271
- let upstreamUrl = upstreamProxies.default;
272
- let isCustomResponse = false;
273
- let customResponseData = null;
274
-
275
- // Logic to determine Proxy Type
276
- if (matchers.noProxy(hostname)) {
277
- // A. Direct
278
- proxyType = "NO_PROXY";
279
- upstreamUrl = null;
280
- } else if (matchers.proxy1(hostname) && upstreamProxies.p1) {
281
- // C1. Proxy 1
282
- proxyType = "PROXY_1";
283
- upstreamUrl = upstreamProxies.p1;
284
- } else if (matchers.proxy2(hostname) && upstreamProxies.p2) {
285
- // C2. Proxy 2
286
- proxyType = "PROXY_2";
287
- upstreamUrl = upstreamProxies.p2;
288
- }
257
+ // 5. Create server and bind with retry (handles race conditions between concurrent processes)
258
+ const PORT_RANGE_START = 50001;
259
+ const PORT_RANGE_END = 50200;
260
+ const MAX_BIND_RETRIES = 10;
289
261
 
290
- // B. IP Check Interception (Overrules standard routing for specific domain)
291
- if (hostname === "ip.bablosoft.com") {
292
- isCustomResponse = true;
293
- // Inherit the proxyType determined above to fetch the correct IP details
294
- // (e.g. if it matched PROXY_1 matchers, we show PROXY_1 IP)
295
- let displayedIP;
296
- if (proxyType === "PROXY_1") displayedIP = p1Details?.ip;
297
- else if (proxyType === "PROXY_2") displayedIP = p2Details?.ip;
298
- else if (proxyType === "NO_PROXY") displayedIP = "127.0.0.1";
299
- else displayedIP = defaultDetails?.ip;
300
-
301
- customResponseData = {
302
- statusCode: 200,
303
- headers: { "Content-Type": "text/plain", Connection: "close" },
304
- body: displayedIP || "Unknown IP",
305
- };
306
- }
262
+ let selectedPort = null;
263
+ let server = null;
264
+
265
+ for (let attempt = 1; attempt <= MAX_BIND_RETRIES; attempt++) {
266
+ const candidatePort = await findAvailablePort(PORT_RANGE_START, PORT_RANGE_END);
267
+ if (!candidatePort) {
268
+ console.error("░░ Critical Error: No available ports in range " + PORT_RANGE_START + "-" + PORT_RANGE_END);
269
+ return null;
270
+ }
307
271
 
308
- // Record Stats
309
- connectionMap[connectionId] = { type: proxyType, hostname: hostname };
310
- if (host_stats && hostname) {
311
- // Ensure the type exists in map (it should, but safety first)
312
- if (!hostStatsMap[proxyType]) hostStatsMap[proxyType] = {};
313
- if (!hostStatsMap[proxyType][hostname]) {
314
- hostStatsMap[proxyType][hostname] = { req: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 };
272
+ server = new ProxyChain.Server({
273
+ port: candidatePort,
274
+ host: "127.0.0.1",
275
+ verbose: debug,
276
+ prepareRequestFunction: ({ hostname, connectionId }) => {
277
+ let proxyType = "DEFAULT_PROXY";
278
+ let upstreamUrl = upstreamProxies.default;
279
+ let isCustomResponse = false;
280
+ let customResponseData = null;
281
+
282
+ // Logic to determine Proxy Type
283
+ if (matchers.noProxy(hostname)) {
284
+ // A. Direct
285
+ proxyType = "NO_PROXY";
286
+ upstreamUrl = null;
287
+ } else if (matchers.proxy1(hostname) && upstreamProxies.p1) {
288
+ // C1. Proxy 1
289
+ proxyType = "PROXY_1";
290
+ upstreamUrl = upstreamProxies.p1;
291
+ } else if (matchers.proxy2(hostname) && upstreamProxies.p2) {
292
+ // C2. Proxy 2
293
+ proxyType = "PROXY_2";
294
+ upstreamUrl = upstreamProxies.p2;
295
+ }
296
+
297
+ // B. IP Check Interception (Overrules standard routing for specific domain)
298
+ if (hostname === "ip.bablosoft.com") {
299
+ isCustomResponse = true;
300
+ let displayedIP;
301
+ if (proxyType === "PROXY_1") displayedIP = p1Details?.ip;
302
+ else if (proxyType === "PROXY_2") displayedIP = p2Details?.ip;
303
+ else if (proxyType === "NO_PROXY") displayedIP = "127.0.0.1";
304
+ else displayedIP = defaultDetails?.ip;
305
+
306
+ customResponseData = {
307
+ statusCode: 200,
308
+ headers: { "Content-Type": "text/plain", Connection: "close" },
309
+ body: displayedIP || "Unknown IP",
310
+ };
315
311
  }
316
- hostStatsMap[proxyType][hostname].req++;
317
- }
318
312
 
319
- // Return Decision
320
- if (isCustomResponse) {
321
- return { customResponseFunction: () => customResponseData };
313
+ // Record Stats
314
+ connectionMap[connectionId] = { type: proxyType, hostname: hostname };
315
+ if (host_stats && hostname) {
316
+ if (!hostStatsMap[proxyType]) hostStatsMap[proxyType] = {};
317
+ if (!hostStatsMap[proxyType][hostname]) {
318
+ hostStatsMap[proxyType][hostname] = { req: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 };
319
+ }
320
+ hostStatsMap[proxyType][hostname].req++;
321
+ }
322
+
323
+ // Return Decision
324
+ if (isCustomResponse) {
325
+ return { customResponseFunction: () => customResponseData };
326
+ }
327
+
328
+ return {
329
+ upstreamProxyUrl: upstreamUrl,
330
+ requestAuthentication: false,
331
+ };
332
+ },
333
+ });
334
+
335
+ try {
336
+ await server.listen();
337
+ selectedPort = candidatePort;
338
+ serverRunning = true;
339
+ console.log(`░░ Local Proxy Started: http://127.0.0.1:${selectedPort}`);
340
+ break; // Successfully bound — exit retry loop
341
+ } catch (err) {
342
+ if (err.code === "EADDRINUSE" && attempt < MAX_BIND_RETRIES) {
343
+ console.warn(`░░ Port ${candidatePort} taken (race), retrying... (${attempt}/${MAX_BIND_RETRIES})`);
344
+ await sleep(50 + Math.random() * 100); // Small random delay to de-sync concurrent processes
345
+ continue;
322
346
  }
347
+ console.error("░░ Failed to start proxy server:", err);
348
+ return null;
349
+ }
350
+ }
323
351
 
324
- return {
325
- upstreamProxyUrl: upstreamUrl,
326
- requestAuthentication: false, // Auto-handle upstream auth via URL
327
- };
328
- },
329
- });
352
+ if (!selectedPort || !server) {
353
+ console.error("░░ Critical Error: Could not bind to any port after " + MAX_BIND_RETRIES + " attempts.");
354
+ return null;
355
+ }
330
356
 
331
357
  server.on("connectionClosed", ({ connectionId, stats: connStats }) => {
332
358
  const connectionInfo = connectionMap[connectionId];
@@ -355,15 +381,6 @@ export async function startProxyServer({
355
381
  delete connectionMap[connectionId];
356
382
  });
357
383
 
358
- try {
359
- await server.listen();
360
- serverRunning = true;
361
- console.log(`░░ Local Proxy Started: http://127.0.0.1:${selectedPort}`);
362
- } catch (err) {
363
- console.error("░░ Failed to start proxy server:", err);
364
- return null;
365
- }
366
-
367
384
  const formatBytes = (bytes) => {
368
385
  if (bytes < 1024) return bytes + " B";
369
386
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
@@ -54,8 +54,9 @@ export async function get_multilogin_proxy({
54
54
  // 1. Get Token
55
55
  const token = await getMultiloginToken();
56
56
 
57
- // 2. Prepare Data
58
- const data = { country, sessionType, protocol, region, city, IPTTL, count: 1 };
57
+ // 2. Prepare Data (sanitize region: spaces → underscores)
58
+ const sanitizedRegion = region ? region.toLowerCase().replace(/ /g, '_') : region;
59
+ const data = { country, sessionType, protocol, region: sanitizedRegion, city, IPTTL, count: 1 };
59
60
  Object.keys(data).forEach((k) => (data[k] === "" || data[k] === 0 || data[k] == null) && delete data[k]);
60
61
 
61
62
  // 3. Setup Timeout
@@ -64,7 +64,18 @@ export interface PpLaunchOptions {
64
64
  which_browser?: "chrome" | "chromium" | "brave" | "multilogin";
65
65
 
66
66
  // ========================================================================
67
- // 2. STORAGE & PROFILES
67
+ // 2. GENERAL & NETWORK
68
+ // ========================================================================
69
+
70
+ /**
71
+ * Timezone ID (IANA format, e.g., "America/New_York").
72
+ * Applied via CDP `Emulation.setTimezoneOverride` after connecting.
73
+ * Default: null (uses system timezone)
74
+ */
75
+ timezoneId?: string | null;
76
+
77
+ // ========================================================================
78
+ // 3. STORAGE & PROFILES
68
79
  // ========================================================================
69
80
 
70
81
  /**
@@ -86,7 +97,7 @@ export interface PpLaunchOptions {
86
97
  cleanupMinutes?: number;
87
98
 
88
99
  // ========================================================================
89
- // 3. NETWORK
100
+ // 4. NETWORK
90
101
  // ========================================================================
91
102
 
92
103
  /**
@@ -96,7 +107,7 @@ export interface PpLaunchOptions {
96
107
  proxy?: ProxyConfig | string | null;
97
108
 
98
109
  // ========================================================================
99
- // 4. BROWSER ARGS
110
+ // 5. BROWSER ARGS
100
111
  // ========================================================================
101
112
 
102
113
  /**
@@ -108,7 +119,7 @@ export interface PpLaunchOptions {
108
119
  extraArgs?: string[];
109
120
 
110
121
  // ========================================================================
111
- // 5. ENGINE SPECIFIC
122
+ // 6. ENGINE SPECIFIC
112
123
  // ========================================================================
113
124
 
114
125
  /**
@@ -117,7 +128,7 @@ export interface PpLaunchOptions {
117
128
  multilogin_options?: PpMultiloginOptions;
118
129
 
119
130
  // ========================================================================
120
- // 6. FINGERPRINT SPOOFING
131
+ // 7. FINGERPRINT SPOOFING
121
132
  // ========================================================================
122
133
 
123
134
  /**
@@ -169,7 +180,7 @@ export interface PpLaunchOptions {
169
180
  };
170
181
 
171
182
  // ========================================================================
172
- // 7. LOGGING
183
+ // 8. LOGGING
173
184
  // ========================================================================
174
185
 
175
186
  /**
@@ -265,6 +265,9 @@ export async function ppLaunch({
265
265
  // Browser selection
266
266
  which_browser = "chrome",
267
267
 
268
+ // Common
269
+ timezoneId = null,
270
+
268
271
  // Path & Storage
269
272
  profile_path = null,
270
273
  cleanupMinutes = 0,
@@ -316,6 +319,7 @@ export async function ppLaunch({
316
319
  result = await chromeLauncher({
317
320
  profilePath: fullPath,
318
321
  proxy,
322
+ timezoneId,
319
323
  extraArgs,
320
324
  spoof_fingerprint,
321
325
  cleanupMinutes: effectiveCleanupMinutes,
@@ -325,6 +329,7 @@ export async function ppLaunch({
325
329
  result = await braveLauncher({
326
330
  profilePath: fullPath,
327
331
  proxy,
332
+ timezoneId,
328
333
  extraArgs,
329
334
  spoof_fingerprint,
330
335
  cleanupMinutes: effectiveCleanupMinutes,
@@ -351,7 +356,7 @@ export async function ppLaunch({
351
356
  // 4. ENGINE: CHROME (CDP)
352
357
  // ==========================================================================
353
358
 
354
- async function chromeLauncher({ profilePath, proxy, extraArgs, spoof_fingerprint, cleanupMinutes }) {
359
+ async function chromeLauncher({ profilePath, proxy, timezoneId, extraArgs, spoof_fingerprint, cleanupMinutes }) {
355
360
  const isPersistent = !!profilePath;
356
361
  const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
357
362
 
@@ -365,6 +370,7 @@ async function chromeLauncher({ profilePath, proxy, extraArgs, spoof_fingerprint
365
370
  profilePath: activePath,
366
371
  isPersistent,
367
372
  proxy,
373
+ timezoneId,
368
374
  extraArgs,
369
375
  spoof_fingerprint,
370
376
  browserLabel: "Chrome",
@@ -375,7 +381,7 @@ async function chromeLauncher({ profilePath, proxy, extraArgs, spoof_fingerprint
375
381
  // 5. ENGINE: BRAVE (CDP)
376
382
  // ==========================================================================
377
383
 
378
- async function braveLauncher({ profilePath, proxy, extraArgs, spoof_fingerprint, cleanupMinutes }) {
384
+ async function braveLauncher({ profilePath, proxy, timezoneId, extraArgs, spoof_fingerprint, cleanupMinutes }) {
379
385
  const isPersistent = !!profilePath;
380
386
  const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
381
387
 
@@ -445,6 +451,7 @@ async function braveLauncher({ profilePath, proxy, extraArgs, spoof_fingerprint,
445
451
  profilePath: activePath,
446
452
  isPersistent,
447
453
  proxy,
454
+ timezoneId,
448
455
  extraArgs: [...braveArgs, ...extraArgs],
449
456
  spoof_fingerprint,
450
457
  browserLabel: "Brave",
@@ -455,7 +462,7 @@ async function braveLauncher({ profilePath, proxy, extraArgs, spoof_fingerprint,
455
462
  // 6. CDP SPAWN & CONNECT (Shared between Chrome & Brave)
456
463
  // ==========================================================================
457
464
 
458
- async function spawnAndConnect({ binaryPath, profilePath, isPersistent, proxy, extraArgs = [], spoof_fingerprint = false, browserLabel = "Browser" }) {
465
+ async function spawnAndConnect({ binaryPath, profilePath, isPersistent, proxy, timezoneId = null, extraArgs = [], spoof_fingerprint = false, browserLabel = "Browser" }) {
459
466
  let browser;
460
467
  let closing = false;
461
468
  let signalHandler;
@@ -597,6 +604,13 @@ async function spawnAndConnect({ binaryPath, profilePath, isPersistent, proxy, e
597
604
  const pages = await browser.pages();
598
605
  const page = pages[0] ?? (await browser.newPage());
599
606
 
607
+ // Apply timezone emulation via CDP
608
+ const tz = timezoneId || undefined;
609
+ if (tz) {
610
+ await page.emulateTimezone(tz);
611
+ if (_launchLogs) console.log(`░░░░░ Timezone set to ${tz} for ${browserLabel}`);
612
+ }
613
+
600
614
  // Fingerprint injection logic:
601
615
  // 1. If persistent profile has a saved fingerprint.json → ALWAYS use it (even if spoof_fingerprint is false)
602
616
  // 2. If spoof_fingerprint is truthy → generate new fingerprint (save it for persistent profiles)
@@ -142,6 +142,9 @@ function sanitizeResponseHeaders(headers, logger, url) {
142
142
  }
143
143
  }
144
144
 
145
+ // Always ensure CORS header is present, even if server never sent it
146
+ cleaned["access-control-allow-origin"] = "*";
147
+
145
148
  if (logger && stripped.length) console.log(`[stripGot] Response stripped ${stripped.length} headers: [${stripped.join(", ")}] → ${url}`);
146
149
  return cleaned;
147
150
  }