cfpilot-scanner 0.2.0 → 0.4.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 +81 -2
- package/dist/cli.js +1043 -58
- package/package.json +41 -41
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
2
8
|
|
|
3
9
|
// src/cli.ts
|
|
4
10
|
import { readFileSync } from "node:fs";
|
|
@@ -89,6 +95,59 @@ var BUILTIN_IPV4_CIDRS = [
|
|
|
89
95
|
"172.64.0.0/13",
|
|
90
96
|
"131.0.72.0/22"
|
|
91
97
|
];
|
|
98
|
+
var IPV4_CIDR_WEIGHTS = {
|
|
99
|
+
"104.16.0.0/13": 1,
|
|
100
|
+
// 实测 8/12
|
|
101
|
+
"104.24.0.0/14": 0.9,
|
|
102
|
+
// 实测 7/12
|
|
103
|
+
"103.21.244.0/22": 0.3,
|
|
104
|
+
"103.31.4.0/22": 0.3,
|
|
105
|
+
"190.93.240.0/20": 0.3,
|
|
106
|
+
"188.114.96.0/20": 0.3,
|
|
107
|
+
"141.101.64.0/18": 0.2,
|
|
108
|
+
"162.158.0.0/16": 0.2,
|
|
109
|
+
// 162.158.x.x:边缘节点,部分可用
|
|
110
|
+
// 162.159.x.x 是 Cloudflare 的管理 / API 网段,真机实测**多数**返回
|
|
111
|
+
// HTTP 403(反爬风控),但**并非全部**——用户实测 162.159.237.59 可用
|
|
112
|
+
// (2.8 MB/s),本地复测同样通过(SJC)。
|
|
113
|
+
//
|
|
114
|
+
// 因此**不能压到 0**(会错杀可用 IP),给一个很低的权重:
|
|
115
|
+
// 仍能采到少量候选,但不让大量 403 IP 占据阶段二额度。
|
|
116
|
+
//
|
|
117
|
+
// ⚠️ 教训:权重粒度为 /16,但实际可用性可能细到单个 IP。
|
|
118
|
+
// 实测样本只有 9 个就下「完全不可用」的结论过于草率。
|
|
119
|
+
"162.159.0.0/16": 0.05,
|
|
120
|
+
"172.64.0.0/13": 0.1,
|
|
121
|
+
// 实测 0/12 —— 不采样
|
|
122
|
+
"173.245.48.0/20": 0,
|
|
123
|
+
"103.22.200.0/22": 0,
|
|
124
|
+
"108.162.192.0/18": 0,
|
|
125
|
+
"197.234.240.0/22": 0,
|
|
126
|
+
"198.41.128.0/17": 0,
|
|
127
|
+
"131.0.72.0/22": 0
|
|
128
|
+
};
|
|
129
|
+
var DEFAULT_CIDR_WEIGHT = 0.2;
|
|
130
|
+
function cidrWeight(cidr) {
|
|
131
|
+
const normalized = cidr.trim();
|
|
132
|
+
if (normalized in IPV4_CIDR_WEIGHTS) return IPV4_CIDR_WEIGHTS[normalized];
|
|
133
|
+
const parsed = parseCidr(normalized);
|
|
134
|
+
if (!parsed || parsed.family !== 4) return DEFAULT_CIDR_WEIGHT;
|
|
135
|
+
let bestPrefix = -1;
|
|
136
|
+
let bestWeight = DEFAULT_CIDR_WEIGHT;
|
|
137
|
+
for (const [tableCidr, weight] of Object.entries(IPV4_CIDR_WEIGHTS)) {
|
|
138
|
+
const tableParsed = parseCidr(tableCidr);
|
|
139
|
+
if (!tableParsed || tableParsed.family !== 4) continue;
|
|
140
|
+
if (tableParsed.prefix > parsed.prefix) continue;
|
|
141
|
+
const parsedNorm = parsed.base >> BigInt(32 - tableParsed.prefix);
|
|
142
|
+
const tableNorm = tableParsed.base >> BigInt(32 - tableParsed.prefix);
|
|
143
|
+
if (parsedNorm !== tableNorm) continue;
|
|
144
|
+
if (tableParsed.prefix > bestPrefix) {
|
|
145
|
+
bestPrefix = tableParsed.prefix;
|
|
146
|
+
bestWeight = weight;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return bestWeight;
|
|
150
|
+
}
|
|
92
151
|
var BUILTIN_IPV6_CIDRS = [
|
|
93
152
|
"2400:cb00::/32",
|
|
94
153
|
"2606:4700::/32",
|
|
@@ -137,7 +196,7 @@ function parseCidr(cidr) {
|
|
|
137
196
|
const hostBits2 = 128 - prefix;
|
|
138
197
|
const size2 = 1n << BigInt(hostBits2);
|
|
139
198
|
const mask2 = hostBits2 === 0 ? 0n : (1n << BigInt(hostBits2)) - 1n;
|
|
140
|
-
return { family: 6, base: base & ~mask2, prefix, size: size2 };
|
|
199
|
+
return { family: 6, base: base & ~mask2, prefix, size: size2, cidr: cidr.trim() };
|
|
141
200
|
}
|
|
142
201
|
if (prefix < 0 || prefix > 32) return null;
|
|
143
202
|
const value = ipv4ToBigInt(addr);
|
|
@@ -145,7 +204,7 @@ function parseCidr(cidr) {
|
|
|
145
204
|
const hostBits = 32 - prefix;
|
|
146
205
|
const size = 1n << BigInt(hostBits);
|
|
147
206
|
const mask = hostBits === 0 ? 0n : (1n << BigInt(hostBits)) - 1n;
|
|
148
|
-
return { family: 4, base: value & ~mask, prefix, size };
|
|
207
|
+
return { family: 4, base: value & ~mask, prefix, size, cidr: cidr.trim() };
|
|
149
208
|
}
|
|
150
209
|
function ipv4ToBigInt(addr) {
|
|
151
210
|
const parts = addr.split(".");
|
|
@@ -207,30 +266,114 @@ function bigIntToIpv6(value) {
|
|
|
207
266
|
const tail = groups.slice(bestStart + bestLen).join(":");
|
|
208
267
|
return `${head}::${tail}`;
|
|
209
268
|
}
|
|
269
|
+
function splitByWeight(cidr) {
|
|
270
|
+
if (cidr.family !== 4) return [cidr];
|
|
271
|
+
const out = [];
|
|
272
|
+
const stack = [{ c: cidr }];
|
|
273
|
+
while (stack.length > 0) {
|
|
274
|
+
const item = stack.pop();
|
|
275
|
+
const c = item.c;
|
|
276
|
+
const weight = item.known ?? cidrWeight(c.cidr);
|
|
277
|
+
if (c.prefix >= 32) {
|
|
278
|
+
out.push(c);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const half = 1n << BigInt(32 - c.prefix - 1);
|
|
282
|
+
const mk = (base) => {
|
|
283
|
+
const sub = { ...c, prefix: c.prefix + 1, size: half, base };
|
|
284
|
+
return { ...sub, cidr: `${bigIntToIpv4(base)}/${c.prefix + 1}` };
|
|
285
|
+
};
|
|
286
|
+
const left = mk(c.base);
|
|
287
|
+
const right = mk(c.base + half);
|
|
288
|
+
const lw = cidrWeight(left.cidr);
|
|
289
|
+
const rw = cidrWeight(right.cidr);
|
|
290
|
+
if (lw === rw) {
|
|
291
|
+
out.push(c);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
stack.push({ c: left, known: lw }, { c: right, known: rw });
|
|
295
|
+
}
|
|
296
|
+
return out.sort((a, b) => a.base < b.base ? -1 : a.base > b.base ? 1 : 0);
|
|
297
|
+
}
|
|
210
298
|
function expandCidrs(cidrs, options = {}) {
|
|
211
299
|
const sample = options.sample ?? 2e4;
|
|
212
300
|
const random = makeRandom(options.seed ?? 24301);
|
|
213
|
-
const
|
|
301
|
+
const parsedRaw = cidrs.map(parseCidr).filter((c) => c !== null);
|
|
302
|
+
if (parsedRaw.length === 0) return [];
|
|
303
|
+
const parsed = parsedRaw.flatMap(splitByWeight);
|
|
214
304
|
if (parsed.length === 0) return [];
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (sample <= 0 || cidr.size <= 2000n) {
|
|
305
|
+
if (sample <= 0) {
|
|
306
|
+
const all = [];
|
|
307
|
+
for (const cidr of parsed) {
|
|
219
308
|
for (let i = 0n; i < cidr.size; i++) {
|
|
220
309
|
const value = cidr.base + i;
|
|
221
|
-
|
|
310
|
+
all.push(cidr.family === 4 ? bigIntToIpv4(value) : bigIntToIpv6(value));
|
|
222
311
|
}
|
|
223
|
-
continue;
|
|
224
312
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
313
|
+
return all;
|
|
314
|
+
}
|
|
315
|
+
const minPerCidr = sample <= 1e3 ? 1 : 64;
|
|
316
|
+
const out = [];
|
|
317
|
+
for (const family of [4, 6]) {
|
|
318
|
+
const group = parsed.filter((c) => c.family === family);
|
|
319
|
+
if (group.length === 0) continue;
|
|
320
|
+
const hasOtherFamily = parsed.some((c) => c.family !== family);
|
|
321
|
+
const familyBudget = Math.max(1, Math.round(hasOtherFamily ? sample / 2 : sample));
|
|
322
|
+
const effectiveSize = (c) => {
|
|
323
|
+
if (c.family !== 4) return c.size;
|
|
324
|
+
const weight = cidrWeight(c.cidr);
|
|
325
|
+
if (weight <= 0) return 0n;
|
|
326
|
+
const scaled = Number(c.size) * weight;
|
|
327
|
+
return scaled >= 1 ? BigInt(Math.floor(scaled)) : 0n;
|
|
328
|
+
};
|
|
329
|
+
const rawTotal = group.reduce((sum, c) => sum + effectiveSize(c), 0n);
|
|
330
|
+
const allZero = rawTotal === 0n;
|
|
331
|
+
let familyTotal = rawTotal;
|
|
332
|
+
let allocOf = effectiveSize;
|
|
333
|
+
if (allZero) {
|
|
334
|
+
familyTotal = group.reduce((sum, c) => sum + c.size, 0n);
|
|
335
|
+
allocOf = (c) => c.size;
|
|
336
|
+
}
|
|
337
|
+
if (familyTotal === 0n) continue;
|
|
338
|
+
const minForCidr = (c) => {
|
|
339
|
+
if (c.family !== 4) return minPerCidr;
|
|
340
|
+
const w = cidrWeight(c.cidr);
|
|
341
|
+
if (w >= 1) return minPerCidr;
|
|
342
|
+
return Math.max(1, Math.round(minPerCidr * w));
|
|
343
|
+
};
|
|
344
|
+
const plans = group.map((cidr) => {
|
|
345
|
+
const ratio = Number(allocOf(cidr) * BigInt(familyBudget) / familyTotal);
|
|
346
|
+
const wanted = Math.max(minForCidr(cidr), Math.min(2e4, ratio));
|
|
347
|
+
return { cidr, count: Math.min(Number(cidr.size), wanted) };
|
|
348
|
+
});
|
|
349
|
+
const usablePlans = allZero ? plans : plans.filter((p) => effectiveSize(p.cidr) > 0n);
|
|
350
|
+
let familySum = usablePlans.reduce((sum, p) => sum + p.count, 0);
|
|
351
|
+
while (familySum > familyBudget) {
|
|
352
|
+
let target = null;
|
|
353
|
+
for (const plan of usablePlans) {
|
|
354
|
+
if (plan.count > 1 && (!target || plan.count > target.count)) target = plan;
|
|
355
|
+
}
|
|
356
|
+
if (!target) break;
|
|
357
|
+
target.count -= 1;
|
|
358
|
+
familySum -= 1;
|
|
359
|
+
}
|
|
360
|
+
for (const { cidr, count } of usablePlans) {
|
|
361
|
+
if (count <= 0) continue;
|
|
362
|
+
if (count >= Number(cidr.size)) {
|
|
363
|
+
for (let i = 0n; i < cidr.size; i++) {
|
|
364
|
+
const value = cidr.base + i;
|
|
365
|
+
out.push(family === 4 ? bigIntToIpv4(value) : bigIntToIpv6(value));
|
|
366
|
+
}
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const step = Math.floor(Number(cidr.size) / count);
|
|
370
|
+
const offset = Math.floor(random() * step);
|
|
371
|
+
for (let i = 0; i < count; i++) {
|
|
372
|
+
const offsetValue = BigInt(offset + Math.floor(i * step));
|
|
373
|
+
if (offsetValue >= cidr.size) break;
|
|
374
|
+
const value = cidr.base + offsetValue;
|
|
375
|
+
out.push(family === 4 ? bigIntToIpv4(value) : bigIntToIpv6(value));
|
|
376
|
+
}
|
|
234
377
|
}
|
|
235
378
|
}
|
|
236
379
|
return out;
|
|
@@ -266,7 +409,7 @@ function readIpFile(text) {
|
|
|
266
409
|
// src/candidates.ts
|
|
267
410
|
var DEFAULT_CANDIDATE_SAMPLE = 2e4;
|
|
268
411
|
var DEFAULT_CANDIDATE_SEED = 24301;
|
|
269
|
-
var DEFAULT_BRIDGE_SAMPLE =
|
|
412
|
+
var DEFAULT_BRIDGE_SAMPLE = 6556;
|
|
270
413
|
function normalizeCidrInput(cidr) {
|
|
271
414
|
if (cidr === void 0) return [];
|
|
272
415
|
const list = Array.isArray(cidr) ? cidr : [cidr];
|
|
@@ -326,11 +469,239 @@ function normalizeCandidateList(input) {
|
|
|
326
469
|
return out;
|
|
327
470
|
}
|
|
328
471
|
|
|
329
|
-
// src/
|
|
472
|
+
// src/net-env.ts
|
|
330
473
|
import * as net from "node:net";
|
|
474
|
+
import { execFile } from "node:child_process";
|
|
475
|
+
var PROXY_PORTS = [
|
|
476
|
+
{ port: 7890, name: "Clash / Mihomo\uFF08HTTP\uFF09" },
|
|
477
|
+
{ port: 7891, name: "Clash / Mihomo\uFF08SOCKS\uFF09" },
|
|
478
|
+
{ port: 7897, name: "Clash Verge" },
|
|
479
|
+
{ port: 1080, name: "\u901A\u7528 SOCKS" },
|
|
480
|
+
{ port: 10808, name: "v2rayN\uFF08SOCKS\uFF09" },
|
|
481
|
+
{ port: 10809, name: "v2rayN\uFF08HTTP\uFF09" },
|
|
482
|
+
{ port: 2080, name: "Nekoray / sing-box" },
|
|
483
|
+
{ port: 2081, name: "Nekoray / sing-box" },
|
|
484
|
+
{ port: 8889, name: "Surge" },
|
|
485
|
+
{ port: 6152, name: "Surge\uFF08HTTP\uFF09" }
|
|
486
|
+
];
|
|
487
|
+
function isPortListening(port, timeoutMs = 400) {
|
|
488
|
+
return new Promise((resolve) => {
|
|
489
|
+
const socket = new net.Socket();
|
|
490
|
+
let settled = false;
|
|
491
|
+
const done = (open) => {
|
|
492
|
+
if (settled) return;
|
|
493
|
+
settled = true;
|
|
494
|
+
try {
|
|
495
|
+
socket.destroy();
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
resolve(open);
|
|
499
|
+
};
|
|
500
|
+
socket.setTimeout(timeoutMs, () => done(false));
|
|
501
|
+
socket.once("connect", () => done(true));
|
|
502
|
+
socket.once("error", () => done(false));
|
|
503
|
+
try {
|
|
504
|
+
socket.connect(port, "127.0.0.1");
|
|
505
|
+
} catch {
|
|
506
|
+
done(false);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
async function readSystemProxy() {
|
|
511
|
+
if (process.platform !== "win32") return "";
|
|
512
|
+
const query = (key) => new Promise((resolve) => {
|
|
513
|
+
execFile(
|
|
514
|
+
"reg",
|
|
515
|
+
["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", "/v", key],
|
|
516
|
+
{ timeout: 3e3, windowsHide: true },
|
|
517
|
+
(err, stdout) => resolve(err ? "" : String(stdout))
|
|
518
|
+
);
|
|
519
|
+
});
|
|
520
|
+
try {
|
|
521
|
+
const enabled = await query("ProxyEnable");
|
|
522
|
+
if (!/ProxyEnable\s+REG_DWORD\s+0x1/i.test(enabled)) return "";
|
|
523
|
+
const server = await query("ProxyServer");
|
|
524
|
+
const m = /ProxyServer\s+REG_SZ\s+(\S+)/i.exec(server);
|
|
525
|
+
return m ? m[1] : "(\u5DF2\u542F\u7528\uFF0C\u4F46\u672A\u8BFB\u5230\u5730\u5740)";
|
|
526
|
+
} catch {
|
|
527
|
+
return "";
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function isFakeIp(ip) {
|
|
531
|
+
const parts = ip.split(".").map(Number);
|
|
532
|
+
const [a, b] = parts;
|
|
533
|
+
return a === 198 && (b === 18 || b === 19);
|
|
534
|
+
}
|
|
535
|
+
async function detectProxyInterference(host) {
|
|
536
|
+
const findings = [];
|
|
537
|
+
const listening = [];
|
|
538
|
+
await Promise.all(
|
|
539
|
+
PROXY_PORTS.map(async ({ port, name }) => {
|
|
540
|
+
if (await isPortListening(port)) listening.push(`${name} \u2192 127.0.0.1:${port}`);
|
|
541
|
+
})
|
|
542
|
+
);
|
|
543
|
+
if (listening.length > 0) {
|
|
544
|
+
findings.push({
|
|
545
|
+
message: "\u672C\u673A\u6709\u4EE3\u7406\u7A0B\u5E8F\u5728\u76D1\u542C\uFF1A\n " + listening.join("\n "),
|
|
546
|
+
advice: "\u5173\u95ED\u8FD9\u4E9B\u4EE3\u7406\u7A0B\u5E8F\uFF0C\u6216\u5728\u5176\u4E2D\u628A\u672C\u7A0B\u5E8F\u52A0\u5165\u76F4\u8FDE/\u7ED5\u8FC7\u540D\u5355"
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
const sysProxy = await readSystemProxy();
|
|
550
|
+
if (sysProxy) {
|
|
551
|
+
findings.push({
|
|
552
|
+
message: `\u7CFB\u7EDF\u4EE3\u7406\u5DF2\u542F\u7528\uFF1A${sysProxy}`,
|
|
553
|
+
advice: "\u5728\u300C\u8BBE\u7F6E \u2192 \u7F51\u7EDC\u548C Internet \u2192 \u4EE3\u7406\u300D\u4E2D\u5173\u95ED\u7CFB\u7EDF\u4EE3\u7406"
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
const probeHost = host?.trim() || "www.cloudflare.com";
|
|
557
|
+
try {
|
|
558
|
+
const { resolve4 } = await import("node:dns/promises");
|
|
559
|
+
const ips = await resolve4(probeHost).catch(() => []);
|
|
560
|
+
const fake = ips.find((ip) => isFakeIp(ip));
|
|
561
|
+
if (fake) {
|
|
562
|
+
findings.push({
|
|
563
|
+
message: `DNS \u628A ${probeHost} \u89E3\u6790\u5230 ${fake}\uFF08198.18/15 \u4FDD\u7559\u6BB5\uFF09\uFF0CTUN / fake-ip \u6B63\u5728\u751F\u6548`,
|
|
564
|
+
advice: "\u5173\u95ED\u4EE3\u7406\u5DE5\u5177\u7684 TUN \u6A21\u5F0F\u4E0E fake-ip DNS"
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
} catch {
|
|
568
|
+
}
|
|
569
|
+
return { clean: findings.length === 0, findings };
|
|
570
|
+
}
|
|
571
|
+
function isMsysPollutedPath(value) {
|
|
572
|
+
const looksWindows = /^[A-Za-z]:[\\/]/.test(value) || value.includes("\\");
|
|
573
|
+
const hasMsysHint = /node_modules|bash-win32|@dsh-ops|msys/i.test(value);
|
|
574
|
+
return looksWindows || hasMsysHint;
|
|
575
|
+
}
|
|
331
576
|
|
|
332
|
-
// src/
|
|
577
|
+
// src/speed-url.ts
|
|
333
578
|
import * as tls from "node:tls";
|
|
579
|
+
var SPEED_URL_PRESETS = [
|
|
580
|
+
{ url: "speed.cloudflare.com/__down?bytes=104857600", label: "Cloudflare \u5B98\u65B9" },
|
|
581
|
+
{ url: "cf.090227.xyz/__down?bytes=104857600", label: "CM \u63D0\u4F9B" }
|
|
582
|
+
];
|
|
583
|
+
var DEFAULT_SPEED_URL = SPEED_URL_PRESETS[0].url;
|
|
584
|
+
function parseSpeedUrl(raw, fallbackScheme = "https") {
|
|
585
|
+
const value = (raw ?? "").trim();
|
|
586
|
+
if (!value) throw new Error("\u6D4B\u901F\u5730\u5740\u4E3A\u7A7A");
|
|
587
|
+
const schemeMatch = /^([a-z][a-z0-9+.-]*):\/\//i.exec(value);
|
|
588
|
+
if (schemeMatch) {
|
|
589
|
+
const scheme2 = schemeMatch[1].toLowerCase();
|
|
590
|
+
if (scheme2 !== "http" && scheme2 !== "https") {
|
|
591
|
+
throw new Error(`\u4E0D\u652F\u6301\u7684\u6D4B\u901F\u5730\u5740\u534F\u8BAE\uFF1A${scheme2}\uFF08\u53EA\u652F\u6301 http / https\uFF09`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
let normalized = value;
|
|
595
|
+
if (normalized.startsWith("//")) {
|
|
596
|
+
normalized = `${fallbackScheme}:${normalized}`;
|
|
597
|
+
} else if (!/^https?:\/\//i.test(normalized)) {
|
|
598
|
+
normalized = `${fallbackScheme}://${normalized}`;
|
|
599
|
+
}
|
|
600
|
+
let parsed;
|
|
601
|
+
try {
|
|
602
|
+
parsed = new URL(normalized);
|
|
603
|
+
} catch {
|
|
604
|
+
throw new Error(`\u6D4B\u901F\u5730\u5740\u65E0\u6CD5\u89E3\u6790\uFF1A${value}`);
|
|
605
|
+
}
|
|
606
|
+
const scheme = parsed.protocol.replace(":", "").toLowerCase();
|
|
607
|
+
if (scheme !== "http" && scheme !== "https") {
|
|
608
|
+
throw new Error(`\u4E0D\u652F\u6301\u7684\u6D4B\u901F\u5730\u5740\u534F\u8BAE\uFF1A${scheme}`);
|
|
609
|
+
}
|
|
610
|
+
if (!parsed.hostname) throw new Error(`\u6D4B\u901F\u5730\u5740\u7F3A\u5C11\u57DF\u540D\uFF1A${value}`);
|
|
611
|
+
return {
|
|
612
|
+
raw: value,
|
|
613
|
+
scheme,
|
|
614
|
+
host: parsed.hostname,
|
|
615
|
+
path: `${parsed.pathname}${parsed.search}` || "/"
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function schemeForPort(port) {
|
|
619
|
+
return port === 443 || port === 2053 || port === 2083 || port === 2087 || port === 2096 || port === 8443 ? "https" : "http";
|
|
620
|
+
}
|
|
621
|
+
function checkSpeedUrlAvailable(parsed, timeoutMs = 6e3) {
|
|
622
|
+
return new Promise((resolve) => {
|
|
623
|
+
let settled = false;
|
|
624
|
+
let received = "";
|
|
625
|
+
const finish = (available, status, error) => {
|
|
626
|
+
if (settled) return;
|
|
627
|
+
settled = true;
|
|
628
|
+
clearTimeout(timer);
|
|
629
|
+
try {
|
|
630
|
+
socket.destroy();
|
|
631
|
+
} catch {
|
|
632
|
+
}
|
|
633
|
+
resolve({ available, status, error });
|
|
634
|
+
};
|
|
635
|
+
const path = parsed.path.includes("bytes=") ? parsed.path.replace(/bytes=\d+/, "bytes=1024") : parsed.path;
|
|
636
|
+
const request = [
|
|
637
|
+
`GET ${path} HTTP/1.1`,
|
|
638
|
+
`Host: ${parsed.host}`,
|
|
639
|
+
`User-Agent: ${USER_AGENT_FOR_CHECK}`,
|
|
640
|
+
"Accept: */*",
|
|
641
|
+
"Connection: close",
|
|
642
|
+
"",
|
|
643
|
+
""
|
|
644
|
+
].join("\r\n");
|
|
645
|
+
const connectOptions = {
|
|
646
|
+
host: parsed.host,
|
|
647
|
+
port: parsed.scheme === "https" ? 443 : 80,
|
|
648
|
+
servername: parsed.host,
|
|
649
|
+
rejectUnauthorized: false
|
|
650
|
+
};
|
|
651
|
+
const socket = parsed.scheme === "https" ? tls.connect(connectOptions, () => socket.write(request)) : (
|
|
652
|
+
// http 明文:用 net 连接
|
|
653
|
+
(() => {
|
|
654
|
+
const net3 = __require("node:net");
|
|
655
|
+
const s = net3.connect({ host: parsed.host, port: 80 }, () => s.write(request));
|
|
656
|
+
return s;
|
|
657
|
+
})()
|
|
658
|
+
);
|
|
659
|
+
const timer = setTimeout(() => finish(false, 0, "\u9884\u68C0\u8D85\u65F6"), timeoutMs);
|
|
660
|
+
socket.setTimeout(timeoutMs, () => finish(false, 0, "\u9884\u68C0\u8D85\u65F6"));
|
|
661
|
+
socket.on("error", (err) => finish(false, 0, err.message));
|
|
662
|
+
socket.on("data", (chunk) => {
|
|
663
|
+
received += chunk.toString("utf8");
|
|
664
|
+
const m = /^HTTP\/1\.[01]\s+(\d{3})/.exec(received);
|
|
665
|
+
if (m) {
|
|
666
|
+
const status = Number(m[1]);
|
|
667
|
+
finish(status === 200 || status === 206, status);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
var USER_AGENT_FOR_CHECK = "CFPilot-Scanner/1.0 (+https://github.com/xmwengxing/CFPilot)";
|
|
673
|
+
async function pickAvailableSpeedUrl(candidates, log) {
|
|
674
|
+
const list = candidates.length > 0 ? candidates : [DEFAULT_SPEED_URL];
|
|
675
|
+
for (const candidate of list) {
|
|
676
|
+
let parsed;
|
|
677
|
+
try {
|
|
678
|
+
parsed = parseSpeedUrl(candidate, "https");
|
|
679
|
+
} catch {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const result = await checkSpeedUrlAvailable(parsed);
|
|
683
|
+
if (result.available) {
|
|
684
|
+
log?.(`\u6D4B\u901F\u5730\u5740\u53EF\u7528\uFF1A${candidate}`);
|
|
685
|
+
return candidate;
|
|
686
|
+
}
|
|
687
|
+
log?.(`\u6D4B\u901F\u5730\u5740\u4E0D\u53EF\u7528\uFF08${result.error ?? "HTTP " + result.status}\uFF09\uFF1A${candidate}`);
|
|
688
|
+
}
|
|
689
|
+
log?.(`\u6240\u6709\u6D4B\u901F\u5730\u5740\u90FD\u4E0D\u53EF\u7528\uFF0C\u56DE\u9000\u5230\u9ED8\u8BA4\uFF1A${DEFAULT_SPEED_URL}`);
|
|
690
|
+
return DEFAULT_SPEED_URL;
|
|
691
|
+
}
|
|
692
|
+
function withBytes(parsed, bytes) {
|
|
693
|
+
if (/bytes=\d+/.test(parsed.path)) {
|
|
694
|
+
return parsed.path.replace(/bytes=\d+/, `bytes=${bytes}`);
|
|
695
|
+
}
|
|
696
|
+
const sep = parsed.path.includes("?") ? "&" : "?";
|
|
697
|
+
return `${parsed.path}${sep}bytes=${bytes}`;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/scan.ts
|
|
701
|
+
import * as net2 from "node:net";
|
|
702
|
+
|
|
703
|
+
// src/true-probe.ts
|
|
704
|
+
import * as tls2 from "node:tls";
|
|
334
705
|
import * as crypto from "node:crypto";
|
|
335
706
|
var USER_AGENT = "CFPilot-Scanner/1.0 (+https://github.com/xmwengxing/CFPilot)";
|
|
336
707
|
function extractColo(text) {
|
|
@@ -364,7 +735,7 @@ function probeWs101(ip, port, host, timeoutMs = 3e3, path = "/__edge_probe") {
|
|
|
364
735
|
"",
|
|
365
736
|
""
|
|
366
737
|
].join("\r\n");
|
|
367
|
-
const socket =
|
|
738
|
+
const socket = tls2.connect(
|
|
368
739
|
{
|
|
369
740
|
host: ip,
|
|
370
741
|
port,
|
|
@@ -405,6 +776,216 @@ function probeWs101(ip, port, host, timeoutMs = 3e3, path = "/__edge_probe") {
|
|
|
405
776
|
});
|
|
406
777
|
});
|
|
407
778
|
}
|
|
779
|
+
function encodeWsFrame(payload, opcode = 2) {
|
|
780
|
+
const mask = crypto.randomBytes(4);
|
|
781
|
+
const masked = Buffer.alloc(payload.length);
|
|
782
|
+
for (let i = 0; i < payload.length; i++) {
|
|
783
|
+
masked[i] = payload[i] ^ mask[i % 4];
|
|
784
|
+
}
|
|
785
|
+
let header;
|
|
786
|
+
if (payload.length < 126) {
|
|
787
|
+
header = Buffer.from([128 | opcode, 128 | payload.length]);
|
|
788
|
+
} else if (payload.length < 65536) {
|
|
789
|
+
header = Buffer.alloc(4);
|
|
790
|
+
header[0] = 128 | opcode;
|
|
791
|
+
header[1] = 254;
|
|
792
|
+
header.writeUInt16BE(payload.length, 2);
|
|
793
|
+
} else {
|
|
794
|
+
header = Buffer.alloc(10);
|
|
795
|
+
header[0] = 128 | opcode;
|
|
796
|
+
header[1] = 255;
|
|
797
|
+
header.writeBigUInt64BE(BigInt(payload.length), 2);
|
|
798
|
+
}
|
|
799
|
+
return Buffer.concat([header, mask, masked]);
|
|
800
|
+
}
|
|
801
|
+
function decodeWsFrame(buf) {
|
|
802
|
+
if (buf.length < 2) return null;
|
|
803
|
+
const opcode = buf[0] & 15;
|
|
804
|
+
const len7 = buf[1] & 127;
|
|
805
|
+
let offset;
|
|
806
|
+
let payloadLen;
|
|
807
|
+
if (len7 < 126) {
|
|
808
|
+
offset = 2;
|
|
809
|
+
payloadLen = len7;
|
|
810
|
+
} else if (len7 === 126) {
|
|
811
|
+
if (buf.length < 4) return null;
|
|
812
|
+
offset = 4;
|
|
813
|
+
payloadLen = buf.readUInt16BE(2);
|
|
814
|
+
} else {
|
|
815
|
+
if (buf.length < 10) return null;
|
|
816
|
+
offset = 10;
|
|
817
|
+
payloadLen = Number(buf.readBigUInt64BE(2));
|
|
818
|
+
}
|
|
819
|
+
if (buf.length < offset + payloadLen) return null;
|
|
820
|
+
const frame = {
|
|
821
|
+
opcode,
|
|
822
|
+
payload: Buffer.from(buf.subarray(offset, offset + payloadLen))
|
|
823
|
+
};
|
|
824
|
+
const rest = Buffer.from(buf.subarray(offset + payloadLen));
|
|
825
|
+
return { frame, rest };
|
|
826
|
+
}
|
|
827
|
+
function probeWsPing(ip, port, host, timeoutMs = 5e3, path = "/__edge_probe") {
|
|
828
|
+
return new Promise((resolve) => {
|
|
829
|
+
const started = Date.now();
|
|
830
|
+
const key = crypto.randomBytes(16).toString("base64");
|
|
831
|
+
let settled = false;
|
|
832
|
+
let handshakeDone = false;
|
|
833
|
+
let buf = Buffer.alloc(0);
|
|
834
|
+
let receivedText = "";
|
|
835
|
+
const finish = (result) => {
|
|
836
|
+
if (settled) return;
|
|
837
|
+
settled = true;
|
|
838
|
+
clearTimeout(timer);
|
|
839
|
+
try {
|
|
840
|
+
socket.destroy();
|
|
841
|
+
} catch {
|
|
842
|
+
}
|
|
843
|
+
resolve({ ...result, latencyMs: Date.now() - started });
|
|
844
|
+
};
|
|
845
|
+
const request = [
|
|
846
|
+
`GET ${path} HTTP/1.1`,
|
|
847
|
+
`Host: ${host}`,
|
|
848
|
+
`Upgrade: websocket`,
|
|
849
|
+
`Connection: Upgrade`,
|
|
850
|
+
`Sec-WebSocket-Key: ${key}`,
|
|
851
|
+
`Sec-WebSocket-Version: 13`,
|
|
852
|
+
`User-Agent: ${USER_AGENT}`,
|
|
853
|
+
"",
|
|
854
|
+
""
|
|
855
|
+
].join("\r\n");
|
|
856
|
+
const socket = tls2.connect(
|
|
857
|
+
{
|
|
858
|
+
host: ip,
|
|
859
|
+
port,
|
|
860
|
+
servername: host,
|
|
861
|
+
rejectUnauthorized: false,
|
|
862
|
+
ALPNProtocols: ["http/1.1"]
|
|
863
|
+
},
|
|
864
|
+
() => {
|
|
865
|
+
socket.write(request);
|
|
866
|
+
}
|
|
867
|
+
);
|
|
868
|
+
const timer = setTimeout(() => {
|
|
869
|
+
finish({
|
|
870
|
+
ok: false,
|
|
871
|
+
colo: "",
|
|
872
|
+
status: 0,
|
|
873
|
+
error: handshakeDone ? "PING \u8D85\u65F6\uFF08\u6570\u636E\u9762\u672A\u54CD\u5E94\uFF09" : "\u63E1\u624B\u8D85\u65F6",
|
|
874
|
+
pongVerified: handshakeDone ? false : void 0
|
|
875
|
+
});
|
|
876
|
+
}, timeoutMs);
|
|
877
|
+
socket.setTimeout(timeoutMs, () => {
|
|
878
|
+
finish({
|
|
879
|
+
ok: false,
|
|
880
|
+
colo: "",
|
|
881
|
+
status: 0,
|
|
882
|
+
error: handshakeDone ? "PING \u8D85\u65F6\uFF08\u6570\u636E\u9762\u672A\u54CD\u5E94\uFF09" : "\u8FDE\u63A5\u8D85\u65F6",
|
|
883
|
+
pongVerified: handshakeDone ? false : void 0
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
socket.on("error", (err) => {
|
|
887
|
+
finish({
|
|
888
|
+
ok: false,
|
|
889
|
+
colo: "",
|
|
890
|
+
status: 0,
|
|
891
|
+
error: err.message,
|
|
892
|
+
pongVerified: handshakeDone ? false : void 0
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
socket.on("data", (chunk) => {
|
|
896
|
+
buf = Buffer.concat([buf, chunk]);
|
|
897
|
+
if (!handshakeDone) {
|
|
898
|
+
const lineEnd = buf.indexOf("\r\n\r\n");
|
|
899
|
+
if (lineEnd < 0) {
|
|
900
|
+
if (buf.length > 8192) finish({ ok: false, colo: "", status: 0, error: "\u54CD\u5E94\u5934\u5F02\u5E38" });
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const head = buf.slice(0, lineEnd).toString("latin1");
|
|
904
|
+
const statusLine = head.split("\r\n")[0] ?? "";
|
|
905
|
+
const statusMatch = /^HTTP\/1\.[01]\s+(\d{3})/.exec(statusLine);
|
|
906
|
+
const status = statusMatch ? Number(statusMatch[1]) : 0;
|
|
907
|
+
const colo = extractColo(head);
|
|
908
|
+
buf = buf.subarray(lineEnd + 4);
|
|
909
|
+
if (status !== 101) {
|
|
910
|
+
finish({ ok: false, colo, status, error: `HTTP ${status}` });
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
handshakeDone = true;
|
|
914
|
+
socket.write(encodeWsFrame(new TextEncoder().encode("PING"), 1));
|
|
915
|
+
}
|
|
916
|
+
for (; ; ) {
|
|
917
|
+
const decoded = decodeWsFrame(buf);
|
|
918
|
+
if (!decoded) break;
|
|
919
|
+
buf = decoded.rest;
|
|
920
|
+
const { opcode, payload } = decoded.frame;
|
|
921
|
+
if (opcode === 8) {
|
|
922
|
+
const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;
|
|
923
|
+
finish({
|
|
924
|
+
ok: false,
|
|
925
|
+
colo: "",
|
|
926
|
+
status: 101,
|
|
927
|
+
error: `WS \u5173\u95ED\uFF08code ${code}\uFF09`,
|
|
928
|
+
pongVerified: false
|
|
929
|
+
});
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (opcode === 1 || opcode === 2) {
|
|
933
|
+
receivedText += payload.toString("utf8");
|
|
934
|
+
try {
|
|
935
|
+
const parsed = JSON.parse(receivedText);
|
|
936
|
+
if (parsed.pong === true) {
|
|
937
|
+
finish({
|
|
938
|
+
ok: true,
|
|
939
|
+
colo: typeof parsed.colo === "string" ? parsed.colo.toUpperCase() : "",
|
|
940
|
+
status: 101,
|
|
941
|
+
pongVerified: true,
|
|
942
|
+
probeColo: typeof parsed.colo === "string" ? parsed.colo.toUpperCase() : void 0
|
|
943
|
+
});
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
} catch {
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
function probeTlsHandshake(ip, port, host, timeoutMs = 4e3) {
|
|
954
|
+
return new Promise((resolve) => {
|
|
955
|
+
const started = Date.now();
|
|
956
|
+
let settled = false;
|
|
957
|
+
const finish = (result) => {
|
|
958
|
+
if (settled) return;
|
|
959
|
+
settled = true;
|
|
960
|
+
clearTimeout(timer);
|
|
961
|
+
try {
|
|
962
|
+
socket.destroy();
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
965
|
+
resolve({ ip, tlsMs: result.tlsMs ?? Date.now() - started, ...result });
|
|
966
|
+
};
|
|
967
|
+
const socket = tls2.connect(
|
|
968
|
+
{
|
|
969
|
+
host: ip,
|
|
970
|
+
port,
|
|
971
|
+
servername: host,
|
|
972
|
+
rejectUnauthorized: false,
|
|
973
|
+
ALPNProtocols: ["http/1.1"]
|
|
974
|
+
},
|
|
975
|
+
() => {
|
|
976
|
+
const alpn = socket.alpnProtocol;
|
|
977
|
+
if (alpn && alpn !== "http/1.1") {
|
|
978
|
+
finish({ error: `ALPN=${alpn}` });
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
finish({ tlsMs: Date.now() - started, alpn: alpn || void 0 });
|
|
982
|
+
}
|
|
983
|
+
);
|
|
984
|
+
const timer = setTimeout(() => finish({ error: "TLS \u63E1\u624B\u8D85\u65F6" }), timeoutMs);
|
|
985
|
+
socket.setTimeout(timeoutMs, () => finish({ error: "TLS \u8FDE\u63A5\u8D85\u65F6" }));
|
|
986
|
+
socket.on("error", (err) => finish({ error: err.message }));
|
|
987
|
+
});
|
|
988
|
+
}
|
|
408
989
|
function measureDownload(ip, port, host, bytes, timeoutMs = 12e3, path = "/__edge_probe") {
|
|
409
990
|
return new Promise((resolve) => {
|
|
410
991
|
const started = Date.now();
|
|
@@ -430,7 +1011,7 @@ function measureDownload(ip, port, host, bytes, timeoutMs = 12e3, path = "/__edg
|
|
|
430
1011
|
"",
|
|
431
1012
|
""
|
|
432
1013
|
].join("\r\n");
|
|
433
|
-
const socket =
|
|
1014
|
+
const socket = tls2.connect(
|
|
434
1015
|
{
|
|
435
1016
|
host: ip,
|
|
436
1017
|
port,
|
|
@@ -489,13 +1070,215 @@ function measureDownload(ip, port, host, bytes, timeoutMs = 12e3, path = "/__edg
|
|
|
489
1070
|
socket.on("close", settle);
|
|
490
1071
|
});
|
|
491
1072
|
}
|
|
1073
|
+
function measureThroughput(options) {
|
|
1074
|
+
const {
|
|
1075
|
+
ip,
|
|
1076
|
+
port,
|
|
1077
|
+
sniHost,
|
|
1078
|
+
path,
|
|
1079
|
+
requestHost = sniHost,
|
|
1080
|
+
durationMs = 6e3,
|
|
1081
|
+
warmupMs = 1e3,
|
|
1082
|
+
windowMs = 400,
|
|
1083
|
+
connections = 3,
|
|
1084
|
+
timeoutMs = 2e4
|
|
1085
|
+
} = options;
|
|
1086
|
+
const connCount = Math.max(1, Math.min(8, connections));
|
|
1087
|
+
return new Promise((resolve) => {
|
|
1088
|
+
const started = Date.now();
|
|
1089
|
+
let settled = false;
|
|
1090
|
+
let totalBytes = 0;
|
|
1091
|
+
let windowBytes = 0;
|
|
1092
|
+
let windowStart = started;
|
|
1093
|
+
let peak = 0;
|
|
1094
|
+
let openedCount = 0;
|
|
1095
|
+
let lastError = "";
|
|
1096
|
+
const sockets = [];
|
|
1097
|
+
const finish = () => {
|
|
1098
|
+
if (settled) return;
|
|
1099
|
+
settled = true;
|
|
1100
|
+
clearInterval(ticker);
|
|
1101
|
+
clearTimeout(timer);
|
|
1102
|
+
for (const s of sockets) {
|
|
1103
|
+
try {
|
|
1104
|
+
s.destroy();
|
|
1105
|
+
} catch {
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const elapsedNow = Date.now() - started;
|
|
1109
|
+
if (elapsedNow >= warmupMs) {
|
|
1110
|
+
const dt = (Date.now() - windowStart) / 1e3;
|
|
1111
|
+
if (dt > 0) {
|
|
1112
|
+
const speed = windowBytes / dt / 1024 / 1024;
|
|
1113
|
+
if (speed > peak) peak = speed;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
const elapsed = Math.max(1e-3, (Date.now() - started) / 1e3);
|
|
1117
|
+
resolve({
|
|
1118
|
+
ok: totalBytes > 0,
|
|
1119
|
+
bytes: totalBytes,
|
|
1120
|
+
ms: Date.now() - started,
|
|
1121
|
+
peakMBps: Number(peak.toFixed(2)),
|
|
1122
|
+
avgMBps: Number((totalBytes / 1024 / 1024 / elapsed).toFixed(2)),
|
|
1123
|
+
connections: openedCount,
|
|
1124
|
+
error: totalBytes > 0 ? void 0 : lastError || "\u672A\u6536\u5230\u6570\u636E"
|
|
1125
|
+
});
|
|
1126
|
+
};
|
|
1127
|
+
const request = [
|
|
1128
|
+
`GET ${path} HTTP/1.1`,
|
|
1129
|
+
`Host: ${requestHost}`,
|
|
1130
|
+
`User-Agent: ${USER_AGENT}`,
|
|
1131
|
+
"Accept: */*",
|
|
1132
|
+
"Accept-Encoding: identity",
|
|
1133
|
+
"Connection: close",
|
|
1134
|
+
"",
|
|
1135
|
+
""
|
|
1136
|
+
].join("\r\n");
|
|
1137
|
+
const openOne = () => {
|
|
1138
|
+
let headerDone = false;
|
|
1139
|
+
let headerBuffer = "";
|
|
1140
|
+
const socket = tls2.connect(
|
|
1141
|
+
{
|
|
1142
|
+
host: ip,
|
|
1143
|
+
port,
|
|
1144
|
+
servername: sniHost,
|
|
1145
|
+
rejectUnauthorized: false,
|
|
1146
|
+
ALPNProtocols: ["http/1.1"]
|
|
1147
|
+
},
|
|
1148
|
+
() => {
|
|
1149
|
+
openedCount++;
|
|
1150
|
+
socket.write(request);
|
|
1151
|
+
}
|
|
1152
|
+
);
|
|
1153
|
+
sockets.push(socket);
|
|
1154
|
+
socket.setTimeout(timeoutMs, () => {
|
|
1155
|
+
try {
|
|
1156
|
+
socket.destroy();
|
|
1157
|
+
} catch {
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
socket.on("error", (err) => {
|
|
1161
|
+
lastError = err.message;
|
|
1162
|
+
});
|
|
1163
|
+
socket.on("data", (chunk) => {
|
|
1164
|
+
if (!headerDone) {
|
|
1165
|
+
headerBuffer += chunk.toString("latin1");
|
|
1166
|
+
const idx = headerBuffer.indexOf("\r\n\r\n");
|
|
1167
|
+
if (idx < 0) {
|
|
1168
|
+
if (headerBuffer.length > 8192) {
|
|
1169
|
+
lastError = "\u54CD\u5E94\u5934\u5F02\u5E38";
|
|
1170
|
+
socket.destroy();
|
|
1171
|
+
}
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
const head = headerBuffer.slice(0, idx);
|
|
1175
|
+
const status = /^HTTP\/1\.[01]\s+(\d{3})/.exec(head)?.[1];
|
|
1176
|
+
if (status !== "200") {
|
|
1177
|
+
lastError = `HTTP ${status ?? "?"}`;
|
|
1178
|
+
socket.destroy();
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
headerDone = true;
|
|
1182
|
+
const body = Buffer.from(headerBuffer, "latin1").subarray(idx + 4);
|
|
1183
|
+
totalBytes += body.byteLength;
|
|
1184
|
+
windowBytes += body.byteLength;
|
|
1185
|
+
headerBuffer = "";
|
|
1186
|
+
} else {
|
|
1187
|
+
totalBytes += chunk.byteLength;
|
|
1188
|
+
windowBytes += chunk.byteLength;
|
|
1189
|
+
}
|
|
1190
|
+
if (Date.now() - started > durationMs) finish();
|
|
1191
|
+
});
|
|
1192
|
+
socket.on("end", () => {
|
|
1193
|
+
if (sockets.every((s) => s.destroyed || s.readableEnded)) finish();
|
|
1194
|
+
});
|
|
1195
|
+
socket.on("close", () => {
|
|
1196
|
+
if (sockets.every((s) => s.destroyed)) finish();
|
|
1197
|
+
});
|
|
1198
|
+
};
|
|
1199
|
+
const ticker = setInterval(() => {
|
|
1200
|
+
const now = Date.now();
|
|
1201
|
+
const dt = (now - windowStart) / 1e3;
|
|
1202
|
+
if (dt <= 0) return;
|
|
1203
|
+
const bytes = windowBytes;
|
|
1204
|
+
windowBytes = 0;
|
|
1205
|
+
windowStart = now;
|
|
1206
|
+
if (now - started < warmupMs) return;
|
|
1207
|
+
const speed = bytes / dt / 1024 / 1024;
|
|
1208
|
+
if (speed > peak) peak = speed;
|
|
1209
|
+
}, windowMs);
|
|
1210
|
+
const timer = setTimeout(finish, durationMs + 4e3);
|
|
1211
|
+
for (let i = 0; i < connCount; i++) {
|
|
1212
|
+
setTimeout(openOne, i * 60);
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/countries.ts
|
|
1218
|
+
var COUNTRY_COLOS = {
|
|
1219
|
+
HK: ["HKG"],
|
|
1220
|
+
TW: ["TPE", "KHH"],
|
|
1221
|
+
JP: ["NRT", "KIX", "CTS", "FUK"],
|
|
1222
|
+
KR: ["ICN", "PUS"],
|
|
1223
|
+
SG: ["SIN"],
|
|
1224
|
+
MY: ["KUL"],
|
|
1225
|
+
TH: ["BKK"],
|
|
1226
|
+
VN: ["SGN", "HAN"],
|
|
1227
|
+
ID: ["CGK", "DPS"],
|
|
1228
|
+
PH: ["MNL"],
|
|
1229
|
+
IN: ["BOM", "MAA", "DEL"],
|
|
1230
|
+
AU: ["SYD", "MEL", "BNE", "PER"],
|
|
1231
|
+
NZ: ["AKL"],
|
|
1232
|
+
US: ["LAX", "SJC", "SEA", "ORD", "DFW", "IAD", "EWR", "ATL", "MIA", "DEN", "PHX", "BOS"],
|
|
1233
|
+
CA: ["YYZ", "YVR", "YYC"],
|
|
1234
|
+
GB: ["LHR", "MAN"],
|
|
1235
|
+
DE: ["FRA", "MUC", "BER"],
|
|
1236
|
+
FR: ["CDG", "MRS"],
|
|
1237
|
+
NL: ["AMS"],
|
|
1238
|
+
IT: ["MXP", "FCO"],
|
|
1239
|
+
ES: ["MAD", "BCN"],
|
|
1240
|
+
SE: ["ARN"],
|
|
1241
|
+
CH: ["ZRH"],
|
|
1242
|
+
AT: ["VIE"],
|
|
1243
|
+
PL: ["WAW"],
|
|
1244
|
+
RU: ["DME", "SVO"],
|
|
1245
|
+
TR: ["IST"],
|
|
1246
|
+
AE: ["DXB"],
|
|
1247
|
+
IL: ["TLV"],
|
|
1248
|
+
ZA: ["JNB"],
|
|
1249
|
+
BR: ["GRU", "GIG"],
|
|
1250
|
+
AR: ["EZE"],
|
|
1251
|
+
CL: ["SCL"],
|
|
1252
|
+
MX: ["MEX"]
|
|
1253
|
+
};
|
|
1254
|
+
var coloToCountry = /* @__PURE__ */ new Map();
|
|
1255
|
+
for (const [country, colos] of Object.entries(COUNTRY_COLOS)) {
|
|
1256
|
+
for (const colo of colos) coloToCountry.set(colo, country);
|
|
1257
|
+
}
|
|
1258
|
+
function countryOfColo(colo) {
|
|
1259
|
+
return coloToCountry.get(colo.toUpperCase()) ?? "";
|
|
1260
|
+
}
|
|
1261
|
+
function isKnownCountry(code) {
|
|
1262
|
+
return Object.prototype.hasOwnProperty.call(COUNTRY_COLOS, code.toUpperCase());
|
|
1263
|
+
}
|
|
1264
|
+
var SUPPORTED_COUNTRIES = Object.keys(COUNTRY_COLOS);
|
|
1265
|
+
function normalizeCountryFilter(input) {
|
|
1266
|
+
if (!Array.isArray(input)) return [];
|
|
1267
|
+
const out = [];
|
|
1268
|
+
for (const raw of input) {
|
|
1269
|
+
const code = String(raw ?? "").trim().toUpperCase();
|
|
1270
|
+
if (!code || out.includes(code)) continue;
|
|
1271
|
+
if (isKnownCountry(code)) out.push(code);
|
|
1272
|
+
}
|
|
1273
|
+
return out;
|
|
1274
|
+
}
|
|
492
1275
|
|
|
493
1276
|
// src/scan.ts
|
|
494
1277
|
async function tcpPing(ip, port, timeoutMs) {
|
|
495
1278
|
return new Promise((resolve) => {
|
|
496
1279
|
const started = Date.now();
|
|
497
1280
|
let settled = false;
|
|
498
|
-
const socket = new
|
|
1281
|
+
const socket = new net2.Socket();
|
|
499
1282
|
const done = (ok) => {
|
|
500
1283
|
if (settled) return;
|
|
501
1284
|
settled = true;
|
|
@@ -528,6 +1311,8 @@ async function runScan(options) {
|
|
|
528
1311
|
const started = Date.now();
|
|
529
1312
|
const concurrency = options.concurrency ?? 300;
|
|
530
1313
|
const tcpTimeoutMs = options.tcpTimeoutMs ?? 1200;
|
|
1314
|
+
const tlsConcurrency = options.tlsConcurrency ?? 64;
|
|
1315
|
+
const tlsTimeoutMs = options.tlsTimeoutMs ?? 4e3;
|
|
531
1316
|
const maxLatencyMs = options.maxLatencyMs ?? 0;
|
|
532
1317
|
const probeLimit = options.probeLimit ?? 120;
|
|
533
1318
|
const probeConcurrency = options.probeConcurrency ?? 24;
|
|
@@ -558,29 +1343,115 @@ async function runScan(options) {
|
|
|
558
1343
|
});
|
|
559
1344
|
const tcpOk = tcpResults.filter((r) => r.ok);
|
|
560
1345
|
const filtered = maxLatencyMs > 0 ? tcpOk.filter((r) => r.latency <= maxLatencyMs) : tcpOk;
|
|
561
|
-
|
|
562
|
-
|
|
1346
|
+
report({ type: "stage", stage: 1, name: "TLS \u63E1\u624B\u7B5B\u9009", total: filtered.length });
|
|
1347
|
+
let tlsDone = 0;
|
|
1348
|
+
const tlsResults = await pool(filtered, tlsConcurrency, async (item) => {
|
|
1349
|
+
const probe = await probeTlsHandshake(item.ip, options.port, options.host, tlsTimeoutMs);
|
|
1350
|
+
tlsDone++;
|
|
1351
|
+
if (tlsDone % 200 === 0 || tlsDone === filtered.length) {
|
|
1352
|
+
report({
|
|
1353
|
+
type: "progress",
|
|
1354
|
+
stage: 1,
|
|
1355
|
+
done: tlsDone,
|
|
1356
|
+
total: filtered.length,
|
|
1357
|
+
message: `TLS \u63E1\u624B ${tlsDone}/${filtered.length}${probe.tlsMs > 0 ? ` ${probe.tlsMs}ms` : " \u2717"}`
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
return { ...item, tls: probe };
|
|
1361
|
+
});
|
|
1362
|
+
const tlsOk = tlsResults.filter((r) => r.tls.tlsMs > 0);
|
|
1363
|
+
const stage1 = tlsOk.sort((a, b) => a.tls.tlsMs - b.tls.tlsMs || a.latency - b.latency).slice(0, probeLimit);
|
|
1364
|
+
const strictProbe = options.verifyPayload !== false;
|
|
1365
|
+
const strictProbePath = "/__edge_probe";
|
|
1366
|
+
report({
|
|
1367
|
+
type: "stage",
|
|
1368
|
+
stage: 2,
|
|
1369
|
+
name: strictProbe ? "WS 101 + \u54CD\u5E94\u4F53\u6821\u9A8C\uFF08\u63A2\u9488\u7AEF\u70B9\uFF09" : "WebSocket 101 \u6821\u9A8C",
|
|
1370
|
+
total: stage1.length
|
|
1371
|
+
});
|
|
563
1372
|
let probeDone = 0;
|
|
1373
|
+
let verifiedCount = 0;
|
|
564
1374
|
const probeResults = await pool(stage1, probeConcurrency, async (item) => {
|
|
565
|
-
const
|
|
1375
|
+
const probeFn = strictProbe ? probeWsPing : probeWs101;
|
|
1376
|
+
const probePath = strictProbe ? strictProbePath : options.wsPath ?? "/__edge_probe";
|
|
1377
|
+
const result = await probeFn(
|
|
1378
|
+
item.ip,
|
|
1379
|
+
options.port,
|
|
1380
|
+
options.host,
|
|
1381
|
+
probeTimeoutMs,
|
|
1382
|
+
probePath
|
|
1383
|
+
);
|
|
566
1384
|
probeDone++;
|
|
1385
|
+
if (result.pongVerified) verifiedCount++;
|
|
567
1386
|
report({
|
|
568
1387
|
type: "progress",
|
|
569
1388
|
stage: 2,
|
|
570
1389
|
done: probeDone,
|
|
571
1390
|
total: stage1.length,
|
|
572
|
-
message: `101 \u6821\u9A8C ${probeDone}/${stage1.length}${result.ok ? ` \u2713 ${result.colo}` : ""}`
|
|
1391
|
+
message: strictProbe ? `101+PING \u6821\u9A8C ${probeDone}/${stage1.length}${result.ok ? ` \u2713 ${result.colo}` : result.pongVerified === false ? " \u2717 \u65E0\u54CD\u5E94" : ""}` : `101 \u6821\u9A8C ${probeDone}/${stage1.length}${result.ok ? ` \u2713 ${result.colo}` : ""}`
|
|
573
1392
|
});
|
|
574
1393
|
return { ...item, probe: result };
|
|
575
1394
|
});
|
|
576
|
-
const wsOk = probeResults.filter((r) => r.probe.ok);
|
|
577
|
-
const
|
|
1395
|
+
const wsOk = strictProbe ? probeResults.filter((r) => r.probe.ok && r.probe.pongVerified === true) : probeResults.filter((r) => r.probe.ok);
|
|
1396
|
+
const countryFilter = normalizeCountryFilter(options.countries ?? []);
|
|
1397
|
+
let wsFiltered = wsOk;
|
|
1398
|
+
let appliedCountries = [];
|
|
1399
|
+
if (countryFilter.length > 0 && wsOk.length > 0) {
|
|
1400
|
+
const matched = wsOk.filter((r) => countryFilter.includes(countryOfColo(r.probe.colo || "")));
|
|
1401
|
+
if (matched.length > 0) {
|
|
1402
|
+
wsFiltered = matched;
|
|
1403
|
+
appliedCountries = countryFilter;
|
|
1404
|
+
report({
|
|
1405
|
+
type: "progress",
|
|
1406
|
+
stage: 2,
|
|
1407
|
+
done: wsOk.length,
|
|
1408
|
+
total: wsOk.length,
|
|
1409
|
+
message: `\u56FD\u5BB6\u7B5B\u9009 ${countryFilter.join("/")}\uFF1A\u4FDD\u7559 ${matched.length}/${wsOk.length}\uFF0C\u8FDB\u5165\u6D4B\u901F`
|
|
1410
|
+
});
|
|
1411
|
+
} else {
|
|
1412
|
+
report({
|
|
1413
|
+
type: "progress",
|
|
1414
|
+
stage: 2,
|
|
1415
|
+
done: wsOk.length,
|
|
1416
|
+
total: wsOk.length,
|
|
1417
|
+
message: `\u56FD\u5BB6\u7B5B\u9009 ${countryFilter.join("/")} \u540E\u65E0\u5019\u9009\uFF0C\u5DF2\u5FFD\u7565\uFF08\u91C7\u6837\u592A\u5C0F\uFF1F\u8C03\u5230 500~1000 \u518D\u8BD5\uFF09`
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const stage2 = wsFiltered.sort((a, b) => a.tls.tlsMs - b.tls.tlsMs || a.latency - b.latency).slice(0, speedLimit);
|
|
578
1422
|
let speedResults;
|
|
1423
|
+
const speedMode = options.speedMode ?? "probe";
|
|
579
1424
|
if (options.skipSpeed) {
|
|
580
1425
|
report({ type: "stage", stage: 3, name: "\u8DF3\u8FC7\u6D4B\u901F\uFF08\u6309\u5EF6\u8FDF\u6392\u5E8F\uFF09", total: stage2.length });
|
|
581
1426
|
speedResults = stage2.map((r) => ({ ...r }));
|
|
1427
|
+
} else if (speedMode === "url") {
|
|
1428
|
+
report({ type: "stage", stage: 3, name: "\u771F\u5B9E\u4E0B\u884C\u901F\u5EA6\u6D4B\u8BD5\uFF08\u516C\u5F00\u6D4B\u901F\u5730\u5740\uFF09", total: stage2.length });
|
|
1429
|
+
const parsedUrl = parseSpeedUrl(options.speedUrl ?? DEFAULT_SPEED_URL, schemeForPort(options.port));
|
|
1430
|
+
const requestPath = withBytes(parsedUrl, options.speedBytes ?? 104857600);
|
|
1431
|
+
let speedDone = 0;
|
|
1432
|
+
speedResults = await pool(stage2, speedConcurrency, async (item) => {
|
|
1433
|
+
const result = await measureThroughput({
|
|
1434
|
+
ip: item.ip,
|
|
1435
|
+
port: options.port,
|
|
1436
|
+
sniHost: parsedUrl.host,
|
|
1437
|
+
requestHost: parsedUrl.host,
|
|
1438
|
+
path: requestPath,
|
|
1439
|
+
connections: options.speedConnections ?? 3,
|
|
1440
|
+
durationMs: options.speedDurationMs ?? 6e3,
|
|
1441
|
+
timeoutMs: options.speedTimeoutMs ?? 2e4
|
|
1442
|
+
});
|
|
1443
|
+
speedDone++;
|
|
1444
|
+
report({
|
|
1445
|
+
type: "progress",
|
|
1446
|
+
stage: 3,
|
|
1447
|
+
done: speedDone,
|
|
1448
|
+
total: stage2.length,
|
|
1449
|
+
message: `\u6D4B\u901F ${speedDone}/${stage2.length} ${result.peakMBps.toFixed(1)} MB/s`
|
|
1450
|
+
});
|
|
1451
|
+
return { ...item, speed: result.ok ? result.peakMBps : 0 };
|
|
1452
|
+
});
|
|
582
1453
|
} else {
|
|
583
|
-
report({ type: "stage", stage: 3, name: "\u771F\u5B9E\u4E0B\u884C\u901F\u5EA6\u6D4B\u8BD5", total: stage2.length });
|
|
1454
|
+
report({ type: "stage", stage: 3, name: "\u771F\u5B9E\u4E0B\u884C\u901F\u5EA6\u6D4B\u8BD5\uFF08\u9879\u76EE\u63A2\u9488\u7AEF\u70B9\uFF09", total: stage2.length });
|
|
584
1455
|
let speedDone = 0;
|
|
585
1456
|
speedResults = await pool(stage2, speedConcurrency, async (item) => {
|
|
586
1457
|
const result = await measureDownload(
|
|
@@ -599,16 +1470,19 @@ async function runScan(options) {
|
|
|
599
1470
|
total: stage2.length,
|
|
600
1471
|
message: `\u6D4B\u901F ${speedDone}/${stage2.length} ${result.speedMBps.toFixed(1)} MB/s`
|
|
601
1472
|
});
|
|
602
|
-
return { ...item,
|
|
1473
|
+
return { ...item, speed: result.ok ? result.speedMBps : 0 };
|
|
603
1474
|
});
|
|
604
1475
|
}
|
|
605
|
-
const entries = speedResults.filter((r) => options.skipSpeed ? true : Boolean(r.
|
|
1476
|
+
const entries = speedResults.filter((r) => options.skipSpeed ? true : Boolean(r.speed && r.speed > 0)).map((r) => ({
|
|
606
1477
|
ip: r.ip,
|
|
607
1478
|
port: options.port,
|
|
608
1479
|
colo: r.probe.colo || "UNK",
|
|
609
1480
|
latency: r.latency,
|
|
610
|
-
|
|
611
|
-
|
|
1481
|
+
tlsMs: r.tls.tlsMs,
|
|
1482
|
+
speed: r.speed ? Number(r.speed.toFixed(2)) : 0
|
|
1483
|
+
})).sort(
|
|
1484
|
+
(a, b) => options.skipSpeed ? a.tlsMs - b.tlsMs || a.latency - b.latency : b.speed - a.speed || a.tlsMs - b.tlsMs || a.latency - b.latency
|
|
1485
|
+
).slice(0, top);
|
|
612
1486
|
for (const entry of entries) {
|
|
613
1487
|
report({ type: "result", entry });
|
|
614
1488
|
}
|
|
@@ -617,9 +1491,13 @@ async function runScan(options) {
|
|
|
617
1491
|
stats: {
|
|
618
1492
|
candidates: candidates.length,
|
|
619
1493
|
tcpOk: tcpOk.length,
|
|
1494
|
+
tlsOk: tlsOk.length,
|
|
620
1495
|
wsOk: wsOk.length,
|
|
1496
|
+
verified: verifiedCount,
|
|
621
1497
|
speedOk: entries.length,
|
|
622
|
-
elapsedMs: Date.now() - started
|
|
1498
|
+
elapsedMs: Date.now() - started,
|
|
1499
|
+
countries: appliedCountries,
|
|
1500
|
+
wsFiltered: wsFiltered.length
|
|
623
1501
|
}
|
|
624
1502
|
};
|
|
625
1503
|
report({ type: "done", count: entries.length });
|
|
@@ -857,6 +1735,8 @@ async function handleRequest(req, res) {
|
|
|
857
1735
|
host: body.host.trim(),
|
|
858
1736
|
port: body.port ?? 443,
|
|
859
1737
|
candidates: plan.candidates,
|
|
1738
|
+
// 国家过滤(可多选):不传 = 全量;无效代码在 normalizeCountryFilter 里被丢弃
|
|
1739
|
+
countries: Array.isArray(body.countries) ? body.countries : void 0,
|
|
860
1740
|
top: body.top ?? 15,
|
|
861
1741
|
onProgress: (event) => broadcast(job, event)
|
|
862
1742
|
});
|
|
@@ -924,7 +1804,7 @@ async function handleRequest(req, res) {
|
|
|
924
1804
|
}
|
|
925
1805
|
|
|
926
1806
|
// src/version.ts
|
|
927
|
-
var VERSION = "0.
|
|
1807
|
+
var VERSION = "0.4.0";
|
|
928
1808
|
var NAME = "cfpilot-scanner";
|
|
929
1809
|
|
|
930
1810
|
// src/cli.ts
|
|
@@ -948,14 +1828,33 @@ scan \u9009\u9879:
|
|
|
948
1828
|
--no-callback \u53EA\u6253\u5370\u7ED3\u679C\uFF0C\u4E0D\u56DE\u586B
|
|
949
1829
|
--path <path> \u9636\u6BB5\u4E8C\u6821\u9A8C\u7684 WebSocket \u8DEF\u5F84\uFF0C\u9ED8\u8BA4 /__edge_probe
|
|
950
1830
|
\uFF08\u5BF9\u63A5\u5176\u5B83\u9879\u76EE\u65F6\u586B\u5B83\u81EA\u5DF1\u7684 WS \u8DEF\u5F84\uFF0C\u4F8B\u5982 /vless \u6216 /<uuid>\uFF09
|
|
951
|
-
--probe-path <path> \
|
|
952
|
-
--
|
|
1831
|
+
--probe-path <path> probe \u6A21\u5F0F\u7684\u6D4B\u901F\u8DEF\u5F84\uFF0C\u9ED8\u8BA4 /__edge_probe
|
|
1832
|
+
--speed-mode <mode> \u6D4B\u901F\u65B9\u5F0F\uFF1Aauto\uFF08\u9ED8\u8BA4\uFF09/ probe / url
|
|
1833
|
+
auto = \u76EE\u6807\u6709\u63A2\u9488\u7AEF\u70B9\u5C31\u7528\u5B83\uFF0C\u5426\u5219\u81EA\u52A8\u6539\u7528\u516C\u5F00\u6D4B\u901F\u5730\u5740
|
|
1834
|
+
probe = \u53EA\u7528\u76EE\u6807\u9879\u76EE\u7684\u63A2\u9488\u7AEF\u70B9\uFF08\u6700\u5FE0\u5B9E\uFF0C\u8981\u6C42\u5BF9\u65B9\u6709\u8BE5\u7AEF\u70B9\uFF09
|
|
1835
|
+
url = \u53EA\u7528\u516C\u5F00\u6D4B\u901F\u5730\u5740\uFF08\u4EFB\u4F55\u9879\u76EE\u90FD\u80FD\u6D4B\uFF09
|
|
1836
|
+
--speed-url <url> \u516C\u5F00\u6D4B\u901F\u5730\u5740\uFF0C\u9ED8\u8BA4 ${DEFAULT_SPEED_URL}
|
|
1837
|
+
\u53EF\u6362\u6210\u4EFB\u610F\u8FD4\u56DE\u5927\u6587\u4EF6\u7684\u5730\u5740
|
|
1838
|
+
--speed-conns <n> \u6D4B\u901F\u5E76\u53D1\u8FDE\u63A5\u6570\uFF0C\u9ED8\u8BA4 3
|
|
1839
|
+
\u5355\u6761 TCP \u8FDE\u63A5\u53D7\u62E5\u585E\u7A97\u53E3\u9650\u5236\uFF0C\u591A\u8FDE\u63A5\u66F4\u63A5\u8FD1\u771F\u5B9E\u5E26\u5BBD
|
|
1840
|
+
--speed-duration <ms> \u6D4B\u901F\u65F6\u957F\uFF0C\u9ED8\u8BA4 6000
|
|
1841
|
+
--no-verify \u9636\u6BB5\u4E8C\u53EA\u505A 101 \u63E1\u624B\uFF0C\u8DF3\u8FC7\u54CD\u5E94\u4F53\u6821\u9A8C
|
|
1842
|
+
\uFF08\u9ED8\u8BA4\u6821\u9A8C\uFF1A\u53D1 PING \u5E76\u9A8C\u8BC1\u6570\u636E\u9762\u56DE {"pong":true}\uFF09
|
|
1843
|
+
--no-speed \u8DF3\u8FC7\u6D4B\u901F\uFF0C\u53EA\u6309\u5EF6\u8FDF\u6392\u5E8F
|
|
953
1844
|
--top <n> \u8FD4\u56DE\u6761\u6570\uFF0C\u9ED8\u8BA4 15
|
|
954
|
-
--concurrency <n> \u9636\u6BB5\u4E00\u5E76\u53D1\uFF0C\u9ED8\u8BA4 300
|
|
1845
|
+
--concurrency <n> \u9636\u6BB5\u4E00 TCP \u5E76\u53D1\uFF0C\u9ED8\u8BA4 300
|
|
1846
|
+
--tls-concurrency <n> \u9636\u6BB5\u4E00 TLS \u63E1\u624B\u5E76\u53D1\uFF0C\u9ED8\u8BA4 64
|
|
1847
|
+
TLS \u63E1\u624B\u662F CPU \u5BC6\u96C6\u578B\u64CD\u4F5C\uFF0C\u8FC7\u9AD8\u4F1A\u8BA9\u672C\u673A\u6210\u4E3A\u74F6\u9888
|
|
955
1848
|
--sample <n> \u5019\u9009 IP \u91C7\u6837\u6570\uFF0C\u9ED8\u8BA4 20000
|
|
1849
|
+
\u5EFA\u8BAE 6556 \u8D77\uFF1A\u9636\u6BB5\u4E00\u6309 TLS \u63E1\u624B\u8017\u65F6\u7B5B\u9009\uFF0C\u800C\u63E1\u624B\u5FEB\u6162
|
|
1850
|
+
\u4E0E TCP \u5EF6\u8FDF\u51E0\u4E4E\u65E0\u5173\uFF08\u5B9E\u6D4B TCP 9ms \u7684 IP \u53EF\u80FD\u8981 3.5 \u79D2\uFF09\uFF0C
|
|
1851
|
+
\u91C7\u6837\u5C0F\u4E86\u6839\u672C\u78B0\u4E0D\u5230\u63E1\u624B\u5FEB\u7684 IP
|
|
1852
|
+
--countries <a,b> \u56FD\u5BB6 / \u5730\u533A\u8FC7\u6EE4\uFF08\u53EF\u591A\u9009\uFF0C\u9017\u53F7\u5206\u9694\uFF0C\u5982 HK,JP,SG\uFF09
|
|
1853
|
+
\u4E0D\u9009 = \u5168\u91CF\u3002\u673A\u623F\u6765\u81EA 101 \u63E1\u624B\u54CD\u5E94\uFF1B\u7B5B\u5B8C\u4E3A\u7A7A\u4F1A\u81EA\u52A8\u5FFD\u7565
|
|
956
1854
|
--cidr <a,b> \u81EA\u5B9A\u4E49\u7F51\u6BB5\uFF08\u53EF\u91CD\u590D\uFF0C\u7528\u9017\u53F7\u5206\u9694\uFF09
|
|
957
1855
|
--ip-file <path> \u4F7F\u7528\u672C\u5730 IP \u5217\u8868\u6587\u4EF6
|
|
958
|
-
--max-latency <ms> \u9636\u6BB5\u4E00\u5EF6\u8FDF\u4E0A\u9650\uFF0C\u9ED8\u8BA4 0\uFF08\u4E0D\u9650\uFF09
|
|
1856
|
+
--max-latency <ms> \u9636\u6BB5\u4E00 TCP \u5EF6\u8FDF\u4E0A\u9650\uFF0C\u9ED8\u8BA4 0\uFF08\u4E0D\u9650\uFF09
|
|
1857
|
+
--tls-timeout <ms> \u9636\u6BB5\u4E00 TLS \u63E1\u624B\u8D85\u65F6\uFF0C\u9ED8\u8BA4 4000
|
|
959
1858
|
--probe-limit <n> \u8FDB\u5165 101 \u6821\u9A8C\u7684\u5019\u9009\u6570\uFF0C\u9ED8\u8BA4 120
|
|
960
1859
|
--speed-limit <n> \u8FDB\u5165\u6D4B\u901F\u7684\u5019\u9009\u6570\uFF0C\u9ED8\u8BA4 30
|
|
961
1860
|
--speed-bytes <n> \u6D4B\u901F\u5B57\u8282\u6570\uFF0C\u9ED8\u8BA4 5242880
|
|
@@ -972,6 +1871,35 @@ serve \u9009\u9879:
|
|
|
972
1871
|
${NAME} scan --host cfpilot.example.com --token abc123
|
|
973
1872
|
${NAME} serve
|
|
974
1873
|
`;
|
|
1874
|
+
function normalizeWsPath(raw, fallback) {
|
|
1875
|
+
const value = (raw ?? fallback).trim();
|
|
1876
|
+
if (!value) return fallback;
|
|
1877
|
+
if (isMsysPollutedPath(value)) {
|
|
1878
|
+
process.stderr.write(
|
|
1879
|
+
`
|
|
1880
|
+
\u2717 --path \u88AB MSYS\uFF08Git-Bash\uFF09\u8F6C\u6362\u6210\u4E86 Windows \u8DEF\u5F84\uFF1A
|
|
1881
|
+
\u6536\u5230\u7684\u662F\uFF1A${value}
|
|
1882
|
+
|
|
1883
|
+
\u8FD9\u662F Git-Bash \u7684\u81EA\u52A8\u8DEF\u5F84\u8F6C\u6362\u884C\u4E3A\uFF0C\u4E0D\u662F\u672C\u5DE5\u5177\u7684\u95EE\u9898\u3002
|
|
1884
|
+
\u4E09\u79CD\u7ED5\u5F00\u65B9\u5F0F\uFF0C\u4EFB\u9009\u5176\u4E00\uFF1A
|
|
1885
|
+
1. \u7528\u7B49\u53F7\u5199\u6CD5\uFF1A --path=/vless
|
|
1886
|
+
2. \u52A0\u73AF\u5883\u53D8\u91CF\uFF1A MSYS_NO_PATHCONV=1 cfpilot-scanner scan ...
|
|
1887
|
+
3. \u6362\u7528 PowerShell / CMD \u6267\u884C
|
|
1888
|
+
`
|
|
1889
|
+
);
|
|
1890
|
+
process.exit(1);
|
|
1891
|
+
}
|
|
1892
|
+
if (!value.startsWith("/")) {
|
|
1893
|
+
process.stderr.write(
|
|
1894
|
+
`
|
|
1895
|
+
\u2717 --path \u5FC5\u987B\u4EE5 / \u5F00\u5934\uFF0C\u6536\u5230\u7684\u662F\uFF1A\u300C${value}\u300D
|
|
1896
|
+
\u4F8B\u5982\uFF1A--path /vless \u6216 --path /\uFF08\u76EE\u6807\u9879\u76EE\u7528\u6839\u8DEF\u5F84\u65F6\uFF09
|
|
1897
|
+
`
|
|
1898
|
+
);
|
|
1899
|
+
process.exit(1);
|
|
1900
|
+
}
|
|
1901
|
+
return value;
|
|
1902
|
+
}
|
|
975
1903
|
async function main() {
|
|
976
1904
|
const args = parseArgs(process.argv.slice(2));
|
|
977
1905
|
const command = args.command && !args.command.startsWith("-") ? args.command : "";
|
|
@@ -1067,15 +1995,16 @@ function printTable(entries) {
|
|
|
1067
1995
|
process.stdout.write("\n\u6CA1\u6709\u901A\u8FC7\u5168\u90E8\u6821\u9A8C\u7684\u8282\u70B9\u3002\n");
|
|
1068
1996
|
return;
|
|
1069
1997
|
}
|
|
1070
|
-
process.stdout.write("\n\u6392\u540D \u5730\u5740 \u673A\u623F
|
|
1071
|
-
process.stdout.write("\u2500".repeat(
|
|
1998
|
+
process.stdout.write("\n\u6392\u540D \u5730\u5740 \u673A\u623F TLS(ms) TCP(ms) \u4E0B\u884C(MB/s)\n");
|
|
1999
|
+
process.stdout.write("\u2500".repeat(66) + "\n");
|
|
1072
2000
|
entries.forEach((entry, index) => {
|
|
1073
2001
|
const rank = String(index + 1).padStart(2, " ");
|
|
1074
2002
|
const address = `${entry.ip}:${entry.port}`.padEnd(24, " ");
|
|
1075
2003
|
const colo = (entry.colo || "UNK").padEnd(6, " ");
|
|
2004
|
+
const tls3 = String(entry.tlsMs).padStart(7, " ");
|
|
1076
2005
|
const latency = String(entry.latency).padStart(7, " ");
|
|
1077
2006
|
const speed = entry.speed.toFixed(1).padStart(9, " ");
|
|
1078
|
-
process.stdout.write(`${rank} ${address} ${colo} ${latency} ${speed}
|
|
2007
|
+
process.stdout.write(`${rank} ${address} ${colo} ${tls3} ${latency} ${speed}
|
|
1079
2008
|
`);
|
|
1080
2009
|
});
|
|
1081
2010
|
}
|
|
@@ -1097,9 +2026,11 @@ async function commandScan(args) {
|
|
|
1097
2026
|
const top = flagNumber(args, "top", 15);
|
|
1098
2027
|
const token = flagString(args, "token");
|
|
1099
2028
|
const noCallback = flagBool(args, "no-callback");
|
|
1100
|
-
const wsPath = flagString(args, "path", "/__edge_probe");
|
|
1101
|
-
const probePath = flagString(args, "probe-path", "/__edge_probe");
|
|
2029
|
+
const wsPath = normalizeWsPath(flagString(args, "path"), "/__edge_probe");
|
|
2030
|
+
const probePath = normalizeWsPath(flagString(args, "probe-path"), "/__edge_probe");
|
|
1102
2031
|
const noSpeed = flagBool(args, "no-speed");
|
|
2032
|
+
const verifyPayload = !flagBool(args, "no-verify");
|
|
2033
|
+
const countries = normalizeCountryFilter(flagList(args, "countries"));
|
|
1103
2034
|
const callback = noCallback ? "" : flagString(args, "callback", token ? `https://${host}/api/v1/preferred-ips` : "");
|
|
1104
2035
|
if (!quiet) {
|
|
1105
2036
|
process.stdout.write(`
|
|
@@ -1107,8 +2038,31 @@ ${NAME} v${VERSION}
|
|
|
1107
2038
|
`);
|
|
1108
2039
|
process.stdout.write(`\u76EE\u6807\u57DF\u540D\uFF1A${host}:${port}\uFF08\u5C06\u4F5C\u4E3A TLS SNI \u4E0E HTTP Host\uFF09
|
|
1109
2040
|
`);
|
|
1110
|
-
process.stdout.write(
|
|
2041
|
+
process.stdout.write(`WS \u8DEF\u5F84\uFF1A${wsPath}
|
|
2042
|
+
`);
|
|
2043
|
+
if (countries.length > 0) {
|
|
2044
|
+
process.stdout.write(`\u56FD\u5BB6\u7B5B\u9009\uFF1A${countries.join(" / ")}\uFF08\u4E0D\u586B\u5219\u5168\u91CF\uFF09
|
|
2045
|
+
`);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
if (!quiet) {
|
|
2049
|
+
const env = await detectProxyInterference(host);
|
|
2050
|
+
if (!env.clean) {
|
|
2051
|
+
process.stdout.write("\n\u26A0\uFE0F \u68C0\u6D4B\u5230\u53EF\u80FD\u5E72\u6270\u4F18\u9009\u7684\u56E0\u7D20\uFF1A\n");
|
|
2052
|
+
for (const f of env.findings) {
|
|
2053
|
+
process.stdout.write(` ${f.message}
|
|
2054
|
+
`);
|
|
2055
|
+
process.stdout.write(` \u2192 ${f.advice}
|
|
2056
|
+
`);
|
|
2057
|
+
}
|
|
2058
|
+
process.stdout.write("\n \u4F18\u9009\u6D4B\u7684\u662F\u300C\u672C\u673A\u76F4\u8FDE\u8BE5 IP\u300D\u7684\u771F\u5B9E\u8D28\u91CF\u3002\n");
|
|
2059
|
+
process.stdout.write(" \u6D41\u91CF\u88AB\u4EE3\u7406\u63A5\u7BA1\u65F6\uFF0C\u7ED3\u679C\u770B\u8D77\u6765\u6B63\u5E38\uFF0C\u4F46\u6240\u6709 IP \u6210\u7EE9\u8D8B\u4E8E\u4E00\u81F4\uFF0C\n");
|
|
2060
|
+
process.stdout.write(" \u5BFC\u5165\u5BA2\u6237\u7AEF\u540E\u5927\u91CF -1\uFF08\u56E0\u4E3A\u4ECE\u672A\u771F\u6B63\u76F4\u8FDE\u9A8C\u8BC1\u8FC7\uFF09\u3002\n");
|
|
2061
|
+
process.stdout.write(" \u2192 \u5EFA\u8BAE\u5173\u95ED\u4EE3\u7406\u5DE5\u5177\uFF08\u542B TUN \u6A21\u5F0F\u4E0E\u7CFB\u7EDF\u4EE3\u7406\uFF09\u540E\u91CD\u8DD1\u3002\n");
|
|
2062
|
+
process.stdout.write(" \u2192 \u6216\u628A\u672C\u7A0B\u5E8F\u4E0E\u5F85\u6D4B IP \u6BB5\u8BBE\u4E3A\u76F4\u8FDE\u3002\n\n");
|
|
2063
|
+
}
|
|
1111
2064
|
}
|
|
2065
|
+
if (!quiet) process.stdout.write("\u6B63\u5728\u51C6\u5907 Cloudflare \u7F51\u6BB5\u2026\n");
|
|
1112
2066
|
const candidates = await resolveCandidatesFromArgs(args);
|
|
1113
2067
|
if (candidates.length === 0) {
|
|
1114
2068
|
process.stderr.write("\u5019\u9009 IP \u4E3A\u7A7A\uFF0C\u8BF7\u68C0\u67E5 --ip-file / --cidr \u53C2\u6570\n");
|
|
@@ -1117,38 +2071,69 @@ ${NAME} v${VERSION}
|
|
|
1117
2071
|
if (!quiet) process.stdout.write(`\u5019\u9009 IP\uFF1A${candidates.length} \u4E2A
|
|
1118
2072
|
`);
|
|
1119
2073
|
let skipSpeed = noSpeed;
|
|
2074
|
+
let speedMode = "probe";
|
|
2075
|
+
let speedUrl = flagString(args, "speed-url", DEFAULT_SPEED_URL);
|
|
1120
2076
|
if (!skipSpeed) {
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
2077
|
+
const forcedMode = flagString(args, "speed-mode");
|
|
2078
|
+
if (forcedMode === "url") {
|
|
2079
|
+
speedMode = "url";
|
|
2080
|
+
if (!quiet) process.stdout.write(`\u6D4B\u901F\u65B9\u5F0F\uFF1A\u516C\u5F00\u6D4B\u901F\u5730\u5740\uFF08${speedUrl}\uFF09
|
|
2081
|
+
`);
|
|
2082
|
+
} else if (forcedMode === "probe") {
|
|
2083
|
+
speedMode = "probe";
|
|
2084
|
+
} else {
|
|
2085
|
+
let hasProbe = false;
|
|
2086
|
+
try {
|
|
2087
|
+
const probe = await fetch(`https://${host}${probePath}?bytes=1024`, {
|
|
2088
|
+
method: "GET",
|
|
2089
|
+
signal: AbortSignal.timeout(8e3)
|
|
2090
|
+
});
|
|
2091
|
+
hasProbe = Boolean(probe.headers.get("x-probe-bytes"));
|
|
2092
|
+
} catch {
|
|
2093
|
+
hasProbe = false;
|
|
2094
|
+
}
|
|
2095
|
+
if (hasProbe) {
|
|
2096
|
+
speedMode = "probe";
|
|
2097
|
+
if (!quiet) process.stdout.write("\u6D4B\u901F\u65B9\u5F0F\uFF1A\u9879\u76EE\u63A2\u9488\u7AEF\u70B9\uFF08\u6700\u5FE0\u5B9E\uFF09\n");
|
|
2098
|
+
} else {
|
|
2099
|
+
speedMode = "url";
|
|
1128
2100
|
if (!quiet) {
|
|
1129
|
-
process.stdout.write(
|
|
1130
|
-
|
|
2101
|
+
process.stdout.write(`
|
|
2102
|
+
\u63D0\u793A\uFF1A${probePath} \u4E0D\u662F CFPilot \u63A2\u9488\u7AEF\u70B9\uFF0C\u6539\u7528\u516C\u5F00\u6D4B\u901F\u5730\u5740\u6D4B\u901F
|
|
2103
|
+
`);
|
|
2104
|
+
process.stdout.write(" \uFF08\u4ECD\u4F1A\u5148\u505A 101 \u6821\u9A8C\uFF0C\u56E0\u6B64\u4E0D\u4F1A\u9009\u51FA\u8FDE\u4E0D\u4E0A\u7684 IP\uFF09\n");
|
|
1131
2105
|
}
|
|
2106
|
+
speedUrl = await pickAvailableSpeedUrl(
|
|
2107
|
+
[speedUrl, ...SPEED_URL_PRESETS.map((p) => p.url)],
|
|
2108
|
+
quiet ? void 0 : (m) => process.stdout.write(` ${m}
|
|
2109
|
+
`)
|
|
2110
|
+
);
|
|
1132
2111
|
}
|
|
1133
|
-
} catch {
|
|
1134
|
-
skipSpeed = true;
|
|
1135
2112
|
}
|
|
1136
2113
|
}
|
|
1137
2114
|
const summary = await runScan({
|
|
1138
2115
|
host,
|
|
1139
2116
|
port,
|
|
1140
2117
|
candidates,
|
|
2118
|
+
countries,
|
|
1141
2119
|
wsPath,
|
|
1142
2120
|
probePath,
|
|
1143
2121
|
skipSpeed,
|
|
2122
|
+
verifyPayload,
|
|
1144
2123
|
concurrency: flagNumber(args, "concurrency", 300),
|
|
1145
2124
|
tcpTimeoutMs: flagNumber(args, "timeout", 1200),
|
|
2125
|
+
tlsConcurrency: flagNumber(args, "tls-concurrency", 64),
|
|
2126
|
+
tlsTimeoutMs: flagNumber(args, "tls-timeout", 4e3),
|
|
1146
2127
|
maxLatencyMs: flagNumber(args, "max-latency", 0),
|
|
1147
2128
|
probeLimit: flagNumber(args, "probe-limit", 120),
|
|
1148
2129
|
probeTimeoutMs: flagNumber(args, "probe-timeout", 3e3),
|
|
1149
2130
|
speedLimit: flagNumber(args, "speed-limit", 30),
|
|
1150
2131
|
speedTimeoutMs: flagNumber(args, "speed-timeout", 12e3),
|
|
1151
2132
|
speedBytes: flagNumber(args, "speed-bytes", 5 * 1024 * 1024),
|
|
2133
|
+
speedMode,
|
|
2134
|
+
speedUrl,
|
|
2135
|
+
speedConnections: flagNumber(args, "speed-conns", 3),
|
|
2136
|
+
speedDurationMs: flagNumber(args, "speed-duration", 6e3),
|
|
1152
2137
|
top,
|
|
1153
2138
|
onProgress: (event) => printProgress(event, quiet)
|
|
1154
2139
|
});
|