cfpilot-scanner 0.0.0-stage → 0.1.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 +88 -2
- package/dist/cli.js +1132 -0
- package/package.json +39 -4
package/README.md
CHANGED
|
@@ -1,3 +1,89 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cfpilot-scanner
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**CFPilot 本地真连接优选器** —— 用你自己的 Worker 域名做 SNI/Host,完成
|
|
4
|
+
**WebSocket 101 真连接校验**,只保留真正能连通的 Cloudflare 优选 IP,
|
|
5
|
+
彻底消灭客户端里的 `-1`。
|
|
6
|
+
|
|
7
|
+
> 绝大多数"优选 IP"工具只做 TCP 握手(端口通就算可用),
|
|
8
|
+
> 结果填进客户端大量 `-1`。本工具会真的发起 TLS + WebSocket 升级,
|
|
9
|
+
> 走完你自己 Worker 的入站路径,只有返回 `101` 的 IP 才会被保留。
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx -y cfpilot-scanner --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
无需安装,`npx` 直接运行;也可 `npm i -g cfpilot-scanner`。
|
|
18
|
+
|
|
19
|
+
## 快速开始
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# 1. 在你的面板里生成一次性扫描令牌(有效期短,用完即废)
|
|
23
|
+
# 2. 执行优选(域名必须是绑好自定义域名的那个)
|
|
24
|
+
npx -y cfpilot-scanner scan --host cfpilot.example.com --token <令牌>
|
|
25
|
+
|
|
26
|
+
# 结果会自动回填到面板的「手动优选 IP」池
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
只打印不回填:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx -y cfpilot-scanner scan --host cfpilot.example.com --no-callback --top 20
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 三阶扫描
|
|
36
|
+
|
|
37
|
+
| 阶段 | 做什么 | 默认开销 |
|
|
38
|
+
|------|--------|----------|
|
|
39
|
+
| 一阶 · 延迟探测 | 采样大量 IP 做 TCP 连接测延迟 | 采样 20000,并发 300 |
|
|
40
|
+
| 二阶 · 真连接校验 | 对候选发起 **TLS + WebSocket 101** 握手 | 120 个候选 |
|
|
41
|
+
| 三阶 · 下行测速 | 通过 `/__edge_probe` 流式下载测真实速度 | 30 个候选,5 MB |
|
|
42
|
+
|
|
43
|
+
只有通过二阶的 IP 才算"可用",三阶用于排序。
|
|
44
|
+
|
|
45
|
+
## 常用参数
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
--host <domain> 你的 Worker 自定义域名(SNI 与 Host)
|
|
49
|
+
--token <token> 面板生成的一次性令牌
|
|
50
|
+
--top <n> 保留条数(默认 15)
|
|
51
|
+
--concurrency <n> 一阶并发(默认 300)
|
|
52
|
+
--sample <n> 候选采样数(默认 20000)
|
|
53
|
+
--cidr <a,b> 自定义网段
|
|
54
|
+
--ip-file <path> 使用本地 IP 列表(支持 CloudflareSpeedTest 的 result.csv)
|
|
55
|
+
--max-latency <ms> 一阶延迟上限
|
|
56
|
+
--json 以 JSON 输出
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 本地面板桥接
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx -y cfpilot-scanner serve
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
在 `127.0.0.1:19527` 起一个本地服务,面板里点「本地真连接优选」会自动唤醒它,
|
|
66
|
+
通过 SSE 实时推送进度,不需要手动复制令牌。
|
|
67
|
+
|
|
68
|
+
## 输出示例
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
阶段一:采样 20000 个候选,并发 300 …
|
|
72
|
+
阶段二:120 个候选进入 101 校验 …
|
|
73
|
+
阶段三:30 个候选测速(5 MB)…
|
|
74
|
+
✅ 可用 14 个,已回填面板
|
|
75
|
+
|
|
76
|
+
104.17.242.168:443 12ms 18.4 MB/s
|
|
77
|
+
104.19.73.179:443 18ms 15.1 MB/s
|
|
78
|
+
...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 为什么需要它
|
|
82
|
+
|
|
83
|
+
随机抽取的 Cloudflare 官方 IP 里,能真正完成 WebSocket 升级的比例只有约 1/4,
|
|
84
|
+
而且**因地区、运营商、时间而异**。只有用你自己的域名实测过,
|
|
85
|
+
才能得到当前网络下真正可用的那批 IP。
|
|
86
|
+
|
|
87
|
+
## 许可
|
|
88
|
+
|
|
89
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
// src/args.ts
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
const flags = {};
|
|
9
|
+
const positionals = [];
|
|
10
|
+
let command = "";
|
|
11
|
+
for (let i = 0; i < argv.length; i++) {
|
|
12
|
+
const arg = argv[i];
|
|
13
|
+
if (arg.startsWith("--")) {
|
|
14
|
+
const body = arg.slice(2);
|
|
15
|
+
const eq = body.indexOf("=");
|
|
16
|
+
if (eq >= 0) {
|
|
17
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const next = argv[i + 1];
|
|
21
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
22
|
+
flags[body] = next;
|
|
23
|
+
i++;
|
|
24
|
+
} else {
|
|
25
|
+
flags[body] = true;
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (arg.startsWith("-") && arg.length === 2) {
|
|
30
|
+
const next = argv[i + 1];
|
|
31
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
32
|
+
flags[arg.slice(1)] = next;
|
|
33
|
+
i++;
|
|
34
|
+
} else {
|
|
35
|
+
flags[arg.slice(1)] = true;
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!command) {
|
|
40
|
+
command = arg;
|
|
41
|
+
} else {
|
|
42
|
+
positionals.push(arg);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { command, flags, positionals };
|
|
46
|
+
}
|
|
47
|
+
function flagString(args, key, fallback = "") {
|
|
48
|
+
const value = args.flags[key];
|
|
49
|
+
if (typeof value === "string") return value;
|
|
50
|
+
return fallback;
|
|
51
|
+
}
|
|
52
|
+
function flagNumber(args, key, fallback) {
|
|
53
|
+
const value = args.flags[key];
|
|
54
|
+
if (typeof value === "string") {
|
|
55
|
+
const n = Number(value);
|
|
56
|
+
if (Number.isFinite(n)) return n;
|
|
57
|
+
}
|
|
58
|
+
return fallback;
|
|
59
|
+
}
|
|
60
|
+
function flagBool(args, key) {
|
|
61
|
+
const value = args.flags[key];
|
|
62
|
+
if (typeof value === "boolean") return value;
|
|
63
|
+
if (typeof value === "string") {
|
|
64
|
+
return !["0", "false", "no", "off"].includes(value.toLowerCase());
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
function flagList(args, key) {
|
|
69
|
+
const value = args.flags[key];
|
|
70
|
+
if (typeof value !== "string") return [];
|
|
71
|
+
return value.split(/[\n,]+/).map((v) => v.trim()).filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/ip-ranges.ts
|
|
75
|
+
var BUILTIN_IPV4_CIDRS = [
|
|
76
|
+
"173.245.48.0/20",
|
|
77
|
+
"103.21.244.0/22",
|
|
78
|
+
"103.22.200.0/22",
|
|
79
|
+
"103.31.4.0/22",
|
|
80
|
+
"141.101.64.0/18",
|
|
81
|
+
"108.162.192.0/18",
|
|
82
|
+
"190.93.240.0/20",
|
|
83
|
+
"188.114.96.0/20",
|
|
84
|
+
"197.234.240.0/22",
|
|
85
|
+
"198.41.128.0/17",
|
|
86
|
+
"162.158.0.0/15",
|
|
87
|
+
"104.16.0.0/13",
|
|
88
|
+
"104.24.0.0/14",
|
|
89
|
+
"172.64.0.0/13",
|
|
90
|
+
"131.0.72.0/22"
|
|
91
|
+
];
|
|
92
|
+
var BUILTIN_IPV6_CIDRS = [
|
|
93
|
+
"2400:cb00::/32",
|
|
94
|
+
"2606:4700::/32",
|
|
95
|
+
"2803:f800::/32",
|
|
96
|
+
"2405:b500::/32",
|
|
97
|
+
"2405:8100::/32",
|
|
98
|
+
"2a06:98c0::/29",
|
|
99
|
+
"2c0f:f248::/32"
|
|
100
|
+
];
|
|
101
|
+
async function fetchCloudflareCidrs(timeoutMs = 8e3) {
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
104
|
+
try {
|
|
105
|
+
const [v4, v6] = await Promise.all([
|
|
106
|
+
fetch("https://www.cloudflare.com/ips-v4", { signal: controller.signal }),
|
|
107
|
+
fetch("https://www.cloudflare.com/ips-v6", { signal: controller.signal })
|
|
108
|
+
]);
|
|
109
|
+
const parse = (text) => text.split("\n").map((l) => l.trim()).filter((l) => l.includes("/"));
|
|
110
|
+
return {
|
|
111
|
+
ipv4: v4.ok ? parse(await v4.text()) : BUILTIN_IPV4_CIDRS,
|
|
112
|
+
ipv6: v6.ok ? parse(await v6.text()) : BUILTIN_IPV6_CIDRS
|
|
113
|
+
};
|
|
114
|
+
} finally {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function makeRandom(seed) {
|
|
119
|
+
let a = seed >>> 0;
|
|
120
|
+
return () => {
|
|
121
|
+
a |= 0;
|
|
122
|
+
a = a + 1831565813 | 0;
|
|
123
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
124
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
125
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function parseCidr(cidr) {
|
|
129
|
+
const [addr, prefixText] = cidr.trim().split("/");
|
|
130
|
+
if (!addr) return null;
|
|
131
|
+
const prefix = Number(prefixText);
|
|
132
|
+
if (!Number.isInteger(prefix)) return null;
|
|
133
|
+
if (addr.includes(":")) {
|
|
134
|
+
if (prefix < 0 || prefix > 128) return null;
|
|
135
|
+
const base = ipv6ToBigInt(addr);
|
|
136
|
+
if (base === null) return null;
|
|
137
|
+
const hostBits2 = 128 - prefix;
|
|
138
|
+
const size2 = 1n << BigInt(hostBits2);
|
|
139
|
+
const mask2 = hostBits2 === 0 ? 0n : (1n << BigInt(hostBits2)) - 1n;
|
|
140
|
+
return { family: 6, base: base & ~mask2, prefix, size: size2 };
|
|
141
|
+
}
|
|
142
|
+
if (prefix < 0 || prefix > 32) return null;
|
|
143
|
+
const value = ipv4ToBigInt(addr);
|
|
144
|
+
if (value === null) return null;
|
|
145
|
+
const hostBits = 32 - prefix;
|
|
146
|
+
const size = 1n << BigInt(hostBits);
|
|
147
|
+
const mask = hostBits === 0 ? 0n : (1n << BigInt(hostBits)) - 1n;
|
|
148
|
+
return { family: 4, base: value & ~mask, prefix, size };
|
|
149
|
+
}
|
|
150
|
+
function ipv4ToBigInt(addr) {
|
|
151
|
+
const parts = addr.split(".");
|
|
152
|
+
if (parts.length !== 4) return null;
|
|
153
|
+
let value = 0n;
|
|
154
|
+
for (const part of parts) {
|
|
155
|
+
if (!/^\d{1,3}$/.test(part)) return null;
|
|
156
|
+
const n = Number(part);
|
|
157
|
+
if (n > 255) return null;
|
|
158
|
+
value = value << 8n | BigInt(n);
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
function ipv6ToBigInt(addr) {
|
|
163
|
+
try {
|
|
164
|
+
const parts = addr.split("::");
|
|
165
|
+
if (parts.length > 2) return null;
|
|
166
|
+
const head = parts[0] ? parts[0].split(":") : [];
|
|
167
|
+
const tail = parts.length === 2 && parts[1] ? parts[1].split(":") : [];
|
|
168
|
+
const groups = [...head, ...Array(8 - head.length - tail.length).fill("0"), ...tail];
|
|
169
|
+
if (groups.length !== 8) return null;
|
|
170
|
+
let value = 0n;
|
|
171
|
+
for (const group of groups) {
|
|
172
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(group)) return null;
|
|
173
|
+
value = value << 16n | BigInt(parseInt(group, 16));
|
|
174
|
+
}
|
|
175
|
+
return value;
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function bigIntToIpv4(value) {
|
|
181
|
+
return [24n, 16n, 8n, 0n].map((shift) => (value >> shift & 0xffn).toString()).join(".");
|
|
182
|
+
}
|
|
183
|
+
function bigIntToIpv6(value) {
|
|
184
|
+
const groups = [];
|
|
185
|
+
for (let i = 7; i >= 0; i--) {
|
|
186
|
+
groups.push((value >> BigInt(i * 16) & 0xffffn).toString(16));
|
|
187
|
+
}
|
|
188
|
+
let bestStart = -1;
|
|
189
|
+
let bestLen = 0;
|
|
190
|
+
let start = -1;
|
|
191
|
+
let len = 0;
|
|
192
|
+
for (let i = 0; i < groups.length; i++) {
|
|
193
|
+
if (groups[i] === "0") {
|
|
194
|
+
if (start < 0) start = i;
|
|
195
|
+
len++;
|
|
196
|
+
if (len > bestLen) {
|
|
197
|
+
bestLen = len;
|
|
198
|
+
bestStart = start;
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
start = -1;
|
|
202
|
+
len = 0;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (bestLen < 2) return groups.join(":");
|
|
206
|
+
const head = groups.slice(0, bestStart).join(":");
|
|
207
|
+
const tail = groups.slice(bestStart + bestLen).join(":");
|
|
208
|
+
return `${head}::${tail}`;
|
|
209
|
+
}
|
|
210
|
+
function expandCidrs(cidrs, options = {}) {
|
|
211
|
+
const sample = options.sample ?? 2e4;
|
|
212
|
+
const random = makeRandom(options.seed ?? 24301);
|
|
213
|
+
const parsed = cidrs.map(parseCidr).filter((c) => c !== null);
|
|
214
|
+
if (parsed.length === 0) return [];
|
|
215
|
+
const totalSize = parsed.reduce((sum, c) => sum + c.size, 0n);
|
|
216
|
+
const out = [];
|
|
217
|
+
for (const cidr of parsed) {
|
|
218
|
+
if (sample <= 0 || cidr.size <= 2000n) {
|
|
219
|
+
for (let i = 0n; i < cidr.size; i++) {
|
|
220
|
+
const value = cidr.base + i;
|
|
221
|
+
out.push(cidr.family === 4 ? bigIntToIpv4(value) : bigIntToIpv6(value));
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const ratio = Number(totalSize === 0n ? 1n : cidr.size * BigInt(sample) / totalSize);
|
|
226
|
+
const count = Math.max(64, Math.min(2e4, ratio));
|
|
227
|
+
const step = Number(cidr.size / BigInt(count));
|
|
228
|
+
const offset = Math.floor(random() * step);
|
|
229
|
+
for (let i = 0; i < count; i++) {
|
|
230
|
+
const offsetValue = BigInt(offset + Math.floor(i * step));
|
|
231
|
+
if (offsetValue >= cidr.size) break;
|
|
232
|
+
const value = cidr.base + offsetValue;
|
|
233
|
+
out.push(cidr.family === 4 ? bigIntToIpv4(value) : bigIntToIpv6(value));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
function readIpFile(text) {
|
|
239
|
+
const out = [];
|
|
240
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
241
|
+
let line = raw.trim();
|
|
242
|
+
if (!line || line.startsWith("#")) continue;
|
|
243
|
+
if (line.includes(",")) line = line.split(",")[0].trim();
|
|
244
|
+
const hash = line.indexOf("#");
|
|
245
|
+
if (hash >= 0) line = line.slice(0, hash).trim();
|
|
246
|
+
if (!line) continue;
|
|
247
|
+
const bracket = /^\[([^\]]+)\](?::\d+)?$/.exec(line);
|
|
248
|
+
if (bracket) {
|
|
249
|
+
out.push(bracket[1]);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (line.includes(":") && line.split(":").length > 2) {
|
|
253
|
+
out.push(line);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (line.includes(":")) {
|
|
257
|
+
const [host] = line.split(":");
|
|
258
|
+
if (host) out.push(host);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
out.push(line);
|
|
262
|
+
}
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/candidates.ts
|
|
267
|
+
var DEFAULT_CANDIDATE_SAMPLE = 2e4;
|
|
268
|
+
var DEFAULT_CANDIDATE_SEED = 24301;
|
|
269
|
+
var DEFAULT_BRIDGE_SAMPLE = 300;
|
|
270
|
+
function normalizeCidrInput(cidr) {
|
|
271
|
+
if (cidr === void 0) return [];
|
|
272
|
+
const list = Array.isArray(cidr) ? cidr : [cidr];
|
|
273
|
+
const out = [];
|
|
274
|
+
for (const item of list) {
|
|
275
|
+
if (typeof item !== "string") continue;
|
|
276
|
+
for (const part of item.split(/[\n,]+/)) {
|
|
277
|
+
const value = part.trim();
|
|
278
|
+
if (value) out.push(value);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
function isIpv6Cidr(cidr) {
|
|
284
|
+
return cidr.includes(":");
|
|
285
|
+
}
|
|
286
|
+
function filterByFamily(cidrs, family) {
|
|
287
|
+
if (family === "both") return cidrs;
|
|
288
|
+
const wantIpv6 = family === "v6";
|
|
289
|
+
return cidrs.filter((cidr) => isIpv6Cidr(cidr) === wantIpv6);
|
|
290
|
+
}
|
|
291
|
+
async function loadCloudflareCidrs(family) {
|
|
292
|
+
let ipv4 = BUILTIN_IPV4_CIDRS;
|
|
293
|
+
let ipv6 = BUILTIN_IPV6_CIDRS;
|
|
294
|
+
try {
|
|
295
|
+
const ranges = await fetchCloudflareCidrs();
|
|
296
|
+
if (ranges.ipv4.length > 0) ipv4 = ranges.ipv4;
|
|
297
|
+
if (ranges.ipv6.length > 0) ipv6 = ranges.ipv6;
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
if (family === "v4") return [...ipv4];
|
|
301
|
+
if (family === "v6") return [...ipv6];
|
|
302
|
+
return [...ipv4, ...ipv6];
|
|
303
|
+
}
|
|
304
|
+
async function resolveCandidates(opts = {}) {
|
|
305
|
+
const sample = opts.sample ?? DEFAULT_CANDIDATE_SAMPLE;
|
|
306
|
+
const seed = opts.seed ?? DEFAULT_CANDIDATE_SEED;
|
|
307
|
+
const family = opts.family ?? "both";
|
|
308
|
+
const customCidrs = normalizeCidrInput(opts.cidr);
|
|
309
|
+
if (customCidrs.length > 0) {
|
|
310
|
+
return expandCidrs(filterByFamily(customCidrs, family), { sample, seed });
|
|
311
|
+
}
|
|
312
|
+
const cidrs = await loadCloudflareCidrs(family);
|
|
313
|
+
return expandCidrs(cidrs, { sample, seed });
|
|
314
|
+
}
|
|
315
|
+
function normalizeCandidateList(input) {
|
|
316
|
+
if (!Array.isArray(input) || input.length === 0) return [];
|
|
317
|
+
const lines = input.filter((item) => typeof item === "string");
|
|
318
|
+
if (lines.length === 0) return [];
|
|
319
|
+
const seen = /* @__PURE__ */ new Set();
|
|
320
|
+
const out = [];
|
|
321
|
+
for (const ip of readIpFile(lines.join("\n"))) {
|
|
322
|
+
if (seen.has(ip)) continue;
|
|
323
|
+
seen.add(ip);
|
|
324
|
+
out.push(ip);
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/scan.ts
|
|
330
|
+
import * as net from "node:net";
|
|
331
|
+
|
|
332
|
+
// src/true-probe.ts
|
|
333
|
+
import * as tls from "node:tls";
|
|
334
|
+
import * as crypto from "node:crypto";
|
|
335
|
+
var USER_AGENT = "CFPilot-Scanner/1.0 (+https://github.com/xmwengxing/CFPilot)";
|
|
336
|
+
function extractColo(text) {
|
|
337
|
+
const match = /cf-ray:\s*[0-9a-f]+-([A-Z]{3})/i.exec(text);
|
|
338
|
+
return match ? match[1].toUpperCase() : "";
|
|
339
|
+
}
|
|
340
|
+
function probeWs101(ip, port, host, timeoutMs = 3e3) {
|
|
341
|
+
return new Promise((resolve) => {
|
|
342
|
+
const started = Date.now();
|
|
343
|
+
const key = crypto.randomBytes(16).toString("base64");
|
|
344
|
+
let settled = false;
|
|
345
|
+
let received = "";
|
|
346
|
+
const finish = (result) => {
|
|
347
|
+
if (settled) return;
|
|
348
|
+
settled = true;
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
try {
|
|
351
|
+
socket.destroy();
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
resolve({ ...result, latencyMs: Date.now() - started });
|
|
355
|
+
};
|
|
356
|
+
const request = [
|
|
357
|
+
`GET /__edge_probe HTTP/1.1`,
|
|
358
|
+
`Host: ${host}`,
|
|
359
|
+
`Upgrade: websocket`,
|
|
360
|
+
`Connection: Upgrade`,
|
|
361
|
+
`Sec-WebSocket-Key: ${key}`,
|
|
362
|
+
`Sec-WebSocket-Version: 13`,
|
|
363
|
+
`User-Agent: ${USER_AGENT}`,
|
|
364
|
+
"",
|
|
365
|
+
""
|
|
366
|
+
].join("\r\n");
|
|
367
|
+
const socket = tls.connect(
|
|
368
|
+
{
|
|
369
|
+
host: ip,
|
|
370
|
+
port,
|
|
371
|
+
servername: host,
|
|
372
|
+
rejectUnauthorized: false,
|
|
373
|
+
ALPNProtocols: ["http/1.1"]
|
|
374
|
+
},
|
|
375
|
+
() => {
|
|
376
|
+
socket.write(request);
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
const timer = setTimeout(() => {
|
|
380
|
+
finish({ ok: false, colo: "", status: 0, error: "\u63E1\u624B\u8D85\u65F6" });
|
|
381
|
+
}, timeoutMs);
|
|
382
|
+
socket.setTimeout(timeoutMs, () => {
|
|
383
|
+
finish({ ok: false, colo: "", status: 0, error: "\u8FDE\u63A5\u8D85\u65F6" });
|
|
384
|
+
});
|
|
385
|
+
socket.on("error", (err) => {
|
|
386
|
+
finish({ ok: false, colo: "", status: 0, error: err.message });
|
|
387
|
+
});
|
|
388
|
+
socket.on("data", (chunk) => {
|
|
389
|
+
received += chunk.toString("utf8");
|
|
390
|
+
const lineEnd = received.indexOf("\r\n");
|
|
391
|
+
const statusLine = lineEnd >= 0 ? received.slice(0, lineEnd) : received;
|
|
392
|
+
const statusMatch = /^HTTP\/1\.[01]\s+(\d{3})/.exec(statusLine);
|
|
393
|
+
if (statusMatch) {
|
|
394
|
+
const status = Number(statusMatch[1]);
|
|
395
|
+
if (status === 101) {
|
|
396
|
+
finish({ ok: true, colo: extractColo(received), status });
|
|
397
|
+
} else {
|
|
398
|
+
finish({ ok: false, colo: extractColo(received), status, error: `HTTP ${status}` });
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (received.length > 4096) {
|
|
403
|
+
finish({ ok: false, colo: "", status: 0, error: "\u54CD\u5E94\u5F02\u5E38" });
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function measureDownload(ip, port, host, bytes, timeoutMs = 12e3) {
|
|
409
|
+
return new Promise((resolve) => {
|
|
410
|
+
const started = Date.now();
|
|
411
|
+
let headerEnd = -1;
|
|
412
|
+
let received = 0;
|
|
413
|
+
let settled = false;
|
|
414
|
+
let buffer = "";
|
|
415
|
+
const finish = (result) => {
|
|
416
|
+
if (settled) return;
|
|
417
|
+
settled = true;
|
|
418
|
+
clearTimeout(timer);
|
|
419
|
+
try {
|
|
420
|
+
socket.destroy();
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
resolve({ ...result, ms: Date.now() - started });
|
|
424
|
+
};
|
|
425
|
+
const request = [
|
|
426
|
+
`GET /__edge_probe?bytes=${bytes} HTTP/1.1`,
|
|
427
|
+
`Host: ${host}`,
|
|
428
|
+
`User-Agent: ${USER_AGENT}`,
|
|
429
|
+
`Accept: */*`,
|
|
430
|
+
"",
|
|
431
|
+
""
|
|
432
|
+
].join("\r\n");
|
|
433
|
+
const socket = tls.connect(
|
|
434
|
+
{
|
|
435
|
+
host: ip,
|
|
436
|
+
port,
|
|
437
|
+
servername: host,
|
|
438
|
+
rejectUnauthorized: false,
|
|
439
|
+
ALPNProtocols: ["http/1.1"]
|
|
440
|
+
},
|
|
441
|
+
() => {
|
|
442
|
+
socket.write(request);
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
const timer = setTimeout(() => {
|
|
446
|
+
const seconds = Math.max(1e-3, (Date.now() - started) / 1e3);
|
|
447
|
+
finish({
|
|
448
|
+
ok: received > 0,
|
|
449
|
+
bytes: received,
|
|
450
|
+
speedMBps: received / 1024 / 1024 / seconds,
|
|
451
|
+
error: received > 0 ? void 0 : "\u6D4B\u901F\u8D85\u65F6"
|
|
452
|
+
});
|
|
453
|
+
}, timeoutMs);
|
|
454
|
+
socket.setTimeout(timeoutMs, () => {
|
|
455
|
+
const seconds = Math.max(1e-3, (Date.now() - started) / 1e3);
|
|
456
|
+
finish({
|
|
457
|
+
ok: received > 0,
|
|
458
|
+
bytes: received,
|
|
459
|
+
speedMBps: received / 1024 / 1024 / seconds,
|
|
460
|
+
error: received > 0 ? void 0 : "\u6D4B\u901F\u8D85\u65F6"
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
socket.on("error", (err) => {
|
|
464
|
+
finish({ ok: false, bytes: received, speedMBps: 0, error: err.message });
|
|
465
|
+
});
|
|
466
|
+
socket.on("data", (chunk) => {
|
|
467
|
+
if (headerEnd < 0) {
|
|
468
|
+
buffer += chunk.toString("latin1");
|
|
469
|
+
const idx = buffer.indexOf("\r\n\r\n");
|
|
470
|
+
if (idx < 0) {
|
|
471
|
+
if (buffer.length > 8192) finish({ ok: false, bytes: 0, speedMBps: 0, error: "\u54CD\u5E94\u5934\u5F02\u5E38" });
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
headerEnd = idx + 4;
|
|
475
|
+
const body = Buffer.from(buffer, "latin1").subarray(headerEnd);
|
|
476
|
+
received += body.byteLength;
|
|
477
|
+
headerEnd = 0;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
received += chunk.byteLength;
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/scan.ts
|
|
486
|
+
async function tcpPing(ip, port, timeoutMs) {
|
|
487
|
+
return new Promise((resolve) => {
|
|
488
|
+
const started = Date.now();
|
|
489
|
+
let settled = false;
|
|
490
|
+
const socket = new net.Socket();
|
|
491
|
+
const done = (ok) => {
|
|
492
|
+
if (settled) return;
|
|
493
|
+
settled = true;
|
|
494
|
+
try {
|
|
495
|
+
socket.destroy();
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
resolve({ ip, ok, latency: Date.now() - started });
|
|
499
|
+
};
|
|
500
|
+
socket.setTimeout(timeoutMs, () => done(false));
|
|
501
|
+
socket.once("connect", () => done(true));
|
|
502
|
+
socket.once("error", () => done(false));
|
|
503
|
+
socket.connect(port, ip);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
async function pool(items, concurrency, worker) {
|
|
507
|
+
const out = new Array(items.length);
|
|
508
|
+
let cursor = 0;
|
|
509
|
+
const runners = Array.from({ length: Math.max(1, Math.min(concurrency, items.length)) }, async () => {
|
|
510
|
+
for (; ; ) {
|
|
511
|
+
const index = cursor++;
|
|
512
|
+
if (index >= items.length) return;
|
|
513
|
+
out[index] = await worker(items[index], index);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
await Promise.all(runners);
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
async function runScan(options) {
|
|
520
|
+
const started = Date.now();
|
|
521
|
+
const concurrency = options.concurrency ?? 300;
|
|
522
|
+
const tcpTimeoutMs = options.tcpTimeoutMs ?? 1200;
|
|
523
|
+
const maxLatencyMs = options.maxLatencyMs ?? 0;
|
|
524
|
+
const probeLimit = options.probeLimit ?? 120;
|
|
525
|
+
const probeConcurrency = options.probeConcurrency ?? 24;
|
|
526
|
+
const probeTimeoutMs = options.probeTimeoutMs ?? 3e3;
|
|
527
|
+
const speedLimit = options.speedLimit ?? 30;
|
|
528
|
+
const speedConcurrency = options.speedConcurrency ?? 4;
|
|
529
|
+
const speedTimeoutMs = options.speedTimeoutMs ?? 12e3;
|
|
530
|
+
const speedBytes = options.speedBytes ?? 5 * 1024 * 1024;
|
|
531
|
+
const top = options.top ?? 15;
|
|
532
|
+
const candidates = options.candidates;
|
|
533
|
+
const report = options.onProgress ?? (() => {
|
|
534
|
+
});
|
|
535
|
+
report({ type: "stage", stage: 1, name: "TCP \u63E1\u624B\u521D\u7B5B", total: candidates.length });
|
|
536
|
+
let done = 0;
|
|
537
|
+
const tcpResults = await pool(candidates, concurrency, async (ip) => {
|
|
538
|
+
const result = await tcpPing(ip, options.port, tcpTimeoutMs);
|
|
539
|
+
done++;
|
|
540
|
+
if (done % 500 === 0 || done === candidates.length) {
|
|
541
|
+
report({
|
|
542
|
+
type: "progress",
|
|
543
|
+
stage: 1,
|
|
544
|
+
done,
|
|
545
|
+
total: candidates.length,
|
|
546
|
+
message: `TCP \u521D\u7B5B ${done}/${candidates.length}`
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
return result;
|
|
550
|
+
});
|
|
551
|
+
const tcpOk = tcpResults.filter((r) => r.ok);
|
|
552
|
+
const filtered = maxLatencyMs > 0 ? tcpOk.filter((r) => r.latency <= maxLatencyMs) : tcpOk;
|
|
553
|
+
const stage1 = filtered.sort((a, b) => a.latency - b.latency).slice(0, probeLimit);
|
|
554
|
+
report({ type: "stage", stage: 2, name: "WebSocket 101 \u771F\u8FDE\u63A5\u6821\u9A8C", total: stage1.length });
|
|
555
|
+
let probeDone = 0;
|
|
556
|
+
const probeResults = await pool(stage1, probeConcurrency, async (item) => {
|
|
557
|
+
const result = await probeWs101(item.ip, options.port, options.host, probeTimeoutMs);
|
|
558
|
+
probeDone++;
|
|
559
|
+
report({
|
|
560
|
+
type: "progress",
|
|
561
|
+
stage: 2,
|
|
562
|
+
done: probeDone,
|
|
563
|
+
total: stage1.length,
|
|
564
|
+
message: `101 \u6821\u9A8C ${probeDone}/${stage1.length}${result.ok ? ` \u2713 ${result.colo}` : ""}`
|
|
565
|
+
});
|
|
566
|
+
return { ...item, probe: result };
|
|
567
|
+
});
|
|
568
|
+
const wsOk = probeResults.filter((r) => r.probe.ok);
|
|
569
|
+
const stage2 = wsOk.sort((a, b) => a.latency - b.latency).slice(0, speedLimit);
|
|
570
|
+
report({ type: "stage", stage: 3, name: "\u771F\u5B9E\u4E0B\u884C\u901F\u5EA6\u6D4B\u8BD5", total: stage2.length });
|
|
571
|
+
let speedDone = 0;
|
|
572
|
+
const speedResults = await pool(stage2, speedConcurrency, async (item) => {
|
|
573
|
+
const result = await measureDownload(
|
|
574
|
+
item.ip,
|
|
575
|
+
options.port,
|
|
576
|
+
options.host,
|
|
577
|
+
speedBytes,
|
|
578
|
+
speedTimeoutMs
|
|
579
|
+
);
|
|
580
|
+
speedDone++;
|
|
581
|
+
report({
|
|
582
|
+
type: "progress",
|
|
583
|
+
stage: 3,
|
|
584
|
+
done: speedDone,
|
|
585
|
+
total: stage2.length,
|
|
586
|
+
message: `\u6D4B\u901F ${speedDone}/${stage2.length} ${result.speedMBps.toFixed(1)} MB/s`
|
|
587
|
+
});
|
|
588
|
+
return { ...item, download: result };
|
|
589
|
+
});
|
|
590
|
+
const entries = speedResults.filter((r) => r.download.ok && r.download.speedMBps > 0).map((r) => ({
|
|
591
|
+
ip: r.ip,
|
|
592
|
+
port: options.port,
|
|
593
|
+
colo: r.probe.colo || "UNK",
|
|
594
|
+
latency: r.latency,
|
|
595
|
+
speed: Number(r.download.speedMBps.toFixed(1))
|
|
596
|
+
})).sort((a, b) => b.speed - a.speed || a.latency - b.latency).slice(0, top);
|
|
597
|
+
for (const entry of entries) {
|
|
598
|
+
report({ type: "result", entry });
|
|
599
|
+
}
|
|
600
|
+
const summary = {
|
|
601
|
+
entries,
|
|
602
|
+
stats: {
|
|
603
|
+
candidates: candidates.length,
|
|
604
|
+
tcpOk: tcpOk.length,
|
|
605
|
+
wsOk: wsOk.length,
|
|
606
|
+
speedOk: entries.length,
|
|
607
|
+
elapsedMs: Date.now() - started
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
report({ type: "done", count: entries.length });
|
|
611
|
+
return summary;
|
|
612
|
+
}
|
|
613
|
+
function formatEntries(entries) {
|
|
614
|
+
return entries.map((e) => `${e.ip}:${e.port}#${e.colo}_${e.latency}_${e.speed}`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/callback.ts
|
|
618
|
+
async function postResults(callbackUrl, token, entries, timeoutMs = 15e3) {
|
|
619
|
+
try {
|
|
620
|
+
const response = await fetch(callbackUrl, {
|
|
621
|
+
method: "POST",
|
|
622
|
+
headers: {
|
|
623
|
+
"Content-Type": "application/json",
|
|
624
|
+
Authorization: `Bearer ${token}`,
|
|
625
|
+
"User-Agent": "CFPilot-Scanner/1.0"
|
|
626
|
+
},
|
|
627
|
+
body: JSON.stringify({ ips: entries }),
|
|
628
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
629
|
+
});
|
|
630
|
+
if (!response.ok) {
|
|
631
|
+
const text = await response.text().catch(() => "");
|
|
632
|
+
return { ok: false, status: response.status, error: `\u56DE\u4F20\u5931\u8D25 HTTP ${response.status} ${text}`.trim() };
|
|
633
|
+
}
|
|
634
|
+
return { ok: true, status: response.status };
|
|
635
|
+
} catch (err) {
|
|
636
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/server.ts
|
|
641
|
+
import { createServer } from "node:http";
|
|
642
|
+
var BRIDGE_VERSION = "0.1.0";
|
|
643
|
+
var DEFAULT_BRIDGE_PORT = 19527;
|
|
644
|
+
function normalizeSample(value) {
|
|
645
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
646
|
+
return Math.min(Math.floor(value), DEFAULT_CANDIDATE_SAMPLE);
|
|
647
|
+
}
|
|
648
|
+
return DEFAULT_BRIDGE_SAMPLE;
|
|
649
|
+
}
|
|
650
|
+
function normalizeSeed(value) {
|
|
651
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
|
|
652
|
+
return void 0;
|
|
653
|
+
}
|
|
654
|
+
function normalizeFamily(value) {
|
|
655
|
+
return value === "v4" || value === "v6" || value === "both" ? value : void 0;
|
|
656
|
+
}
|
|
657
|
+
async function resolveScanCandidates(body) {
|
|
658
|
+
const manual = normalizeCandidateList(body.manual);
|
|
659
|
+
if (manual.length > 0) return { candidates: manual, source: "manual" };
|
|
660
|
+
const requested = normalizeCandidateList(body.candidates);
|
|
661
|
+
if (requested.length > 0) return { candidates: requested, source: "request" };
|
|
662
|
+
const candidates = await resolveCandidates({
|
|
663
|
+
sample: normalizeSample(body.sample),
|
|
664
|
+
cidr: body.cidr,
|
|
665
|
+
seed: normalizeSeed(body.seed),
|
|
666
|
+
family: normalizeFamily(body.family)
|
|
667
|
+
});
|
|
668
|
+
return { candidates, source: "generated" };
|
|
669
|
+
}
|
|
670
|
+
function describePlan(plan) {
|
|
671
|
+
if (plan.source === "manual") return `\u4F7F\u7528\u9762\u677F\u624B\u52A8\u6307\u5B9A\u7684 ${plan.candidates.length} \u4E2A\u5019\u9009 IP`;
|
|
672
|
+
if (plan.source === "request") return `\u4F7F\u7528\u8BF7\u6C42\u643A\u5E26\u7684 ${plan.candidates.length} \u4E2A\u5019\u9009 IP`;
|
|
673
|
+
return `\u5DF2\u81EA\u52A8\u751F\u6210 ${plan.candidates.length} \u4E2A\u5019\u9009 IP`;
|
|
674
|
+
}
|
|
675
|
+
var jobs = /* @__PURE__ */ new Map();
|
|
676
|
+
var jobCounter = 0;
|
|
677
|
+
function corsHeaders() {
|
|
678
|
+
return {
|
|
679
|
+
"Access-Control-Allow-Origin": "*",
|
|
680
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
681
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
682
|
+
"Access-Control-Max-Age": "86400"
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function sendJson(res, status, data) {
|
|
686
|
+
const body = JSON.stringify(data);
|
|
687
|
+
res.writeHead(status, {
|
|
688
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
689
|
+
"Content-Length": Buffer.byteLength(body),
|
|
690
|
+
...corsHeaders()
|
|
691
|
+
});
|
|
692
|
+
res.end(body);
|
|
693
|
+
}
|
|
694
|
+
async function readBody(req) {
|
|
695
|
+
const chunks = [];
|
|
696
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
697
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
698
|
+
}
|
|
699
|
+
function parseStartScanRequest(text) {
|
|
700
|
+
try {
|
|
701
|
+
const raw = JSON.parse(text || "{}");
|
|
702
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
703
|
+
return raw;
|
|
704
|
+
} catch {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function eventToSse(event) {
|
|
709
|
+
switch (event.type) {
|
|
710
|
+
case "stage":
|
|
711
|
+
return `event: stage
|
|
712
|
+
data: ${JSON.stringify({ stage: event.stage, name: event.name, total: event.total })}
|
|
713
|
+
|
|
714
|
+
`;
|
|
715
|
+
case "progress":
|
|
716
|
+
return `event: progress
|
|
717
|
+
data: ${JSON.stringify({
|
|
718
|
+
stage: event.stage,
|
|
719
|
+
done: event.done,
|
|
720
|
+
total: event.total,
|
|
721
|
+
percent: event.total > 0 ? Math.round(event.done / event.total * 100) : 0,
|
|
722
|
+
message: event.message
|
|
723
|
+
})}
|
|
724
|
+
|
|
725
|
+
`;
|
|
726
|
+
case "result":
|
|
727
|
+
return `event: result
|
|
728
|
+
data: ${JSON.stringify({ entry: event.entry })}
|
|
729
|
+
|
|
730
|
+
`;
|
|
731
|
+
case "done":
|
|
732
|
+
return `event: done
|
|
733
|
+
data: ${JSON.stringify({ count: event.count })}
|
|
734
|
+
|
|
735
|
+
`;
|
|
736
|
+
case "error":
|
|
737
|
+
default:
|
|
738
|
+
return `event: error
|
|
739
|
+
data: ${JSON.stringify({ message: event.message })}
|
|
740
|
+
|
|
741
|
+
`;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function broadcast(job, event) {
|
|
745
|
+
job.events.push(event);
|
|
746
|
+
const payload = eventToSse(event);
|
|
747
|
+
for (const subscriber of job.subscribers) {
|
|
748
|
+
try {
|
|
749
|
+
subscriber.write(payload);
|
|
750
|
+
} catch {
|
|
751
|
+
job.subscribers.delete(subscriber);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function startBridge(options = {}) {
|
|
756
|
+
const host = options.host ?? "127.0.0.1";
|
|
757
|
+
const port = options.port ?? DEFAULT_BRIDGE_PORT;
|
|
758
|
+
const server = createServer((req, res) => {
|
|
759
|
+
void handleRequest(req, res).catch((err) => {
|
|
760
|
+
sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
return new Promise((resolve, reject) => {
|
|
764
|
+
server.once("error", reject);
|
|
765
|
+
server.listen(port, host, () => {
|
|
766
|
+
const address = server.address();
|
|
767
|
+
const boundPort = typeof address === "object" && address !== null ? address.port : port;
|
|
768
|
+
resolve({
|
|
769
|
+
server,
|
|
770
|
+
port: boundPort,
|
|
771
|
+
close: () => new Promise((done) => {
|
|
772
|
+
for (const job of jobs.values()) {
|
|
773
|
+
for (const subscriber of job.subscribers) subscriber.end();
|
|
774
|
+
job.subscribers.clear();
|
|
775
|
+
}
|
|
776
|
+
server.close(() => done());
|
|
777
|
+
})
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
async function handleRequest(req, res) {
|
|
783
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
784
|
+
if (req.method === "OPTIONS") {
|
|
785
|
+
res.writeHead(204, corsHeaders());
|
|
786
|
+
res.end();
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (url.pathname === "/status") {
|
|
790
|
+
const active = [...jobs.values()].filter((j) => !j.finished);
|
|
791
|
+
const last = [...jobs.values()].sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
792
|
+
sendJson(res, 200, {
|
|
793
|
+
ok: true,
|
|
794
|
+
version: BRIDGE_VERSION,
|
|
795
|
+
busy: active.length > 0,
|
|
796
|
+
lastJobId: last?.id ?? null,
|
|
797
|
+
lastCount: last?.result.length ?? 0
|
|
798
|
+
});
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (url.pathname === "/start-scan" && req.method === "POST") {
|
|
802
|
+
const body = parseStartScanRequest(await readBody(req));
|
|
803
|
+
if (!body) {
|
|
804
|
+
sendJson(res, 400, { ok: false, error: "\u8BF7\u6C42\u4F53\u89E3\u6790\u5931\u8D25\uFF08\u9700\u8981 JSON \u5BF9\u8C61\uFF09" });
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
if (typeof body.host !== "string" || body.host.trim() === "") {
|
|
808
|
+
sendJson(res, 400, { ok: false, error: "\u7F3A\u5C11 host \u53C2\u6570" });
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const job = {
|
|
812
|
+
id: `job-${Date.now()}-${++jobCounter}`,
|
|
813
|
+
startedAt: Date.now(),
|
|
814
|
+
events: [],
|
|
815
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
816
|
+
result: [],
|
|
817
|
+
finished: false
|
|
818
|
+
};
|
|
819
|
+
jobs.set(job.id, job);
|
|
820
|
+
void (async () => {
|
|
821
|
+
try {
|
|
822
|
+
const plan = await resolveScanCandidates(body);
|
|
823
|
+
if (plan.candidates.length === 0) {
|
|
824
|
+
broadcast(job, {
|
|
825
|
+
type: "error",
|
|
826
|
+
message: "\u6CA1\u6709\u53EF\u7528\u7684\u5019\u9009 IP\uFF1A\u8BF7\u68C0\u67E5 candidates / manual / cidr \u53C2\u6570"
|
|
827
|
+
});
|
|
828
|
+
job.finished = true;
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
broadcast(job, {
|
|
832
|
+
type: "progress",
|
|
833
|
+
stage: 0,
|
|
834
|
+
done: 0,
|
|
835
|
+
total: plan.candidates.length,
|
|
836
|
+
message: describePlan(plan)
|
|
837
|
+
});
|
|
838
|
+
const summary = await runScan({
|
|
839
|
+
host: body.host.trim(),
|
|
840
|
+
port: body.port ?? 443,
|
|
841
|
+
candidates: plan.candidates,
|
|
842
|
+
top: body.top ?? 15,
|
|
843
|
+
onProgress: (event) => broadcast(job, event)
|
|
844
|
+
});
|
|
845
|
+
job.result = summary.entries;
|
|
846
|
+
if (body.callback && body.token) {
|
|
847
|
+
const result = await postResults(body.callback, body.token, formatEntries(summary.entries));
|
|
848
|
+
broadcast(
|
|
849
|
+
job,
|
|
850
|
+
result.ok ? { type: "done", count: summary.entries.length } : { type: "error", message: `\u56DE\u4F20\u5931\u8D25\uFF1A${result.error ?? "\u672A\u77E5\u9519\u8BEF"}` }
|
|
851
|
+
);
|
|
852
|
+
} else {
|
|
853
|
+
broadcast(job, { type: "done", count: summary.entries.length });
|
|
854
|
+
}
|
|
855
|
+
} catch (err) {
|
|
856
|
+
broadcast(job, { type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
857
|
+
} finally {
|
|
858
|
+
job.finished = true;
|
|
859
|
+
for (const subscriber of job.subscribers) {
|
|
860
|
+
try {
|
|
861
|
+
subscriber.end();
|
|
862
|
+
} catch {
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
job.subscribers.clear();
|
|
866
|
+
}
|
|
867
|
+
})();
|
|
868
|
+
sendJson(res, 200, { ok: true, jobId: job.id });
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (url.pathname === "/events") {
|
|
872
|
+
const jobId = url.searchParams.get("jobId") ?? "";
|
|
873
|
+
const job = jobs.get(jobId) ?? [...jobs.values()].sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
874
|
+
if (!job) {
|
|
875
|
+
sendJson(res, 404, { ok: false, error: "\u6CA1\u6709\u6B63\u5728\u8FD0\u884C\u7684\u4EFB\u52A1" });
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
res.writeHead(200, {
|
|
879
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
880
|
+
"Cache-Control": "no-store",
|
|
881
|
+
Connection: "keep-alive",
|
|
882
|
+
"X-Accel-Buffering": "no",
|
|
883
|
+
...corsHeaders()
|
|
884
|
+
});
|
|
885
|
+
for (const event of job.events) res.write(eventToSse(event));
|
|
886
|
+
if (job.finished) {
|
|
887
|
+
res.end();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
job.subscribers.add(res);
|
|
891
|
+
req.on("close", () => {
|
|
892
|
+
job.subscribers.delete(res);
|
|
893
|
+
});
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (url.pathname === "/results") {
|
|
897
|
+
const job = [...jobs.values()].sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
898
|
+
sendJson(res, 200, {
|
|
899
|
+
ok: true,
|
|
900
|
+
entries: job?.result ?? [],
|
|
901
|
+
lines: formatEntries(job?.result ?? [])
|
|
902
|
+
});
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
sendJson(res, 404, { ok: false, error: "\u63A5\u53E3\u4E0D\u5B58\u5728" });
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/version.ts
|
|
909
|
+
var VERSION = "0.1.0";
|
|
910
|
+
var NAME = "cfpilot-scanner";
|
|
911
|
+
|
|
912
|
+
// src/cli.ts
|
|
913
|
+
var HELP = `
|
|
914
|
+
${NAME} v${VERSION}
|
|
915
|
+
CFPilot \u672C\u5730\u771F\u8FDE\u63A5\u4F18\u9009\u5668 \u2014\u2014 \u7528\u4F60\u81EA\u5DF1\u7684 Worker \u57DF\u540D\u505A SNI/Host\uFF0C
|
|
916
|
+
\u5B8C\u6210 WebSocket 101 \u771F\u8FDE\u63A5\u6821\u9A8C\uFF0C\u53EA\u4FDD\u7559\u771F\u6B63\u53EF\u7528\u7684 IP\u3002
|
|
917
|
+
|
|
918
|
+
\u7528\u6CD5:
|
|
919
|
+
${NAME} scan [\u9009\u9879] \u6267\u884C\u4E09\u9636\u4F18\u9009\u626B\u63CF\uFF08\u9ED8\u8BA4\u547D\u4EE4\uFF09
|
|
920
|
+
${NAME} serve [\u9009\u9879] \u542F\u52A8\u672C\u5730\u6865\u63A5\u670D\u52A1\uFF08\u4F9B\u9762\u677F\u4E00\u952E\u5524\u9192\uFF09
|
|
921
|
+
${NAME} ips [\u9009\u9879] \u6253\u5370 Cloudflare \u5B98\u65B9\u7F51\u6BB5
|
|
922
|
+
${NAME} --help \u663E\u793A\u5E2E\u52A9
|
|
923
|
+
${NAME} --version \u663E\u793A\u7248\u672C
|
|
924
|
+
|
|
925
|
+
scan \u9009\u9879:
|
|
926
|
+
--host <domain> \u5FC5\u586B\uFF1A\u4F60\u7684 Worker \u81EA\u5B9A\u4E49\u57DF\u540D\uFF08\u4F5C\u4E3A TLS SNI \u4E0E HTTP Host\uFF09
|
|
927
|
+
--port <port> \u7AEF\u53E3\uFF0C\u9ED8\u8BA4 443
|
|
928
|
+
--token <token> \u56DE\u586B\u4EE4\u724C\uFF08\u9762\u677F\u751F\u6210\u7684\u4E00\u6B21\u6027\u4EE4\u724C\u6216 SCANNER_TOKEN\uFF09
|
|
929
|
+
--callback <url> \u56DE\u586B\u5730\u5740\uFF0C\u9ED8\u8BA4 https://<host>/api/v1/preferred-ips
|
|
930
|
+
--no-callback \u53EA\u6253\u5370\u7ED3\u679C\uFF0C\u4E0D\u56DE\u586B
|
|
931
|
+
--top <n> \u8FD4\u56DE\u6761\u6570\uFF0C\u9ED8\u8BA4 15
|
|
932
|
+
--concurrency <n> \u9636\u6BB5\u4E00\u5E76\u53D1\uFF0C\u9ED8\u8BA4 300
|
|
933
|
+
--sample <n> \u5019\u9009 IP \u91C7\u6837\u6570\uFF0C\u9ED8\u8BA4 20000
|
|
934
|
+
--cidr <a,b> \u81EA\u5B9A\u4E49\u7F51\u6BB5\uFF08\u53EF\u91CD\u590D\uFF0C\u7528\u9017\u53F7\u5206\u9694\uFF09
|
|
935
|
+
--ip-file <path> \u4F7F\u7528\u672C\u5730 IP \u5217\u8868\u6587\u4EF6
|
|
936
|
+
--max-latency <ms> \u9636\u6BB5\u4E00\u5EF6\u8FDF\u4E0A\u9650\uFF0C\u9ED8\u8BA4 0\uFF08\u4E0D\u9650\uFF09
|
|
937
|
+
--probe-limit <n> \u8FDB\u5165 101 \u6821\u9A8C\u7684\u5019\u9009\u6570\uFF0C\u9ED8\u8BA4 120
|
|
938
|
+
--speed-limit <n> \u8FDB\u5165\u6D4B\u901F\u7684\u5019\u9009\u6570\uFF0C\u9ED8\u8BA4 30
|
|
939
|
+
--speed-bytes <n> \u6D4B\u901F\u5B57\u8282\u6570\uFF0C\u9ED8\u8BA4 5242880
|
|
940
|
+
--timeout <ms> \u9636\u6BB5\u4E00\u8D85\u65F6\uFF0C\u9ED8\u8BA4 1200
|
|
941
|
+
--seed <n> \u91C7\u6837\u968F\u673A\u79CD\u5B50\uFF0C\u9ED8\u8BA4 24301
|
|
942
|
+
--json \u4EE5 JSON \u8F93\u51FA\u7ED3\u679C
|
|
943
|
+
--quiet \u4EC5\u8F93\u51FA\u7ED3\u679C
|
|
944
|
+
|
|
945
|
+
serve \u9009\u9879:
|
|
946
|
+
--port <port> \u76D1\u542C\u7AEF\u53E3\uFF0C\u9ED8\u8BA4 ${DEFAULT_BRIDGE_PORT}
|
|
947
|
+
--host <addr> \u76D1\u542C\u5730\u5740\uFF0C\u9ED8\u8BA4 127.0.0.1
|
|
948
|
+
|
|
949
|
+
\u793A\u4F8B:
|
|
950
|
+
${NAME} scan --host cfpilot.example.com --token abc123
|
|
951
|
+
${NAME} serve
|
|
952
|
+
`;
|
|
953
|
+
async function main() {
|
|
954
|
+
const args = parseArgs(process.argv.slice(2));
|
|
955
|
+
const command = args.command && !args.command.startsWith("-") ? args.command : "";
|
|
956
|
+
if (flagBool(args, "h") || flagBool(args, "help") || command === "help") {
|
|
957
|
+
process.stdout.write(HELP);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (flagBool(args, "v") || flagBool(args, "version") || command === "version") {
|
|
961
|
+
process.stdout.write(`${VERSION}
|
|
962
|
+
`);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
switch (command) {
|
|
966
|
+
case "serve":
|
|
967
|
+
await commandServe(args);
|
|
968
|
+
return;
|
|
969
|
+
case "ips":
|
|
970
|
+
commandIps(args);
|
|
971
|
+
return;
|
|
972
|
+
case "scan":
|
|
973
|
+
await commandScan(args);
|
|
974
|
+
return;
|
|
975
|
+
default:
|
|
976
|
+
await commandScan(args);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function commandIps(args) {
|
|
980
|
+
const includeIpv6 = !flagBool(args, "no-ipv6");
|
|
981
|
+
const cidrs = [...BUILTIN_IPV4_CIDRS, ...includeIpv6 ? BUILTIN_IPV6_CIDRS : []];
|
|
982
|
+
process.stdout.write(`${cidrs.join("\n")}
|
|
983
|
+
`);
|
|
984
|
+
}
|
|
985
|
+
async function commandServe(args) {
|
|
986
|
+
const port = flagNumber(args, "port", DEFAULT_BRIDGE_PORT);
|
|
987
|
+
const host = flagString(args, "host", "127.0.0.1");
|
|
988
|
+
const handle = await startBridge({ port, host });
|
|
989
|
+
process.stdout.write(
|
|
990
|
+
[
|
|
991
|
+
"",
|
|
992
|
+
` ${NAME} \u672C\u5730\u6865\u63A5\u670D\u52A1\u5DF2\u542F\u52A8`,
|
|
993
|
+
` \u76D1\u542C\u5730\u5740\uFF1Ahttp://${host}:${handle.port}`,
|
|
994
|
+
` \u7248\u672C\uFF1Av${BRIDGE_VERSION}`,
|
|
995
|
+
" \u73B0\u5728\u53EF\u4EE5\u5728 CFPilot \u9762\u677F\u70B9\u51FB\u300C\u4E00\u952E\u672C\u5730\u771F\u8FDE\u63A5\u4F18\u9009\u300D\u3002",
|
|
996
|
+
" \u6309 Ctrl+C \u9000\u51FA\u3002",
|
|
997
|
+
""
|
|
998
|
+
].join("\n")
|
|
999
|
+
);
|
|
1000
|
+
const shutdown = () => {
|
|
1001
|
+
void handle.close().then(() => process.exit(0));
|
|
1002
|
+
};
|
|
1003
|
+
process.on("SIGINT", shutdown);
|
|
1004
|
+
process.on("SIGTERM", shutdown);
|
|
1005
|
+
}
|
|
1006
|
+
async function resolveCandidatesFromArgs(args) {
|
|
1007
|
+
const ipFile = flagString(args, "ip-file");
|
|
1008
|
+
if (ipFile) {
|
|
1009
|
+
const text = readFileSync(ipFile, "utf8");
|
|
1010
|
+
return readIpFile(text);
|
|
1011
|
+
}
|
|
1012
|
+
return resolveCandidates({
|
|
1013
|
+
sample: flagNumber(args, "sample", 2e4),
|
|
1014
|
+
cidr: flagList(args, "cidr"),
|
|
1015
|
+
seed: flagNumber(args, "seed", 24301)
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
function printProgress(event, quiet) {
|
|
1019
|
+
if (quiet) return;
|
|
1020
|
+
switch (event.type) {
|
|
1021
|
+
case "stage":
|
|
1022
|
+
process.stdout.write(`
|
|
1023
|
+
\u25B6 \u9636\u6BB5 ${event.stage}\uFF1A${event.name}\uFF08\u5171 ${event.total} \u4E2A\u5019\u9009\uFF09
|
|
1024
|
+
`);
|
|
1025
|
+
break;
|
|
1026
|
+
case "progress":
|
|
1027
|
+
if (event.message) process.stdout.write(`\r ${event.message}`.padEnd(78));
|
|
1028
|
+
break;
|
|
1029
|
+
case "done":
|
|
1030
|
+
process.stdout.write(`
|
|
1031
|
+
\u2713 \u4F18\u9009\u5B8C\u6210\uFF0C\u5171 ${event.count} \u6761
|
|
1032
|
+
`);
|
|
1033
|
+
break;
|
|
1034
|
+
case "error":
|
|
1035
|
+
process.stdout.write(`
|
|
1036
|
+
\u2717 ${event.message}
|
|
1037
|
+
`);
|
|
1038
|
+
break;
|
|
1039
|
+
default:
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function printTable(entries) {
|
|
1044
|
+
if (entries.length === 0) {
|
|
1045
|
+
process.stdout.write("\n\u6CA1\u6709\u901A\u8FC7\u5168\u90E8\u6821\u9A8C\u7684\u8282\u70B9\u3002\n");
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
process.stdout.write("\n\u6392\u540D \u5730\u5740 \u673A\u623F \u5EF6\u8FDF(ms) \u4E0B\u884C(MB/s)\n");
|
|
1049
|
+
process.stdout.write("\u2500".repeat(58) + "\n");
|
|
1050
|
+
entries.forEach((entry, index) => {
|
|
1051
|
+
const rank = String(index + 1).padStart(2, " ");
|
|
1052
|
+
const address = `${entry.ip}:${entry.port}`.padEnd(24, " ");
|
|
1053
|
+
const colo = (entry.colo || "UNK").padEnd(6, " ");
|
|
1054
|
+
const latency = String(entry.latency).padStart(7, " ");
|
|
1055
|
+
const speed = entry.speed.toFixed(1).padStart(9, " ");
|
|
1056
|
+
process.stdout.write(`${rank} ${address} ${colo} ${latency} ${speed}
|
|
1057
|
+
`);
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
async function commandScan(args) {
|
|
1061
|
+
const host = flagString(args, "host");
|
|
1062
|
+
const quiet = flagBool(args, "quiet");
|
|
1063
|
+
const asJson = flagBool(args, "json");
|
|
1064
|
+
if (!host) {
|
|
1065
|
+
process.stderr.write("\u7F3A\u5C11 --host \u53C2\u6570\uFF08\u4F60\u7684 Worker \u81EA\u5B9A\u4E49\u57DF\u540D\uFF09\n");
|
|
1066
|
+
process.stderr.write('\u63D0\u793A\uFF1Anpx -y cfpilot-scanner@latest --host "\u4F60\u7684\u57DF\u540D" --token "\u4EE4\u724C"\n');
|
|
1067
|
+
process.exit(1);
|
|
1068
|
+
}
|
|
1069
|
+
const port = flagNumber(args, "port", 443);
|
|
1070
|
+
const top = flagNumber(args, "top", 15);
|
|
1071
|
+
const token = flagString(args, "token");
|
|
1072
|
+
const noCallback = flagBool(args, "no-callback");
|
|
1073
|
+
const callback = noCallback ? "" : flagString(args, "callback", token ? `https://${host}/api/v1/preferred-ips` : "");
|
|
1074
|
+
if (!quiet) {
|
|
1075
|
+
process.stdout.write(`
|
|
1076
|
+
${NAME} v${VERSION}
|
|
1077
|
+
`);
|
|
1078
|
+
process.stdout.write(`\u76EE\u6807\u57DF\u540D\uFF1A${host}:${port}\uFF08\u5C06\u4F5C\u4E3A TLS SNI \u4E0E HTTP Host\uFF09
|
|
1079
|
+
`);
|
|
1080
|
+
process.stdout.write("\u6B63\u5728\u51C6\u5907 Cloudflare \u7F51\u6BB5\u2026\n");
|
|
1081
|
+
}
|
|
1082
|
+
const candidates = await resolveCandidatesFromArgs(args);
|
|
1083
|
+
if (candidates.length === 0) {
|
|
1084
|
+
process.stderr.write("\u5019\u9009 IP \u4E3A\u7A7A\uFF0C\u8BF7\u68C0\u67E5 --ip-file / --cidr \u53C2\u6570\n");
|
|
1085
|
+
process.exit(1);
|
|
1086
|
+
}
|
|
1087
|
+
if (!quiet) process.stdout.write(`\u5019\u9009 IP\uFF1A${candidates.length} \u4E2A
|
|
1088
|
+
`);
|
|
1089
|
+
const summary = await runScan({
|
|
1090
|
+
host,
|
|
1091
|
+
port,
|
|
1092
|
+
candidates,
|
|
1093
|
+
concurrency: flagNumber(args, "concurrency", 300),
|
|
1094
|
+
tcpTimeoutMs: flagNumber(args, "timeout", 1200),
|
|
1095
|
+
maxLatencyMs: flagNumber(args, "max-latency", 0),
|
|
1096
|
+
probeLimit: flagNumber(args, "probe-limit", 120),
|
|
1097
|
+
probeTimeoutMs: flagNumber(args, "probe-timeout", 3e3),
|
|
1098
|
+
speedLimit: flagNumber(args, "speed-limit", 30),
|
|
1099
|
+
speedTimeoutMs: flagNumber(args, "speed-timeout", 12e3),
|
|
1100
|
+
speedBytes: flagNumber(args, "speed-bytes", 5 * 1024 * 1024),
|
|
1101
|
+
top,
|
|
1102
|
+
onProgress: (event) => printProgress(event, quiet)
|
|
1103
|
+
});
|
|
1104
|
+
if (asJson) {
|
|
1105
|
+
process.stdout.write(
|
|
1106
|
+
`${JSON.stringify({ entries: summary.entries, stats: summary.stats }, null, 2)}
|
|
1107
|
+
`
|
|
1108
|
+
);
|
|
1109
|
+
} else {
|
|
1110
|
+
printTable(summary.entries);
|
|
1111
|
+
}
|
|
1112
|
+
if (callback && token) {
|
|
1113
|
+
const result = await postResults(callback, token, formatEntries(summary.entries));
|
|
1114
|
+
if (result.ok) {
|
|
1115
|
+
process.stdout.write(`
|
|
1116
|
+
\u2713 \u5DF2\u56DE\u586B ${summary.entries.length} \u6761\u5230\u9762\u677F
|
|
1117
|
+
`);
|
|
1118
|
+
} else {
|
|
1119
|
+
process.stderr.write(`
|
|
1120
|
+
\u2717 \u56DE\u586B\u5931\u8D25\uFF1A${result.error}
|
|
1121
|
+
`);
|
|
1122
|
+
process.exitCode = 1;
|
|
1123
|
+
}
|
|
1124
|
+
} else if (!quiet) {
|
|
1125
|
+
process.stdout.write("\n\uFF08\u672A\u63D0\u4F9B --token\uFF0C\u8DF3\u8FC7\u56DE\u586B\uFF09\n");
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
main().catch((err) => {
|
|
1129
|
+
process.stderr.write(`\u6267\u884C\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
1130
|
+
`);
|
|
1131
|
+
process.exit(1);
|
|
1132
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cfpilot-scanner",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CFPilot 本地真连接优选器:用你自己的 Worker 域名做 SNI/Host 完成 WebSocket 101 真连接校验,彻底消灭 -1 节点",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "CFPilot Contributors",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/xmwengxing/CFPilot.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"cloudflare",
|
|
13
|
+
"ip",
|
|
14
|
+
"optimize",
|
|
15
|
+
"scanner",
|
|
16
|
+
"vless",
|
|
17
|
+
"websocket",
|
|
18
|
+
"cfpilot"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"cfpilot-scanner": "dist/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.17.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "node scripts/build.mjs",
|
|
33
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
34
|
+
"prepublishOnly": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^26.6.5",
|
|
38
|
+
"esbuild": "^0.28.2",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
40
|
+
}
|
|
41
|
+
}
|