block-proxy 0.1.11 → 0.1.13

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.
Files changed (62) hide show
  1. package/.agents/skills/commit/skill.md +40 -0
  2. package/.claude/settings.local.json +29 -1
  3. package/.claude/skills/build-client/skill.md +24 -0
  4. package/.claude/skills/commit/skill.md +34 -26
  5. package/.claude/skills/release-client/skill.md +68 -0
  6. package/CLAUDE.md +109 -47
  7. package/Dockerfile +1 -1
  8. package/README.md +69 -60
  9. package/build/asset-manifest.json +6 -6
  10. package/build/index.html +1 -1
  11. package/build/static/css/main.3f317ce6.css +2 -0
  12. package/build/static/css/main.3f317ce6.css.map +1 -0
  13. package/build/static/js/{main.2247fb80.js → main.68f66be0.js} +3 -3
  14. package/build/static/js/main.68f66be0.js.map +1 -0
  15. package/client/app.py +312 -0
  16. package/client/build.sh +84 -0
  17. package/client/config.py +49 -0
  18. package/client/config_window.py +155 -0
  19. package/client/icons/app.icns +0 -0
  20. package/client/icons/app_example.png +0 -0
  21. package/client/icons/app_icon.png +0 -0
  22. package/client/icons/backup/app_example.png +0 -0
  23. package/client/icons/backup/christmas-sock_dark.png +0 -0
  24. package/client/icons/backup/christmas-sock_light.png +0 -0
  25. package/client/icons/backup/socks_on_G.png +0 -0
  26. package/client/icons/backup/socks_on_M.png +0 -0
  27. package/client/icons/christmas-sock_dark.png +0 -0
  28. package/client/icons/christmas-sock_light.png +0 -0
  29. package/client/icons/christmas-sock_light_bar.png +0 -0
  30. package/client/icons/socks_on_G.png +0 -0
  31. package/client/icons/socks_on_G_bar.png +0 -0
  32. package/client/icons/socks_on_M.png +0 -0
  33. package/client/icons/socks_on_M_bar.png +0 -0
  34. package/client/main.py +28 -0
  35. package/client/proxy_core.py +475 -0
  36. package/client/requirements.txt +3 -0
  37. package/client/scripts/download_xray.sh +30 -0
  38. package/client/setup.py +30 -0
  39. package/client/system_proxy.py +94 -0
  40. package/client/tests/__init__.py +0 -0
  41. package/client/tests/test_config.py +72 -0
  42. package/client/tests/test_system_proxy.py +69 -0
  43. package/client/watch-icons.js +31 -0
  44. package/config.json +82 -5
  45. package/docs/superpowers/plans/2026-05-27-blockproxyclient.md +1274 -0
  46. package/docs/superpowers/specs/2026-05-27-blockproxyclient-design.md +264 -0
  47. package/package.json +11 -5
  48. package/proxy/proxy.js +70 -18
  49. package/server/express.js +17 -1
  50. package/skills-lock.json +11 -0
  51. package/socks5/server.js +2 -2
  52. package/src/App.css +596 -276
  53. package/src/App.js +25 -22
  54. package/src/index.css +3 -4
  55. package/test/lib/mock-server.js +133 -0
  56. package/test/proxy-tests.js +708 -0
  57. package/test/run.js +330 -0
  58. package/build/static/css/main.8bfa3d5f.css +0 -2
  59. package/build/static/css/main.8bfa3d5f.css.map +0 -1
  60. package/build/static/js/main.2247fb80.js.map +0 -1
  61. package/hack-of-anyproxy/lib/requestHandler.js +0 -1060
  62. /package/build/static/js/{main.2247fb80.js.LICENSE.txt → main.68f66be0.js.LICENSE.txt} +0 -0
@@ -0,0 +1,708 @@
1
+ const http = require('http');
2
+ const https = require('https');
3
+ const net = require('net');
4
+ const tls = require('tls');
5
+ const { HttpProxyAgent } = require('http-proxy-agent');
6
+ const { HttpsProxyAgent } = require('https-proxy-agent');
7
+
8
+ // ── 工具函数 ─────────────────────────────────────────────
9
+
10
+ function mean(arr) {
11
+ if (arr.length === 0) return 0;
12
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
13
+ }
14
+
15
+ function percentile(arr, p) {
16
+ if (arr.length === 0) return 0;
17
+ const sorted = [...arr].sort((a, b) => a - b);
18
+ const idx = Math.ceil((p / 100) * sorted.length) - 1;
19
+ return sorted[Math.max(0, idx)];
20
+ }
21
+
22
+ function formatBytes(bytes) {
23
+ if (bytes < 1024) return `${bytes} B`;
24
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
25
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
26
+ }
27
+
28
+ function formatDuration(ms) {
29
+ if (ms < 1000) return `${Math.round(ms)}ms`;
30
+ return `${(ms / 1000).toFixed(2)}s`;
31
+ }
32
+
33
+ function sleep(ms) {
34
+ return new Promise((r) => setTimeout(r, ms));
35
+ }
36
+
37
+ // ── SOCKS5 客户端 ────────────────────────────────────────
38
+
39
+ function socks5Connect(targetHost, targetPort, proxyConfig, timeoutMs = 10000) {
40
+ return new Promise((resolve, reject) => {
41
+ const { host, port, username, password } = proxyConfig;
42
+ const rawSocket = net.createConnection({ host, port });
43
+
44
+ const timer = setTimeout(() => {
45
+ rawSocket.destroy();
46
+ reject(new Error('SOCKS5: connection timeout'));
47
+ }, timeoutMs);
48
+
49
+ function done(err, socket) {
50
+ clearTimeout(timer);
51
+ if (err) {
52
+ if (socket) socket.destroy();
53
+ reject(err);
54
+ } else {
55
+ resolve(socket);
56
+ }
57
+ }
58
+
59
+ rawSocket.once('connect', () => {
60
+ const tlsSocket = tls.connect(
61
+ { socket: rawSocket, rejectUnauthorized: false },
62
+ () => {
63
+ // Step 1: Greeting (0x02 = user/password auth)
64
+ tlsSocket.write(Buffer.from([0x05, 0x01, 0x02]));
65
+
66
+ tlsSocket.once('data', (data) => {
67
+ if (data.length < 2 || data[0] !== 0x05 || data[1] !== 0x02) {
68
+ return done(new Error(`SOCKS5: unexpected auth method 0x${data[1]?.toString(16)}`), tlsSocket);
69
+ }
70
+
71
+ // Step 2: Authentication
72
+ const userBuf = Buffer.from(username || '');
73
+ const passBuf = Buffer.from(password || '');
74
+ const authMsg = Buffer.concat([
75
+ Buffer.from([0x01, userBuf.length]),
76
+ userBuf,
77
+ Buffer.from([passBuf.length]),
78
+ passBuf,
79
+ ]);
80
+ tlsSocket.write(authMsg);
81
+
82
+ tlsSocket.once('data', (authResp) => {
83
+ if (authResp.length < 2 || authResp[0] !== 0x01 || authResp[1] !== 0x00) {
84
+ return done(new Error('SOCKS5: authentication failed'), tlsSocket);
85
+ }
86
+
87
+ // Step 3: CONNECT command (0x03 = domain name)
88
+ const hostBuf = Buffer.from(targetHost);
89
+ const portBuf = Buffer.alloc(2);
90
+ portBuf.writeUInt16BE(targetPort, 0);
91
+
92
+ const connectMsg = Buffer.concat([
93
+ Buffer.from([0x05, 0x01, 0x00, 0x03, hostBuf.length]),
94
+ hostBuf,
95
+ portBuf,
96
+ ]);
97
+ tlsSocket.write(connectMsg);
98
+
99
+ tlsSocket.once('data', (connectResp) => {
100
+ const replyCode = connectResp[1];
101
+ if (replyCode !== 0x00) {
102
+ const errors = {
103
+ 0x01: 'General failure', 0x02: 'Connection not allowed',
104
+ 0x03: 'Network unreachable', 0x04: 'Host unreachable',
105
+ 0x05: 'Connection refused', 0x06: 'TTL expired',
106
+ 0x07: 'Command not supported', 0x08: 'Address type not supported',
107
+ };
108
+ return done(new Error(`SOCKS5 CONNECT: ${errors[replyCode] || `code ${replyCode}`}`), tlsSocket);
109
+ }
110
+ done(null, tlsSocket);
111
+ });
112
+ });
113
+ });
114
+ }
115
+ );
116
+
117
+ tlsSocket.once('error', (err) => done(new Error(`SOCKS5 TLS: ${err.message}`), tlsSocket));
118
+ });
119
+
120
+ rawSocket.once('error', (err) => done(new Error(`SOCKS5 TCP: ${err.message}`), rawSocket));
121
+ });
122
+ }
123
+
124
+ function socks5HttpGet(targetHost, targetPort, path, proxyConfig, timeoutMs = 10000) {
125
+ return socks5Connect(targetHost, targetPort, proxyConfig, timeoutMs).then((socket) => {
126
+ return new Promise((resolve, reject) => {
127
+ const timer = setTimeout(() => {
128
+ socket.destroy();
129
+ reject(new Error('SOCKS5 HTTP: request timeout'));
130
+ }, timeoutMs);
131
+
132
+ const chunks = [];
133
+ socket.on('data', (c) => chunks.push(c));
134
+ socket.once('end', () => {
135
+ clearTimeout(timer);
136
+ const raw = Buffer.concat(chunks).toString();
137
+ resolve(parseHttpResponse(raw));
138
+ });
139
+ socket.once('error', (err) => {
140
+ clearTimeout(timer);
141
+ reject(new Error(`SOCKS5 HTTP: ${err.message}`));
142
+ });
143
+
144
+ const hostHeader = targetPort !== 80 ? `${targetHost}:${targetPort}` : targetHost;
145
+ const req = [
146
+ `GET ${path} HTTP/1.1`,
147
+ `Host: ${hostHeader}`,
148
+ 'Connection: close',
149
+ 'User-Agent: block-proxy-test/1.0',
150
+ '',
151
+ '',
152
+ ].join('\r\n');
153
+ socket.write(req);
154
+ });
155
+ });
156
+ }
157
+
158
+ function parseHttpResponse(raw) {
159
+ const headerEnd = raw.indexOf('\r\n\r\n');
160
+ if (headerEnd === -1) return { status: 0, headers: {}, body: raw };
161
+
162
+ const headerPart = raw.substring(0, headerEnd);
163
+ const body = raw.substring(headerEnd + 4);
164
+ const lines = headerPart.split('\r\n');
165
+ const statusLine = lines[0];
166
+ const statusMatch = statusLine.match(/HTTP\/\d\.\d\s+(\d+)/);
167
+ const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
168
+
169
+ const headers = {};
170
+ for (let i = 1; i < lines.length; i++) {
171
+ const colonIdx = lines[i].indexOf(':');
172
+ if (colonIdx > 0) {
173
+ headers[lines[i].substring(0, colonIdx).trim().toLowerCase()] = lines[i].substring(colonIdx + 1).trim();
174
+ }
175
+ }
176
+
177
+ return { status, headers, body };
178
+ }
179
+
180
+ // ── HTTP 代理请求工具 ─────────────────────────────────────
181
+
182
+ function createProxyAxios(proxyConfig, timeoutMs = 10000) {
183
+ const axios = require('axios');
184
+ const auth = proxyConfig.username
185
+ ? `${encodeURIComponent(proxyConfig.username)}:${encodeURIComponent(proxyConfig.password)}@`
186
+ : '';
187
+ const proxyUrl = `http://${auth}${proxyConfig.host}:${proxyConfig.port}`;
188
+
189
+ return axios.create({
190
+ httpAgent: new HttpProxyAgent(proxyUrl),
191
+ httpsAgent: new HttpsProxyAgent(proxyUrl),
192
+ timeout: timeoutMs,
193
+ validateStatus: () => true,
194
+ });
195
+ }
196
+
197
+ function timed(fn) {
198
+ return async (...args) => {
199
+ const start = Date.now();
200
+ let error = null;
201
+ let result = null;
202
+ try {
203
+ result = await fn(...args);
204
+ } catch (e) {
205
+ error = e;
206
+ }
207
+ return { result, error, duration: Date.now() - start };
208
+ };
209
+ }
210
+
211
+ // ── 连通性检查 ────────────────────────────────────────────
212
+
213
+ async function checkProxyAccessible(host, port, label) {
214
+ return new Promise((resolve) => {
215
+ const sock = net.createConnection({ host, port, timeout: 3000 });
216
+ sock.on('connect', () => {
217
+ sock.destroy();
218
+ resolve({ accessible: true, label });
219
+ });
220
+ sock.on('error', () => {
221
+ sock.destroy();
222
+ resolve({ accessible: false, label });
223
+ });
224
+ sock.on('timeout', () => {
225
+ sock.destroy();
226
+ resolve({ accessible: false, label });
227
+ });
228
+ });
229
+ }
230
+
231
+ // ── 测试:HTTP 代理连通性 ─────────────────────────────────
232
+
233
+ async function testHttpProxyConnectivity(mockBaseUrl, proxyConfig) {
234
+ const results = [];
235
+ const client = createProxyAxios(proxyConfig);
236
+
237
+ // HTTP to mock server
238
+ {
239
+ const { result, error, duration } = await timed(() => client.get(`${mockBaseUrl}/ping`))();
240
+ const bodyOk = result?.data === 'pong';
241
+ results.push({
242
+ name: 'HTTP 代理 > HTTP GET /ping (mock)',
243
+ passed: !error && result?.status === 200 && bodyOk,
244
+ duration,
245
+ detail: error ? error.message
246
+ : !bodyOk ? `status=${result?.status}, body="${String(result?.data).substring(0, 60)}"`
247
+ : `status=${result?.status}`,
248
+ });
249
+ }
250
+
251
+ // HTTP with larger payload
252
+ {
253
+ const { result, error, duration } = await timed(() => client.get(`${mockBaseUrl}/size/102400`))();
254
+ const sizeOk = result?.headers?.['content-length'] === '102400';
255
+ results.push({
256
+ name: 'HTTP 代理 > HTTP GET /size/100k (mock)',
257
+ passed: !error && result?.status === 200 && sizeOk,
258
+ duration,
259
+ detail: error ? error.message : `status=${result?.status}, size=${result?.headers?.['content-length'] || '?'}`,
260
+ });
261
+ }
262
+
263
+ // HTTP POST echo
264
+ {
265
+ const payload = 'x'.repeat(1024);
266
+ const { result, error, duration } = await timed(() => client.post(`${mockBaseUrl}/echo`, payload, {
267
+ headers: { 'Content-Type': 'text/plain' },
268
+ }))();
269
+ let bodyOk = false;
270
+ try {
271
+ const echoData = typeof result?.data === 'string' ? JSON.parse(result.data) : result?.data;
272
+ bodyOk = echoData?.bodyLength === 1024;
273
+ } catch (_) {}
274
+ results.push({
275
+ name: 'HTTP 代理 > HTTP POST /echo (mock)',
276
+ passed: !error && result?.status === 200 && bodyOk,
277
+ duration,
278
+ detail: error ? error.message : `status=${result?.status}`,
279
+ });
280
+ }
281
+
282
+ // HTTPS to external site
283
+ {
284
+ const HTTPS_TIMEOUT = 30000;
285
+ const extClient = createProxyAxios(proxyConfig, HTTPS_TIMEOUT);
286
+ const { result, error, duration } = await timed(() => extClient.get('https://www.baidu.com', {
287
+ headers: { 'User-Agent': 'Mozilla/5.0' },
288
+ }))();
289
+ results.push({
290
+ name: 'HTTP 代理 > HTTPS GET baidu.com',
291
+ passed: !error && result?.status === 200,
292
+ duration,
293
+ detail: error ? error.message : `status=${result?.status}`,
294
+ });
295
+ }
296
+
297
+ return {
298
+ category: 'HTTP 代理 - 连通性',
299
+ passed: results.every((r) => r.passed),
300
+ results,
301
+ };
302
+ }
303
+
304
+ // ── 测试:HTTP 代理延迟 ──────────────────────────────────
305
+
306
+ async function testHttpProxyLatency(mockBaseUrl, proxyConfig, samples = 50) {
307
+ const client = createProxyAxios(proxyConfig);
308
+ const durations = [];
309
+ const errors = [];
310
+
311
+ for (let i = 0; i < samples; i++) {
312
+ const { error, duration } = await timed(() => client.get(`${mockBaseUrl}/size/10240`))();
313
+ if (error) {
314
+ errors.push(error.message);
315
+ } else {
316
+ durations.push(duration);
317
+ }
318
+ }
319
+
320
+ const passed = errors.length <= samples * 0.05; // 95% success
321
+ return {
322
+ category: 'HTTP 代理 - 延迟',
323
+ passed,
324
+ results: [{
325
+ name: `延迟测试 (10KB x ${samples})`,
326
+ passed,
327
+ duration: Math.round(mean(durations)),
328
+ detail: passed
329
+ ? `P50=${Math.round(percentile(durations, 50))}ms P95=${Math.round(percentile(durations, 95))}ms P99=${Math.round(percentile(durations, 99))}ms avg=${Math.round(mean(durations))}ms`
330
+ : `${errors.length}/${samples} 失败`,
331
+ }],
332
+ metrics: {
333
+ samples: durations.length,
334
+ errors: errors.length,
335
+ p50: Math.round(percentile(durations, 50)),
336
+ p95: Math.round(percentile(durations, 95)),
337
+ p99: Math.round(percentile(durations, 99)),
338
+ min: Math.round(Math.min(...durations)),
339
+ max: Math.round(Math.max(...durations)),
340
+ avg: Math.round(mean(durations)),
341
+ },
342
+ };
343
+ }
344
+
345
+ // ── 测试:HTTP 代理并发 ──────────────────────────────────
346
+
347
+ async function testHttpProxyConcurrency(mockBaseUrl, proxyConfig, concurrency = 50, total = 100) {
348
+ const client = createProxyAxios(proxyConfig);
349
+ let idx = 0;
350
+ const durations = [];
351
+ const errors = [];
352
+
353
+ async function worker() {
354
+ while (idx < total) {
355
+ const cur = idx++;
356
+ const { result, error, duration } = await timed(() => client.get(`${mockBaseUrl}/size/10240`))();
357
+ if (error || result?.status !== 200) {
358
+ errors.push(`#${cur}: ${error ? error.message : 'status=' + result?.status}`);
359
+ } else {
360
+ durations.push(duration);
361
+ }
362
+ }
363
+ }
364
+
365
+ const start = Date.now();
366
+ const workers = Array.from({ length: concurrency }, () => worker());
367
+ await Promise.all(workers);
368
+ const elapsed = Date.now() - start;
369
+
370
+ const qps = total / (elapsed / 1000);
371
+ const passed = errors.length === 0;
372
+
373
+ return {
374
+ category: 'HTTP 代理 - 并发吞吐',
375
+ passed,
376
+ results: [{
377
+ name: `并发测试 (${concurrency}并发, ${total}请求, 10KB)`,
378
+ passed,
379
+ duration: elapsed,
380
+ detail: passed
381
+ ? `QPS=${qps.toFixed(1)} 平均延迟=${Math.round(mean(durations))}ms`
382
+ : `失败 ${errors.length}/${total}: ${errors.slice(0, 3).join(', ')}`,
383
+ }],
384
+ metrics: {
385
+ concurrency,
386
+ total,
387
+ qps: Math.round(qps * 10) / 10,
388
+ avgLatency: Math.round(mean(durations)),
389
+ elapsed,
390
+ errors: errors.length,
391
+ },
392
+ };
393
+ }
394
+
395
+ // ── 测试:HTTP 代理稳定性 ─────────────────────────────────
396
+
397
+ async function testHttpProxyStability(mockBaseUrl, proxyConfig, count = 100) {
398
+ const client = createProxyAxios(proxyConfig);
399
+ const durations = [];
400
+ const errors = [];
401
+
402
+ for (let i = 0; i < count; i++) {
403
+ const { error, duration } = await timed(() => client.get(`${mockBaseUrl}/ping`))();
404
+ if (error) {
405
+ errors.push({ index: i, message: error.message });
406
+ } else {
407
+ durations.push(duration);
408
+ }
409
+ }
410
+
411
+ const successRate = (count - errors.length) / count;
412
+ const passed = successRate === 1.0;
413
+
414
+ return {
415
+ category: 'HTTP 代理 - 稳定性',
416
+ passed,
417
+ results: [{
418
+ name: `稳定性测试 (${count} 次顺序请求)`,
419
+ passed,
420
+ duration: Math.round(mean(durations)),
421
+ detail: passed
422
+ ? `成功率 ${(successRate * 100).toFixed(0)}% avg=${Math.round(mean(durations))}ms P95=${Math.round(percentile(durations, 95))}ms`
423
+ : `成功率 ${(successRate * 100).toFixed(1)}% 失败 ${errors.length} 次`,
424
+ }],
425
+ metrics: {
426
+ total: count,
427
+ success: count - errors.length,
428
+ failed: errors.length,
429
+ avg: Math.round(mean(durations)),
430
+ p95: Math.round(percentile(durations, 95)),
431
+ max: durations.length > 0 ? Math.round(Math.max(...durations)) : 0,
432
+ },
433
+ };
434
+ }
435
+
436
+ // ── 测试:HTTP 代理吞吐量 ────────────────────────────────
437
+
438
+ async function testHttpProxyThroughput(mockBaseUrl, proxyConfig, sizeBytes = 1048576) {
439
+ const client = createProxyAxios(proxyConfig, 30000);
440
+ const { result, error, duration } = await timed(() => client.get(`${mockBaseUrl}/size/${sizeBytes}`, {
441
+ responseType: 'arraybuffer',
442
+ }))();
443
+
444
+ const mbps = error ? 0 : (sizeBytes / (1024 * 1024)) / (duration / 1000);
445
+ const passed = !error && result?.status === 200;
446
+
447
+ return {
448
+ category: 'HTTP 代理 - 吞吐量',
449
+ passed,
450
+ results: [{
451
+ name: `吞吐量测试 (${formatBytes(sizeBytes)} 下载)`,
452
+ passed,
453
+ duration,
454
+ detail: passed
455
+ ? `${mbps.toFixed(2)} MB/s (${formatDuration(duration)})`
456
+ : (error ? error.message : `status=${result?.status}`),
457
+ }],
458
+ metrics: {
459
+ sizeBytes,
460
+ duration,
461
+ mbps: Math.round(mbps * 100) / 100,
462
+ },
463
+ };
464
+ }
465
+
466
+ // ── 测试:SOCKS5 连通性 ──────────────────────────────────
467
+
468
+ async function testSocks5Connectivity(mockBaseUrl, proxyConfig) {
469
+ const mockUrl = new URL(mockBaseUrl);
470
+ const mockHost = mockUrl.hostname;
471
+ const mockPort = parseInt(mockUrl.port, 10) || 80;
472
+ const results = [];
473
+
474
+ // SOCKS5 GET /ping
475
+ {
476
+ const { result, error, duration } = await timed(() =>
477
+ socks5HttpGet(mockHost, mockPort, '/ping', proxyConfig)
478
+ )();
479
+ const bodyOk = result?.body === 'pong';
480
+ results.push({
481
+ name: 'SOCKS5 > HTTP GET /ping (mock)',
482
+ passed: !error && result?.status === 200 && bodyOk,
483
+ duration,
484
+ detail: error ? error.message
485
+ : !bodyOk ? `status=${result?.status}, body="${String(result?.body).substring(0, 60)}"`
486
+ : `status=${result?.status}`,
487
+ });
488
+ }
489
+
490
+ // SOCKS5 GET /size/100k
491
+ {
492
+ const { result, error, duration } = await timed(() =>
493
+ socks5HttpGet(mockHost, mockPort, '/size/102400', proxyConfig)
494
+ )();
495
+ results.push({
496
+ name: 'SOCKS5 > HTTP GET /size/100k (mock)',
497
+ passed: !error && result?.status === 200,
498
+ duration,
499
+ detail: error ? error.message : `status=${result?.status}`,
500
+ });
501
+ }
502
+
503
+ // SOCKS5 to external
504
+ {
505
+ const { result, error, duration } = await timed(() =>
506
+ socks5HttpGet('www.baidu.com', 80, '/', proxyConfig, 15000)
507
+ )();
508
+ results.push({
509
+ name: 'SOCKS5 > HTTP GET baidu.com',
510
+ passed: !error && (result?.status === 200 || result?.status === 302),
511
+ duration,
512
+ detail: error ? error.message : `status=${result?.status}`,
513
+ });
514
+ }
515
+
516
+ return {
517
+ category: 'SOCKS5 - 连通性',
518
+ passed: results.every((r) => r.passed),
519
+ results,
520
+ };
521
+ }
522
+
523
+ // ── 测试:SOCKS5 延迟 ────────────────────────────────────
524
+
525
+ async function testSocks5Latency(mockBaseUrl, proxyConfig, samples = 30) {
526
+ const mockUrl = new URL(mockBaseUrl);
527
+ const mockHost = mockUrl.hostname;
528
+ const mockPort = parseInt(mockUrl.port, 10) || 80;
529
+ const durations = [];
530
+ const errors = [];
531
+
532
+ for (let i = 0; i < samples; i++) {
533
+ const { result, error, duration } = await timed(() =>
534
+ socks5HttpGet(mockHost, mockPort, '/size/10240', proxyConfig)
535
+ )();
536
+ if (error) {
537
+ errors.push(error.message);
538
+ } else if (result?.status === 200) {
539
+ durations.push(duration);
540
+ } else {
541
+ errors.push(`unexpected status ${result?.status}`);
542
+ }
543
+ }
544
+
545
+ const passed = errors.length <= samples * 0.1;
546
+ return {
547
+ category: 'SOCKS5 - 延迟',
548
+ passed,
549
+ results: [{
550
+ name: `延迟测试 (10KB x ${samples})`,
551
+ passed,
552
+ duration: Math.round(mean(durations)),
553
+ detail: durations.length > 0
554
+ ? `P50=${Math.round(percentile(durations, 50))}ms P95=${Math.round(percentile(durations, 95))}ms avg=${Math.round(mean(durations))}ms`
555
+ : `全部失败: ${errors[0]}`,
556
+ }],
557
+ metrics: {
558
+ samples: durations.length,
559
+ errors: errors.length,
560
+ p50: Math.round(percentile(durations, 50)),
561
+ p95: Math.round(percentile(durations, 95)),
562
+ avg: Math.round(mean(durations)),
563
+ },
564
+ };
565
+ }
566
+
567
+ // ── 测试:SOCKS5 并发 ────────────────────────────────────
568
+
569
+ async function testSocks5Concurrency(mockBaseUrl, proxyConfig, concurrency = 25, total = 50) {
570
+ const mockUrl = new URL(mockBaseUrl);
571
+ const mockHost = mockUrl.hostname;
572
+ const mockPort = parseInt(mockUrl.port, 10) || 80;
573
+ let idx = 0;
574
+ const durations = [];
575
+ const errors = [];
576
+
577
+ async function worker() {
578
+ while (idx < total) {
579
+ const cur = idx++;
580
+ const { result, error, duration } = await timed(() =>
581
+ socks5HttpGet(mockHost, mockPort, '/size/10240', proxyConfig)
582
+ )();
583
+ if (error || result?.status !== 200) {
584
+ errors.push(`#${cur}: ${error ? error.message : 'status=' + result?.status}`);
585
+ } else {
586
+ durations.push(duration);
587
+ }
588
+ }
589
+ }
590
+
591
+ const start = Date.now();
592
+ const workers = Array.from({ length: concurrency }, () => worker());
593
+ await Promise.all(workers);
594
+ const elapsed = Date.now() - start;
595
+
596
+ const qps = total / (elapsed / 1000);
597
+ const passed = errors.length <= total * 0.05;
598
+
599
+ return {
600
+ category: 'SOCKS5 - 并发',
601
+ passed,
602
+ results: [{
603
+ name: `并发测试 (${concurrency}并发, ${total}请求, 10KB)`,
604
+ passed,
605
+ duration: elapsed,
606
+ detail: passed
607
+ ? `QPS=${qps.toFixed(1)} 平均延迟=${Math.round(mean(durations))}ms`
608
+ : `失败 ${errors.length}/${total}: ${errors.slice(0, 3).join(', ')}`,
609
+ }],
610
+ metrics: {
611
+ concurrency,
612
+ total,
613
+ qps: Math.round(qps * 10) / 10,
614
+ avgLatency: Math.round(mean(durations)),
615
+ elapsed,
616
+ errors: errors.length,
617
+ },
618
+ };
619
+ }
620
+
621
+ // ── 测试:外部站点综合 ────────────────────────────────────
622
+
623
+ async function testExternalSites(proxyConfig) {
624
+ const client = createProxyAxios(proxyConfig, 15000);
625
+ const sites = [
626
+ { name: 'baidu.com (HTTP)', url: 'http://www.baidu.com' },
627
+ { name: 'baidu.com (HTTPS)', url: 'https://www.baidu.com' },
628
+ { name: 'github.com (HTTPS)', url: 'https://github.com' },
629
+ { name: 'bing.com (HTTPS)', url: 'https://www.bing.com' },
630
+ ];
631
+
632
+ const results = [];
633
+ for (const site of sites) {
634
+ const { result, error, duration } = await timed(() => client.get(site.url, {
635
+ headers: { 'User-Agent': 'Mozilla/5.0' },
636
+ maxRedirects: 5,
637
+ }))();
638
+ results.push({
639
+ name: `外部站点 > ${site.name}`,
640
+ passed: !error && result?.status >= 200 && result?.status < 400,
641
+ duration,
642
+ detail: error ? error.message : `status=${result?.status}`,
643
+ });
644
+ }
645
+
646
+ return {
647
+ category: '外部站点 - 连通性',
648
+ passed: results.filter((r) => r.passed).length >= 2,
649
+ results,
650
+ };
651
+ }
652
+
653
+ // ── 主入口 ────────────────────────────────────────────────
654
+
655
+ async function runAllTests(options) {
656
+ const {
657
+ mockBaseUrl,
658
+ httpProxy,
659
+ socks5,
660
+ skipExternal = false,
661
+ } = options;
662
+
663
+ const allResults = [];
664
+
665
+ // ── HTTP 代理测试 ──
666
+ if (httpProxy) {
667
+ allResults.push(await testHttpProxyConnectivity(mockBaseUrl, httpProxy));
668
+ allResults.push(await testHttpProxyLatency(mockBaseUrl, httpProxy));
669
+ allResults.push(await testHttpProxyConcurrency(mockBaseUrl, httpProxy));
670
+ allResults.push(await testHttpProxyStability(mockBaseUrl, httpProxy));
671
+ allResults.push(await testHttpProxyThroughput(mockBaseUrl, httpProxy));
672
+ }
673
+
674
+ // ── SOCKS5 测试 ──
675
+ if (socks5) {
676
+ allResults.push(await testSocks5Connectivity(mockBaseUrl, socks5));
677
+ allResults.push(await testSocks5Latency(mockBaseUrl, socks5));
678
+ allResults.push(await testSocks5Concurrency(mockBaseUrl, socks5));
679
+ }
680
+
681
+ // ── 外部站点 ──
682
+ if (!skipExternal && httpProxy) {
683
+ allResults.push(await testExternalSites(httpProxy));
684
+ }
685
+
686
+ // ── 汇总 ──
687
+ const flatResults = allResults.flatMap((c) => c.results);
688
+ const passed = flatResults.filter((r) => r.passed).length;
689
+ const failed = flatResults.filter((r) => !r.passed).length;
690
+
691
+ return {
692
+ categories: allResults,
693
+ summary: {
694
+ total: flatResults.length,
695
+ passed,
696
+ failed,
697
+ categoriesPassed: allResults.filter((c) => c.passed).length,
698
+ categoriesTotal: allResults.length,
699
+ },
700
+ };
701
+ }
702
+
703
+ module.exports = {
704
+ runAllTests,
705
+ checkProxyAccessible,
706
+ socks5Connect,
707
+ socks5HttpGet,
708
+ };