@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.
- package/README.md +7 -54
- package/dist/backend/select-backend.js +0 -3
- package/dist/cli/commands/index.js +4 -0
- package/dist/cli/main.js +7 -25
- package/dist/cli/native-commands.js +1 -0
- package/dist/doctor/checks.js +17 -2
- package/dist/jobs/commands.js +13 -0
- package/dist/jobs/process.js +92 -10
- package/dist/pipeline/availability.js +0 -96
- package/dist/pipeline/dedupe.js +1 -69
- package/dist/pipeline/deploy.js +165 -301
- package/dist/pipeline/extract.js +0 -80
- package/dist/pipeline/obfuscate.js +1 -80
- package/dist/pipeline/orchestrator.js +111 -130
- package/dist/pipeline/postprocess.js +1 -85
- package/dist/pipeline/proxy-runtime.js +5 -0
- package/dist/pipeline/render.js +1 -73
- package/dist/pipeline/speedtest.js +89 -100
- package/dist/runtime/paths.js +11 -2
- package/dist/runtime/templates.js +37 -0
- package/dist/templates/share-worker/vpn.js +4813 -0
- package/dist/templates/vmess_node.js +27 -0
- package/dist/web/renderer/i18n.js +1 -1
- package/package.json +3 -4
- package/dist/backend/python-backend.js +0 -242
- package/lib/cache.mjs +0 -14
- package/lib/errors.mjs +0 -11
- package/lib/install-python-cli.mjs +0 -63
- package/lib/runner.mjs +0 -113
|
@@ -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
|
|
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
|
-
|
|
77
|
+
let internallyTimedOut = false;
|
|
78
|
+
const timer = setTimeout(() => {
|
|
79
|
+
internallyTimedOut = true;
|
|
80
|
+
controller.abort();
|
|
81
|
+
}, timeoutMs);
|
|
98
82
|
try {
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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 = {}) {
|
package/dist/runtime/paths.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|
+
}
|