@swimmingliu/autovpn 1.6.5 → 1.6.7

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.
@@ -1,27 +1,7 @@
1
- import path from 'node:path';
2
- import { spawn as defaultSpawn } from 'node:child_process';
3
1
  import net from 'node:net';
4
2
  import tls from 'node:tls';
5
- import { mergeProjectEnv } from '../runtime/env.js';
6
3
  import { openMihomoRuntime as defaultOpenMihomoRuntime, probeMihomoProxyDelay as defaultProbeMihomoProxyDelay } from './proxy-runtime.js';
7
- const PYTHON_SPEEDTEST_HELPER = `
8
- import json
9
- import sys
10
- from vpn_automation.config.models import SpeedTestConfig
11
- from vpn_automation.pipeline.speedtest import speedtest_links
12
-
13
- payload = json.load(sys.stdin)
14
- output = [
15
- item.__dict__
16
- for item in speedtest_links(
17
- payload.get("links", []),
18
- SpeedTestConfig(**payload["config"]),
19
- runtime_path=payload.get("runtime_path", ""),
20
- )
21
- ]
22
- json.dump(output, sys.stdout, ensure_ascii=False)
23
- sys.stdout.write("\\n")
24
- `;
4
+ const PROBE_MAX_ATTEMPTS = 2;
25
5
  export function aggregateSpeedMeasurements(values) {
26
6
  if (values.length === 0) {
27
7
  return 0;
@@ -94,9 +74,27 @@ function defaultNow() {
94
74
  }
95
75
  async function fetchWithTimeout(fetchImpl, url, timeoutMs) {
96
76
  const controller = new AbortController();
97
- const timer = setTimeout(() => controller.abort(), timeoutMs);
77
+ let internallyTimedOut = false;
78
+ const timer = setTimeout(() => {
79
+ internallyTimedOut = true;
80
+ controller.abort();
81
+ }, timeoutMs);
98
82
  try {
99
- return await fetchImpl(url, { signal: controller.signal });
83
+ const response = await fetchImpl(url, { signal: controller.signal });
84
+ if (internallyTimedOut) {
85
+ const error = new Error(`request timed out after ${timeoutMs}ms`);
86
+ error.code = 'AUTOVPN_INTERNAL_TIMEOUT';
87
+ throw error;
88
+ }
89
+ return response;
90
+ }
91
+ catch (error) {
92
+ if (internallyTimedOut && !(error instanceof Error && error.code === 'AUTOVPN_INTERNAL_TIMEOUT')) {
93
+ const timeoutError = new Error(`request timed out after ${timeoutMs}ms`, { cause: error });
94
+ timeoutError.code = 'AUTOVPN_INTERNAL_TIMEOUT';
95
+ throw timeoutError;
96
+ }
97
+ throw error;
100
98
  }
101
99
  finally {
102
100
  clearTimeout(timer);
@@ -122,6 +120,7 @@ async function readResponseBytes(response, maxBytes, timeoutMs) {
122
120
  if (response.body?.getReader) {
123
121
  const reader = response.body.getReader();
124
122
  let timedOut = false;
123
+ let reachedCap = false;
125
124
  try {
126
125
  return await withBodyTimeout(async () => {
127
126
  let total = 0;
@@ -131,6 +130,9 @@ async function readResponseBytes(response, maxBytes, timeoutMs) {
131
130
  break;
132
131
  }
133
132
  total += Math.min(value.byteLength, maxBytes - total);
133
+ if (total >= maxBytes) {
134
+ reachedCap = true;
135
+ }
134
136
  }
135
137
  return total;
136
138
  }, timeoutMs);
@@ -140,16 +142,13 @@ async function readResponseBytes(response, maxBytes, timeoutMs) {
140
142
  throw error;
141
143
  }
142
144
  finally {
143
- if (timedOut) {
145
+ if (timedOut || reachedCap) {
144
146
  await reader.cancel().catch(() => { });
145
147
  }
146
148
  reader.releaseLock();
147
149
  }
148
150
  }
149
- if (response.arrayBuffer) {
150
- return await withBodyTimeout(async () => Math.min((await response.arrayBuffer()).byteLength, maxBytes), timeoutMs);
151
- }
152
- return 0;
151
+ throw new Error('streaming response body required for bounded speed test downloads');
153
152
  }
154
153
  function requireOkResponse(response, allowedStatuses) {
155
154
  const status = Number(response.status ?? 200);
@@ -157,6 +156,36 @@ function requireOkResponse(response, allowedStatuses) {
157
156
  throw new Error(`unexpected status ${status}`);
158
157
  }
159
158
  }
159
+ function isTransientProbeError(error) {
160
+ const message = error instanceof Error ? error.message : String(error);
161
+ const code = typeof error === 'object' && error !== null && 'code' in error
162
+ ? String(error.code ?? '')
163
+ : '';
164
+ const statusMatch = /(?:unexpected status|failed with status)\s+(\d{3})\b/i.exec(message);
165
+ if (statusMatch) {
166
+ const status = Number(statusMatch[1]);
167
+ return status >= 500 && status <= 599;
168
+ }
169
+ if (['AUTOVPN_INTERNAL_TIMEOUT', 'ECONNRESET', 'ETIMEDOUT', 'EPIPE', 'ECONNREFUSED', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT'].includes(code.toUpperCase())) {
170
+ return true;
171
+ }
172
+ return error instanceof TypeError && message.trim().toLowerCase() === 'fetch failed';
173
+ }
174
+ async function retryTransientProbe(operation) {
175
+ let lastError;
176
+ for (let attempt = 1; attempt <= PROBE_MAX_ATTEMPTS; attempt += 1) {
177
+ try {
178
+ return await operation();
179
+ }
180
+ catch (error) {
181
+ lastError = error;
182
+ if (attempt >= PROBE_MAX_ATTEMPTS || !isTransientProbeError(error)) {
183
+ throw error;
184
+ }
185
+ }
186
+ }
187
+ throw lastError;
188
+ }
160
189
  function socketConnect(host, port, timeoutMs) {
161
190
  return new Promise((resolve, reject) => {
162
191
  const socket = net.createConnection({ host, port });
@@ -346,12 +375,14 @@ async function probeLinksDirect(links, config, options) {
346
375
  const now = options.now ?? defaultNow;
347
376
  const timeoutMs = Math.max(1, Number(config.timeout_seconds)) * 1000;
348
377
  return mapWithConcurrency(links, config.concurrency, async (link) => {
349
- const started = now();
350
378
  try {
351
- const response = await fetchWithTimeout(fetchImpl, config.probe_url, timeoutMs);
352
- const elapsed = Math.max(now() - started, 1);
353
- requireOkResponse(response, new Set([200, 204]));
354
- return { link, reachable: true, latency_ms: Math.max(Math.round(elapsed), 1), error: '' };
379
+ return await retryTransientProbe(async () => {
380
+ const started = now();
381
+ const response = await fetchWithTimeout(fetchImpl, config.probe_url, timeoutMs);
382
+ const elapsed = Math.max(now() - started, 1);
383
+ requireOkResponse(response, new Set([200, 204]));
384
+ return { link, reachable: true, latency_ms: Math.max(Math.round(elapsed), 1), error: '' };
385
+ });
355
386
  }
356
387
  catch (error) {
357
388
  return { link, reachable: false, latency_ms: 0, error: error instanceof Error ? error.message : String(error) };
@@ -362,22 +393,26 @@ async function probeLinksMihomo(links, config, runtimePath, options) {
362
393
  const openRuntime = options.openMihomoRuntime ?? defaultOpenMihomoRuntime;
363
394
  const probeDelay = options.probeMihomoProxyDelay ?? defaultProbeMihomoProxyDelay;
364
395
  return mapWithConcurrency(links, config.concurrency, async (link) => {
365
- let runtime;
366
396
  try {
367
- runtime = await openRuntime(link, {
368
- runtimePath,
369
- startupWaitSeconds: config.startup_wait_seconds,
370
- env: options.env
397
+ return await retryTransientProbe(async () => {
398
+ let runtime;
399
+ try {
400
+ runtime = await openRuntime(link, {
401
+ runtimePath,
402
+ startupWaitSeconds: config.startup_wait_seconds,
403
+ env: options.env
404
+ });
405
+ const latencyMs = await probeDelay(runtime.controllerUrl, runtime.proxyName, config.probe_url, config.timeout_seconds);
406
+ return { link, reachable: true, latency_ms: latencyMs, error: '' };
407
+ }
408
+ finally {
409
+ await runtime?.close();
410
+ }
371
411
  });
372
- const latencyMs = await probeDelay(runtime.controllerUrl, runtime.proxyName, config.probe_url, config.timeout_seconds);
373
- return { link, reachable: true, latency_ms: latencyMs, error: '' };
374
412
  }
375
413
  catch (error) {
376
414
  return { link, reachable: false, latency_ms: 0, error: error instanceof Error ? error.message : String(error) };
377
415
  }
378
- finally {
379
- await runtime?.close();
380
- }
381
416
  });
382
417
  }
383
418
  async function testLinkDirect(link, config, options) {
@@ -495,6 +530,17 @@ async function speedtestInNode(input, options) {
495
530
  urls: [...config.urls]
496
531
  });
497
532
  const probes = await probeLinks(input.links, config, { runtime_path: runtimePath });
533
+ for (let index = 0; index < probes.length; index += 1) {
534
+ const probe = probes[index];
535
+ emitEvent(options.eventCallback, 'speedtest_probe_result', {
536
+ completed: index + 1,
537
+ total: input.links.length,
538
+ link: probe.link,
539
+ reachable: probe.reachable,
540
+ latency_ms: probe.latency_ms,
541
+ error: probe.error ?? ''
542
+ });
543
+ }
498
544
  const candidateLinks = selectSpeedtestCandidates(probes, config.max_download_candidates);
499
545
  const probeByLink = new Map(probes.map((probe) => [probe.link, probe]));
500
546
  const candidateSet = new Set(candidateLinks);
@@ -533,64 +579,7 @@ async function speedtestInNode(input, options) {
533
579
  results.push(...testedResults);
534
580
  return results;
535
581
  }
536
- export function selectPipelineStageBackend(stage, env = process.env) {
537
- void stage;
538
- void env;
539
- return 'node';
540
- }
541
- async function defaultResolvePythonCli(env) {
542
- // @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
543
- const runner = await import('../../lib/runner.mjs');
544
- return runner.resolveOrInstallPythonCli({ env });
545
- }
546
- function pythonCommandFor(resolved) {
547
- const command = resolved.command;
548
- const name = path.basename(command).toLowerCase();
549
- if (['autovpn', 'autovpn.exe'].includes(name)) {
550
- const executable = process.platform === 'win32' ? 'python.exe' : 'python';
551
- return path.join(path.dirname(command), executable);
552
- }
553
- return process.platform === 'win32' ? 'python.exe' : 'python3';
554
- }
555
- async function speedtestWithPython(input, options) {
556
- const env = mergeProjectEnv(options.cwd ?? process.cwd(), options.env ?? process.env);
557
- const resolved = options.resolvePythonCli ? await options.resolvePythonCli() : await defaultResolvePythonCli(env);
558
- const child = (options.spawn ?? defaultSpawn)(pythonCommandFor(resolved), ['-c', PYTHON_SPEEDTEST_HELPER], {
559
- cwd: options.cwd ?? process.cwd(),
560
- env,
561
- stdio: ['pipe', 'pipe', 'pipe']
562
- });
563
- let stdout = '';
564
- let stderr = '';
565
- child.stdout?.on('data', (chunk) => {
566
- stdout += String(chunk);
567
- });
568
- child.stderr?.on('data', (chunk) => {
569
- stderr += String(chunk);
570
- });
571
- const completion = new Promise((resolve, reject) => {
572
- child.on('error', reject);
573
- child.on('close', (code) => {
574
- if (code !== 0) {
575
- reject(new Error(`Python speedtest backend failed with exit code ${code}: ${stderr.trim()}`));
576
- return;
577
- }
578
- try {
579
- resolve(JSON.parse(stdout));
580
- }
581
- catch (error) {
582
- reject(new Error(`Python speedtest backend returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`));
583
- }
584
- });
585
- });
586
- child.stdin?.write(JSON.stringify(input));
587
- child.stdin?.end();
588
- return completion;
589
- }
590
582
  export async function speedtestLinksWithBackend(input, options = {}) {
591
- if (selectPipelineStageBackend('speedtest', options.env ?? process.env) === 'python') {
592
- return options.pythonSpeedtest ? options.pythonSpeedtest(input) : speedtestWithPython(input, options);
593
- }
594
583
  return speedtestInNode(input, options);
595
584
  }
596
585
  export async function probeSpeedtestLinksInNode(input, options = {}) {
@@ -5,8 +5,17 @@ export function resolveRuntimeRoot(candidate) {
5
5
  const resolved = fs.existsSync(absolute) ? fs.realpathSync(absolute) : absolute;
6
6
  let current = fs.existsSync(resolved) && fs.statSync(resolved).isFile() ? path.dirname(resolved) : resolved;
7
7
  while (true) {
8
- if (fs.existsSync(path.join(current, 'pyproject.toml'))) {
9
- return current;
8
+ const packagePath = path.join(current, 'package.json');
9
+ if (fs.existsSync(packagePath)) {
10
+ try {
11
+ const manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
12
+ if (manifest.name === 'vpn-subscription-automation') {
13
+ return current;
14
+ }
15
+ }
16
+ catch {
17
+ // Continue walking when an unrelated package manifest is invalid.
18
+ }
10
19
  }
11
20
  const parent = path.dirname(current);
12
21
  if (parent === current) {
@@ -0,0 +1,37 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const packagedWorkerTemplatePath = fileURLToPath(new URL('../templates/vmess_node.js', import.meta.url));
5
+ const packagedShareWorkerTemplatePath = fileURLToPath(new URL('../templates/share-worker/vpn.js', import.meta.url));
6
+ function expandHomePath(candidate, env) {
7
+ if (candidate === '~') {
8
+ return env.HOME || candidate;
9
+ }
10
+ if (candidate.startsWith('~/')) {
11
+ return env.HOME ? path.join(env.HOME, candidate.slice(2)) : candidate;
12
+ }
13
+ return candidate;
14
+ }
15
+ function firstExistingPath(candidates, message) {
16
+ const uniqueCandidates = [...new Set(candidates.filter(Boolean).map((candidate) => path.resolve(candidate)))];
17
+ const resolved = uniqueCandidates.find((candidate) => fs.existsSync(candidate));
18
+ if (resolved) {
19
+ return resolved;
20
+ }
21
+ throw new Error(`${message}; checked: ${uniqueCandidates.join(', ')}`);
22
+ }
23
+ export function resolveWorkerTemplatePath(projectRoot) {
24
+ return firstExistingPath([
25
+ path.join(projectRoot, 'templates', 'vmess_node.js'),
26
+ packagedWorkerTemplatePath
27
+ ], 'Worker template is missing');
28
+ }
29
+ export function resolveShareWorkerTemplatePath(projectRoot, env = process.env) {
30
+ return firstExistingPath([
31
+ expandHomePath(String(env.VPN_AUTOMATION_SHARE_WORKER_PATH ?? '').trim(), env),
32
+ path.join(projectRoot, 'electron', 'runtime', 'share-worker', 'vpn.js'),
33
+ path.join(projectRoot, 'templates', 'share-worker', 'vpn.js'),
34
+ packagedShareWorkerTemplatePath,
35
+ path.join(path.dirname(projectRoot), 'cloudflarevpn', 'edgetunnel', 'vpn.js')
36
+ ], 'Share Worker template is missing');
37
+ }