@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,3 +1,4 @@
|
|
|
1
|
+
import { isIsoAlpha2CountryCode } from './country-codes.js';
|
|
1
2
|
const EMOJI_MAP = {
|
|
2
3
|
AE: 'π¦πͺ',
|
|
3
4
|
AR: 'π¦π·',
|
|
@@ -37,7 +38,6 @@ const EMOJI_MAP = {
|
|
|
37
38
|
ZA: 'πΏπ¦'
|
|
38
39
|
};
|
|
39
40
|
const FALLBACK_COUNTRY_CODE = 'US';
|
|
40
|
-
const UNKNOWN_COUNTRY_CODE = 'ZZ';
|
|
41
41
|
const DEFAULT_FILTERS = {
|
|
42
42
|
excluded_country_codes: ['CN'],
|
|
43
43
|
per_country_limit: {}
|
|
@@ -55,7 +55,7 @@ export function generateVmessLink(payload) {
|
|
|
55
55
|
}
|
|
56
56
|
export function normalizeCountryCode(countryCode) {
|
|
57
57
|
const normalized = String(countryCode || '').trim().toUpperCase();
|
|
58
|
-
if (
|
|
58
|
+
if (!isIsoAlpha2CountryCode(normalized)) {
|
|
59
59
|
return FALLBACK_COUNTRY_CODE;
|
|
60
60
|
}
|
|
61
61
|
return normalized;
|
|
@@ -14,6 +14,7 @@ export const PROXY_ENV_KEYS = [
|
|
|
14
14
|
'no_proxy'
|
|
15
15
|
];
|
|
16
16
|
const MIHOMO_DELAY_TIMEOUT_MAX_MS = 30_000;
|
|
17
|
+
const reservedAutomaticPorts = new Set();
|
|
17
18
|
function padBase64(encoded) {
|
|
18
19
|
return encoded + '='.repeat((4 - (encoded.length % 4)) % 4);
|
|
19
20
|
}
|
|
@@ -85,16 +86,45 @@ function requireOkResponse(response, context) {
|
|
|
85
86
|
throw new Error(`${context} failed with status ${response.status ?? 0} ${response.statusText ?? ''}`.trim());
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
|
-
|
|
89
|
+
async function fetchControllerWithTimeout(fetchImpl, url, init, timeoutSeconds) {
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
let timedOut = false;
|
|
92
|
+
const timeoutMs = Math.max(1, Math.trunc(timeoutSeconds * 1000));
|
|
93
|
+
let timer;
|
|
94
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
95
|
+
timer = setTimeout(() => {
|
|
96
|
+
timedOut = true;
|
|
97
|
+
const timeoutError = new Error(`mihomo controller request timed out after ${timeoutMs}ms`);
|
|
98
|
+
timeoutError.code = 'AUTOVPN_INTERNAL_TIMEOUT';
|
|
99
|
+
reject(timeoutError);
|
|
100
|
+
controller.abort();
|
|
101
|
+
}, timeoutMs);
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
return await Promise.race([fetchImpl(url, { ...init, signal: controller.signal }), timeout]);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (!timedOut || (error instanceof Error && error.code === 'AUTOVPN_INTERNAL_TIMEOUT'))
|
|
108
|
+
throw error;
|
|
109
|
+
const timeoutError = new Error(`mihomo controller request timed out after ${timeoutMs}ms`, { cause: error });
|
|
110
|
+
timeoutError.code = 'AUTOVPN_INTERNAL_TIMEOUT';
|
|
111
|
+
throw timeoutError;
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
if (timer)
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export async function selectMihomoProxy(controllerUrl, proxyName, timeoutSeconds, options = {}) {
|
|
89
119
|
if (!controllerUrl) {
|
|
90
120
|
return;
|
|
91
121
|
}
|
|
92
122
|
const fetchImpl = requireFetch(options.fetch);
|
|
93
|
-
const response = await fetchImpl
|
|
123
|
+
const response = await fetchControllerWithTimeout(fetchImpl, `${controllerUrl}/proxies/GLOBAL`, {
|
|
94
124
|
method: 'PUT',
|
|
95
125
|
headers: { 'content-type': 'application/json' },
|
|
96
126
|
body: JSON.stringify({ name: proxyName })
|
|
97
|
-
});
|
|
127
|
+
}, timeoutSeconds);
|
|
98
128
|
requireOkResponse(response, 'mihomo proxy selection');
|
|
99
129
|
}
|
|
100
130
|
export async function probeMihomoProxyDelay(controllerUrl, proxyName, probeUrl, timeoutSeconds, options = {}) {
|
|
@@ -103,7 +133,7 @@ export async function probeMihomoProxyDelay(controllerUrl, proxyName, probeUrl,
|
|
|
103
133
|
const timeoutMs = Math.min(MIHOMO_DELAY_TIMEOUT_MAX_MS, Math.max(1, Math.trunc(timeoutSeconds * 1000)));
|
|
104
134
|
url.searchParams.set('timeout', String(timeoutMs));
|
|
105
135
|
url.searchParams.set('url', probeUrl);
|
|
106
|
-
const response = await fetchImpl
|
|
136
|
+
const response = await fetchControllerWithTimeout(fetchImpl, url.toString(), { method: 'GET' }, timeoutSeconds);
|
|
107
137
|
requireOkResponse(response, 'mihomo proxy delay probe');
|
|
108
138
|
const payload = await response.json?.();
|
|
109
139
|
const delay = Number(payload?.delay ?? -1);
|
|
@@ -132,6 +162,19 @@ async function findFreePort() {
|
|
|
132
162
|
});
|
|
133
163
|
});
|
|
134
164
|
}
|
|
165
|
+
async function reserveAutomaticPort(allocatePort) {
|
|
166
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
167
|
+
const port = await allocatePort();
|
|
168
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
169
|
+
throw new Error(`invalid automatically allocated port: ${port}`);
|
|
170
|
+
}
|
|
171
|
+
if (!reservedAutomaticPorts.has(port)) {
|
|
172
|
+
reservedAutomaticPorts.add(port);
|
|
173
|
+
return port;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
throw new Error('unable to reserve a unique local port after 100 attempts');
|
|
177
|
+
}
|
|
135
178
|
async function canConnectToPort(port) {
|
|
136
179
|
return await new Promise((resolve) => {
|
|
137
180
|
const socket = net.createConnection({ host: '127.0.0.1', port });
|
|
@@ -164,18 +207,29 @@ async function closeChildProcess(process) {
|
|
|
164
207
|
if (process.exitCode !== null || process.killed) {
|
|
165
208
|
return;
|
|
166
209
|
}
|
|
167
|
-
await new Promise((resolve) => {
|
|
210
|
+
await new Promise((resolve, reject) => {
|
|
168
211
|
const timer = setTimeout(() => {
|
|
169
|
-
|
|
170
|
-
process.
|
|
212
|
+
try {
|
|
213
|
+
if (process.exitCode === null && !process.killed) {
|
|
214
|
+
process.kill('SIGKILL');
|
|
215
|
+
}
|
|
216
|
+
resolve();
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
reject(error);
|
|
171
220
|
}
|
|
172
|
-
resolve();
|
|
173
221
|
}, 2000);
|
|
174
222
|
process.once('close', () => {
|
|
175
223
|
clearTimeout(timer);
|
|
176
224
|
resolve();
|
|
177
225
|
});
|
|
178
|
-
|
|
226
|
+
try {
|
|
227
|
+
process.kill('SIGTERM');
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
reject(error);
|
|
232
|
+
}
|
|
179
233
|
});
|
|
180
234
|
}
|
|
181
235
|
async function fileExists(candidate) {
|
|
@@ -227,24 +281,54 @@ async function resolveMihomoCommand(runtimePath, env = process.env) {
|
|
|
227
281
|
}
|
|
228
282
|
export async function openMihomoRuntime(link, options = {}) {
|
|
229
283
|
const startupWaitSeconds = Number(options.startupWaitSeconds ?? 1);
|
|
284
|
+
const payload = parseVmessLink(link);
|
|
285
|
+
const automaticPorts = [];
|
|
286
|
+
const allocatePort = options.allocatePort ?? findFreePort;
|
|
230
287
|
const mixedPort = Number.isInteger(options.mixedPort) && Number(options.mixedPort) > 0
|
|
231
288
|
? Number(options.mixedPort)
|
|
232
|
-
: await
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
289
|
+
: await reserveAutomaticPort(allocatePort);
|
|
290
|
+
if (!(Number.isInteger(options.mixedPort) && Number(options.mixedPort) > 0))
|
|
291
|
+
automaticPorts.push(mixedPort);
|
|
292
|
+
let controllerPort;
|
|
293
|
+
try {
|
|
294
|
+
controllerPort = Number.isInteger(options.controllerPort) && Number(options.controllerPort) > 0
|
|
295
|
+
? Number(options.controllerPort)
|
|
296
|
+
: await reserveAutomaticPort(allocatePort);
|
|
297
|
+
if (!(Number.isInteger(options.controllerPort) && Number(options.controllerPort) > 0))
|
|
298
|
+
automaticPorts.push(controllerPort);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
for (const port of automaticPorts)
|
|
302
|
+
reservedAutomaticPorts.delete(port);
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
236
305
|
const proxyName = 'runtime-node';
|
|
237
306
|
const controllerUrl = `http://127.0.0.1:${controllerPort}`;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
307
|
+
let tempDir = '';
|
|
308
|
+
let configPath = '';
|
|
309
|
+
let child;
|
|
310
|
+
try {
|
|
311
|
+
const config = buildMihomoRuntimeConfig(payload, { mixedPort, controllerPort });
|
|
312
|
+
tempDir = await mkdtemp(path.join(os.tmpdir(), 'autovpn-mihomo-'));
|
|
313
|
+
configPath = path.join(tempDir, 'config.json');
|
|
314
|
+
await writeFile(configPath, JSON.stringify(config), 'utf8');
|
|
315
|
+
const command = await resolveMihomoCommand(options.runtimePath, options.env ?? process.env);
|
|
316
|
+
child = (options.spawn ?? defaultSpawn)(command, ['-f', configPath], {
|
|
317
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
318
|
+
env: stripProxyEnv(options.env ?? process.env)
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
try {
|
|
323
|
+
if (tempDir)
|
|
324
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
for (const port of automaticPorts)
|
|
328
|
+
reservedAutomaticPorts.delete(port);
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
248
332
|
let rejectStartupError;
|
|
249
333
|
const startupError = new Promise((_resolve, reject) => {
|
|
250
334
|
rejectStartupError = reject;
|
|
@@ -253,25 +337,43 @@ export async function openMihomoRuntime(link, options = {}) {
|
|
|
253
337
|
rejectStartupError?.(error);
|
|
254
338
|
};
|
|
255
339
|
child.once('error', onStartupError);
|
|
340
|
+
let rejectStartupExit;
|
|
341
|
+
const startupExit = new Promise((_resolve, reject) => {
|
|
342
|
+
rejectStartupExit = reject;
|
|
343
|
+
});
|
|
344
|
+
const onStartupExit = (code, signal) => {
|
|
345
|
+
const error = new Error(`mihomo exited during startup with code ${code ?? 'unknown'}${signal ? ` signal ${signal}` : ''}`);
|
|
346
|
+
error.code = 'AUTOVPN_INTERNAL_TIMEOUT';
|
|
347
|
+
rejectStartupExit?.(error);
|
|
348
|
+
};
|
|
349
|
+
child.once('exit', onStartupExit);
|
|
256
350
|
let closed = false;
|
|
257
351
|
const close = async () => {
|
|
258
352
|
if (closed) {
|
|
259
353
|
return;
|
|
260
354
|
}
|
|
261
355
|
closed = true;
|
|
262
|
-
|
|
263
|
-
|
|
356
|
+
try {
|
|
357
|
+
await closeChildProcess(child);
|
|
358
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
for (const port of automaticPorts)
|
|
362
|
+
reservedAutomaticPorts.delete(port);
|
|
363
|
+
}
|
|
264
364
|
};
|
|
265
365
|
try {
|
|
266
366
|
const waitForPort = options.waitForPort ?? defaultWaitForPort;
|
|
267
|
-
await Promise.race([waitForPort(mixedPort, startupWaitSeconds + 4), startupError]);
|
|
268
|
-
await Promise.race([waitForPort(controllerPort, startupWaitSeconds + 4), startupError]);
|
|
269
|
-
await Promise.race([(options.selectProxy ?? ((url, name, timeoutSeconds) => selectMihomoProxy(url, name, timeoutSeconds)))(controllerUrl, proxyName, startupWaitSeconds + 4), startupError]);
|
|
367
|
+
await Promise.race([waitForPort(mixedPort, startupWaitSeconds + 4), startupError, startupExit]);
|
|
368
|
+
await Promise.race([waitForPort(controllerPort, startupWaitSeconds + 4), startupError, startupExit]);
|
|
369
|
+
await Promise.race([(options.selectProxy ?? ((url, name, timeoutSeconds) => selectMihomoProxy(url, name, timeoutSeconds)))(controllerUrl, proxyName, startupWaitSeconds + 4), startupError, startupExit]);
|
|
270
370
|
child.off('error', onStartupError);
|
|
371
|
+
child.off('exit', onStartupExit);
|
|
271
372
|
child.on('error', () => { });
|
|
272
373
|
}
|
|
273
374
|
catch (error) {
|
|
274
375
|
child.off('error', onStartupError);
|
|
376
|
+
child.off('exit', onStartupExit);
|
|
275
377
|
child.on('error', () => { });
|
|
276
378
|
await close();
|
|
277
379
|
if (error instanceof Error
|