@swimmingliu/autovpn 1.6.9 → 1.8.0
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 +5 -0
- package/dist/backend/node-backend.js +7 -22
- package/dist/jobs/commands.js +38 -7
- package/dist/jobs/read.js +9 -22
- package/dist/pipeline/availability.js +38 -36
- package/dist/pipeline/country-codes.js +12 -0
- package/dist/pipeline/geoip.js +232 -0
- package/dist/pipeline/network-retry.js +54 -0
- package/dist/pipeline/orchestrator.js +1027 -844
- package/dist/pipeline/postprocess.js +2 -2
- package/dist/pipeline/proxy-runtime.js +130 -28
- package/dist/pipeline/run-store.js +654 -0
- package/dist/pipeline/speedtest.js +44 -66
- package/dist/pipeline/streaming-coordinator.js +154 -0
- package/dist/web/renderer/app.js +275 -52
- package/dist/web/renderer/assets/fonts/AutoVPNVisualTest-NotoSansSC.woff2 +0 -0
- package/dist/web/renderer/assets/fonts/OFL.txt +92 -0
- package/dist/web/renderer/assets/fonts/README.md +5 -0
- package/dist/web/renderer/i18n.js +1 -1
- package/dist/web/renderer/index.html +1 -1
- package/dist/web/renderer/state.js +8 -0
- package/dist/web/renderer/styles.css +410 -19
- package/dist/web/renderer/views.js +96 -51
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import net from 'node:net';
|
|
2
2
|
import tls from 'node:tls';
|
|
3
3
|
import { openMihomoRuntime as defaultOpenMihomoRuntime, probeMihomoProxyDelay as defaultProbeMihomoProxyDelay } from './proxy-runtime.js';
|
|
4
|
+
import { retryTransientNetwork } from './network-retry.js';
|
|
4
5
|
const PROBE_MAX_ATTEMPTS = 2;
|
|
5
6
|
export function aggregateSpeedMeasurements(values) {
|
|
6
7
|
if (values.length === 0) {
|
|
@@ -160,35 +161,8 @@ function requireOkResponse(response, allowedStatuses) {
|
|
|
160
161
|
throw new Error(`unexpected status ${status}`);
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
|
-
function isTransientProbeError(error) {
|
|
164
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
165
|
-
const code = typeof error === 'object' && error !== null && 'code' in error
|
|
166
|
-
? String(error.code ?? '')
|
|
167
|
-
: '';
|
|
168
|
-
const statusMatch = /(?:unexpected status|failed with status)\s+(\d{3})\b/i.exec(message);
|
|
169
|
-
if (statusMatch) {
|
|
170
|
-
const status = Number(statusMatch[1]);
|
|
171
|
-
return status >= 500 && status <= 599;
|
|
172
|
-
}
|
|
173
|
-
if (['AUTOVPN_INTERNAL_TIMEOUT', 'ECONNRESET', 'ETIMEDOUT', 'EPIPE', 'ECONNREFUSED', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT'].includes(code.toUpperCase())) {
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
return error instanceof TypeError && message.trim().toLowerCase() === 'fetch failed';
|
|
177
|
-
}
|
|
178
164
|
async function retryTransientProbe(operation) {
|
|
179
|
-
|
|
180
|
-
for (let attempt = 1; attempt <= PROBE_MAX_ATTEMPTS; attempt += 1) {
|
|
181
|
-
try {
|
|
182
|
-
return await operation();
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
lastError = error;
|
|
186
|
-
if (attempt >= PROBE_MAX_ATTEMPTS || !isTransientProbeError(error)) {
|
|
187
|
-
throw error;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
throw lastError;
|
|
165
|
+
return retryTransientNetwork(operation, { maxAttempts: PROBE_MAX_ATTEMPTS, delayMs: 0 });
|
|
192
166
|
}
|
|
193
167
|
function socketConnect(host, port, timeoutMs) {
|
|
194
168
|
return new Promise((resolve, reject) => {
|
|
@@ -359,8 +333,14 @@ export async function downloadUrlViaHttpProxy(url, proxyUrl, maxBytes, timeoutSe
|
|
|
359
333
|
rejectUnauthorized: false
|
|
360
334
|
});
|
|
361
335
|
await new Promise((resolve, reject) => {
|
|
362
|
-
|
|
363
|
-
|
|
336
|
+
const timer = setTimeout(() => {
|
|
337
|
+
secureSocket.destroy();
|
|
338
|
+
const error = new Error(`TLS handshake timed out after ${timeoutMs}ms`);
|
|
339
|
+
error.code = 'AUTOVPN_INTERNAL_TIMEOUT';
|
|
340
|
+
reject(error);
|
|
341
|
+
}, timeoutMs);
|
|
342
|
+
secureSocket.once('secureConnect', () => { clearTimeout(timer); resolve(); });
|
|
343
|
+
secureSocket.once('error', (error) => { clearTimeout(timer); reject(error); });
|
|
364
344
|
});
|
|
365
345
|
secureSocket.write([
|
|
366
346
|
`GET ${requestPath} HTTP/1.1`,
|
|
@@ -461,43 +441,44 @@ async function testLinkDirect(link, config, options) {
|
|
|
461
441
|
async function testLinkMihomo(link, config, runtimePath, options) {
|
|
462
442
|
const openRuntime = options.openMihomoRuntime ?? defaultOpenMihomoRuntime;
|
|
463
443
|
const downloadViaProxy = options.downloadUrlViaHttpProxy ?? downloadUrlViaHttpProxy;
|
|
464
|
-
let runtime;
|
|
465
444
|
try {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
startupWaitSeconds: config.startup_wait_seconds,
|
|
469
|
-
env: options.env
|
|
470
|
-
});
|
|
471
|
-
const now = options.now ?? defaultNow;
|
|
472
|
-
const speedValues = [];
|
|
473
|
-
const failures = [];
|
|
474
|
-
for (const url of config.urls) {
|
|
475
|
-
const started = now();
|
|
445
|
+
return await retryTransientNetwork(async () => {
|
|
446
|
+
let runtime;
|
|
476
447
|
try {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
448
|
+
runtime = await openRuntime(link, {
|
|
449
|
+
runtimePath,
|
|
450
|
+
startupWaitSeconds: config.startup_wait_seconds,
|
|
451
|
+
env: options.env
|
|
452
|
+
});
|
|
453
|
+
const now = options.now ?? defaultNow;
|
|
454
|
+
const speedValues = [];
|
|
455
|
+
const failures = [];
|
|
456
|
+
let lastFailure;
|
|
457
|
+
for (const url of config.urls) {
|
|
458
|
+
try {
|
|
459
|
+
const measurement = await retryTransientNetwork(async () => {
|
|
460
|
+
const started = now();
|
|
461
|
+
const total = await downloadViaProxy(url, runtime.proxies.http, Math.max(1, Number(config.max_download_bytes)), config.timeout_seconds);
|
|
462
|
+
return { total, elapsedSeconds: Math.max((now() - started) / 1000, 0.001) };
|
|
463
|
+
});
|
|
464
|
+
speedValues.push(measurement.total / measurement.elapsedSeconds / 1024 / 1024);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
lastFailure = error;
|
|
468
|
+
failures.push(`${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (speedValues.length === 0) {
|
|
472
|
+
if (lastFailure)
|
|
473
|
+
throw lastFailure;
|
|
474
|
+
return { link, reachable: false, average_download_mb_s: 0, latency_ms: 0, error: failures.join('; ') || 'all speed test urls failed' };
|
|
475
|
+
}
|
|
476
|
+
return { link, reachable: true, average_download_mb_s: aggregateSpeedMeasurements(speedValues), latency_ms: 0, error: failures.join('; ') };
|
|
480
477
|
}
|
|
481
|
-
|
|
482
|
-
|
|
478
|
+
finally {
|
|
479
|
+
await runtime?.close();
|
|
483
480
|
}
|
|
484
|
-
}
|
|
485
|
-
if (speedValues.length === 0) {
|
|
486
|
-
return {
|
|
487
|
-
link,
|
|
488
|
-
reachable: false,
|
|
489
|
-
average_download_mb_s: 0,
|
|
490
|
-
latency_ms: 0,
|
|
491
|
-
error: failures.join('; ') || 'all speed test urls failed'
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
return {
|
|
495
|
-
link,
|
|
496
|
-
reachable: true,
|
|
497
|
-
average_download_mb_s: aggregateSpeedMeasurements(speedValues),
|
|
498
|
-
latency_ms: 0,
|
|
499
|
-
error: failures.join('; ')
|
|
500
|
-
};
|
|
481
|
+
});
|
|
501
482
|
}
|
|
502
483
|
catch (error) {
|
|
503
484
|
return {
|
|
@@ -508,9 +489,6 @@ async function testLinkMihomo(link, config, runtimePath, options) {
|
|
|
508
489
|
error: error instanceof Error ? error.message : String(error)
|
|
509
490
|
};
|
|
510
491
|
}
|
|
511
|
-
finally {
|
|
512
|
-
await runtime?.close();
|
|
513
|
-
}
|
|
514
492
|
}
|
|
515
493
|
async function speedtestInNode(input, options) {
|
|
516
494
|
if (input.links.length === 0) {
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export class AsyncPermitPool {
|
|
2
|
+
available;
|
|
3
|
+
waiters = [];
|
|
4
|
+
constructor(limit) {
|
|
5
|
+
if (!Number.isInteger(limit) || limit <= 0)
|
|
6
|
+
throw new RangeError('permit limit must be a positive integer');
|
|
7
|
+
this.available = limit;
|
|
8
|
+
}
|
|
9
|
+
async run(operation) {
|
|
10
|
+
await this.acquire();
|
|
11
|
+
try {
|
|
12
|
+
return await operation();
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
this.release();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async acquire() {
|
|
19
|
+
if (this.available > 0) {
|
|
20
|
+
this.available -= 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
24
|
+
}
|
|
25
|
+
release() {
|
|
26
|
+
const waiter = this.waiters.shift();
|
|
27
|
+
if (waiter)
|
|
28
|
+
waiter();
|
|
29
|
+
else
|
|
30
|
+
this.available += 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class BoundedWorkerPool {
|
|
34
|
+
concurrency;
|
|
35
|
+
limit;
|
|
36
|
+
worker;
|
|
37
|
+
queue = [];
|
|
38
|
+
admissionWaiters = [];
|
|
39
|
+
drainWaiters = [];
|
|
40
|
+
active = 0;
|
|
41
|
+
closed = false;
|
|
42
|
+
failure;
|
|
43
|
+
failed = false;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {
|
|
46
|
+
throw new RangeError('concurrency must be a positive integer');
|
|
47
|
+
}
|
|
48
|
+
if (!Number.isInteger(options.capacity) || options.capacity < 0) {
|
|
49
|
+
throw new RangeError('capacity must be a non-negative integer');
|
|
50
|
+
}
|
|
51
|
+
this.concurrency = options.concurrency;
|
|
52
|
+
this.limit = options.concurrency + options.capacity;
|
|
53
|
+
this.worker = options.worker;
|
|
54
|
+
}
|
|
55
|
+
submit(item) {
|
|
56
|
+
if (this.failed) {
|
|
57
|
+
return Promise.reject(this.failure);
|
|
58
|
+
}
|
|
59
|
+
if (this.closed) {
|
|
60
|
+
return Promise.reject(new Error('worker pool is closed'));
|
|
61
|
+
}
|
|
62
|
+
if (this.active + this.queue.length < this.limit) {
|
|
63
|
+
this.accept(item);
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
}
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
this.admissionWaiters.push({ item, resolve, reject });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
close() {
|
|
71
|
+
this.closed = true;
|
|
72
|
+
this.settleDrainWaitersIfIdle();
|
|
73
|
+
}
|
|
74
|
+
drain() {
|
|
75
|
+
if (this.isIdle()) {
|
|
76
|
+
return this.failed ? Promise.reject(this.failure) : Promise.resolve();
|
|
77
|
+
}
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
this.drainWaiters.push({ resolve, reject });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
abort(error) {
|
|
83
|
+
if (!this.failed) {
|
|
84
|
+
this.failed = true;
|
|
85
|
+
this.failure = error;
|
|
86
|
+
}
|
|
87
|
+
this.closed = true;
|
|
88
|
+
this.queue.length = 0;
|
|
89
|
+
this.rejectAdmissionWaiters(this.failure);
|
|
90
|
+
this.settleDrainWaitersIfIdle();
|
|
91
|
+
}
|
|
92
|
+
accept(item) {
|
|
93
|
+
if (this.active < this.concurrency) {
|
|
94
|
+
this.start(item);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.queue.push(item);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
start(item) {
|
|
101
|
+
this.active += 1;
|
|
102
|
+
Promise.resolve()
|
|
103
|
+
.then(() => this.worker(item))
|
|
104
|
+
.catch((error) => this.recordFailure(error))
|
|
105
|
+
.finally(() => {
|
|
106
|
+
this.active -= 1;
|
|
107
|
+
if (!this.failed) {
|
|
108
|
+
this.fillAvailableSlots();
|
|
109
|
+
}
|
|
110
|
+
this.settleDrainWaitersIfIdle();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
fillAvailableSlots() {
|
|
114
|
+
while (this.active < this.concurrency && this.queue.length > 0) {
|
|
115
|
+
this.start(this.queue.shift());
|
|
116
|
+
}
|
|
117
|
+
while (this.active + this.queue.length < this.limit && this.admissionWaiters.length > 0) {
|
|
118
|
+
const admission = this.admissionWaiters.shift();
|
|
119
|
+
this.accept(admission.item);
|
|
120
|
+
admission.resolve();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
recordFailure(error) {
|
|
124
|
+
if (this.failed) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.failed = true;
|
|
128
|
+
this.failure = error;
|
|
129
|
+
this.closed = true;
|
|
130
|
+
this.queue.length = 0;
|
|
131
|
+
this.rejectAdmissionWaiters(error);
|
|
132
|
+
}
|
|
133
|
+
rejectAdmissionWaiters(error) {
|
|
134
|
+
for (const admission of this.admissionWaiters.splice(0)) {
|
|
135
|
+
admission.reject(error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
isIdle() {
|
|
139
|
+
return this.active === 0 && this.queue.length === 0;
|
|
140
|
+
}
|
|
141
|
+
settleDrainWaitersIfIdle() {
|
|
142
|
+
if (!this.isIdle()) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (const waiter of this.drainWaiters.splice(0)) {
|
|
146
|
+
if (this.failed) {
|
|
147
|
+
waiter.reject(this.failure);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
waiter.resolve();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|