block-proxy 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.
Files changed (49) hide show
  1. package/.eslintignore +3 -0
  2. package/AD_BLOCK.md +38 -0
  3. package/Dockerfile +51 -0
  4. package/LICENSE +21 -0
  5. package/README.md +182 -0
  6. package/bin/start.js +45 -0
  7. package/cert/rootCA.crt +20 -0
  8. package/cert/rootCA.key +27 -0
  9. package/config.json +234 -0
  10. package/craco.config.js +52 -0
  11. package/hack-of-anyproxy/lib/requestHandler.js +1028 -0
  12. package/package.json +54 -0
  13. package/proxy/attacker.js +135 -0
  14. package/proxy/domain.js +26 -0
  15. package/proxy/fs.js +46 -0
  16. package/proxy/http.js +224 -0
  17. package/proxy/mitm/persistentStore.js +34 -0
  18. package/proxy/mitm/persistentStore.json +3 -0
  19. package/proxy/mitm/rule.js +116 -0
  20. package/proxy/mitm/uaFilter.js +47 -0
  21. package/proxy/mitm/ydcd/ydcd.js +34 -0
  22. package/proxy/mitm/youtube/youtube.response.js +39 -0
  23. package/proxy/monitor.js +283 -0
  24. package/proxy/proxy.js +1488 -0
  25. package/proxy/scan.js +120 -0
  26. package/proxy/start.js +7 -0
  27. package/proxy/wanip.js +76 -0
  28. package/public/favicon.ico +0 -0
  29. package/public/index.html +43 -0
  30. package/public/iphone-proxy-setting.jpg +0 -0
  31. package/public/logo192.png +0 -0
  32. package/public/logo512.png +0 -0
  33. package/public/manifest.json +25 -0
  34. package/public/proxy.jpg +0 -0
  35. package/public/robots.txt +3 -0
  36. package/server/express.js +232 -0
  37. package/server/start.js +24 -0
  38. package/server/util.js +166 -0
  39. package/socks5/server.js +321 -0
  40. package/socks5/start.js +7 -0
  41. package/socks5/test_tls_reuse.js +40 -0
  42. package/src/App.css +505 -0
  43. package/src/App.js +759 -0
  44. package/src/App.test.js +8 -0
  45. package/src/index.css +13 -0
  46. package/src/index.js +17 -0
  47. package/src/logo.svg +1 -0
  48. package/src/reportWebVitals.js +13 -0
  49. package/src/setupTests.js +5 -0
package/proxy/proxy.js ADDED
@@ -0,0 +1,1488 @@
1
+ // 文件名: proxy/proxy.js
2
+ const AnyProxy = require('anyproxy');
3
+ const { exec } = require('child_process');
4
+ const fs = require('fs');
5
+ const _fs = require('./fs.js');
6
+ const path = require('path');
7
+ const { start } = require('repl');
8
+ const net = require('net');
9
+ const scanNetwork = require("./scan").scanNetwork;
10
+ const util = require('util');
11
+ const zlib = require('zlib');
12
+ const _util = require('../server/util.js');
13
+ const os = require('os');
14
+ const http = require("http");
15
+ const https = require('https');
16
+ const { URL } = require('url');
17
+ const axios = require('axios');
18
+ const { HttpProxyAgent } = require('http-proxy-agent');
19
+ const { HttpsProxyAgent } = require('https-proxy-agent');
20
+ const _request = require("./http.js").request;
21
+ const Rule = require("./mitm/rule.js");
22
+ const uaFilter = require("./mitm/uaFilter.js");
23
+ const attacker = require('./attacker.js');
24
+ const monitor = require('./monitor.js');
25
+ const domain = require('./domain.js');
26
+ const wanip = require('./wanip.js');
27
+
28
+ // 启用全局 keep-alive,使 AnyProxy 内部转发也复用连接
29
+ http.globalAgent.keepAlive = true;
30
+ https.globalAgent.keepAlive = true;
31
+ http.globalAgent.maxSockets = 50;
32
+ https.globalAgent.maxSockets = 50;
33
+
34
+ const httpAgent = new http.Agent({ keepAlive: true, maxSockets: 50 });
35
+ const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 50 });
36
+
37
+ // 全局参数
38
+ const configPath = path.join(__dirname, '../config.json');
39
+ var anyproxy_started = false;
40
+ var blockHosts = [];
41
+ var proxyPort = 8001; // http 代理端口
42
+ var socks5Port = 8002; // socks5 端口
43
+ var webInterfacePort = 8003; // anyproxy 监控端口
44
+ var vpn_proxy = "";
45
+ var devices = [];
46
+ var progress_time_stamp = "";
47
+ var localMac = getLocalMacAddress();
48
+ var localIp = getLocalIp();
49
+ var network_scanning_status = "0";
50
+ var auth_username = "";
51
+ var auth_password = "";
52
+ var your_domain = "";
53
+ var wan_ip = "0.0.0.0";
54
+ var is_running_in_docker = false;
55
+ var docker_host_IP = '';
56
+ var enable_express = "1"; // "0", "1"
57
+ var enable_socks5 = "1";
58
+ var enable_webinterface = "1"; // "0", "1"
59
+ // 域名判断,区分浏览器和 App
60
+ var filtered_mitm_domains = [
61
+ ...uaFilter.filtered_mitm_domains
62
+ ];
63
+
64
+ // 对 Rule 里的正则表达式进行预编译
65
+ function preCompileRuleRegexp() {
66
+ Object.keys(Rule).forEach(key => {
67
+ if (Array.isArray(Rule[key])) {
68
+ Rule[key] = Rule[key].map(item => {
69
+ if (typeof item.regexp === 'string' && item.regexp.trim() !== '') {
70
+ try {
71
+ item.compiledRegexp = new RegExp(item.regexp);
72
+ } catch (e) {
73
+ console.error(`Invalid regex in MITM rule: "${item.regexp}", skipping compilation. Error:`, e.message);
74
+ item.compiledRegexp = /^$/; // 或其他处理方式
75
+ }
76
+ }
77
+ return item;
78
+ });
79
+ }
80
+ });
81
+ }
82
+
83
+ function isEmpty(obj) {
84
+ if (obj === null || obj === undefined) {
85
+ return true;
86
+ }
87
+ return Object.keys(obj).length === 0;
88
+ }
89
+
90
+ // 读取配置文件的函数
91
+ async function loadConfig() {
92
+ let config = {
93
+ network_scanning_status: network_scanning_status,
94
+ progress_time_stamp: progress_time_stamp,
95
+ block_hosts: blockHosts,
96
+ proxy_port: proxyPort,
97
+ web_interface_port: webInterfacePort,
98
+ your_domain: your_domain,
99
+ vpn_proxy:"",
100
+ auth_username:"",
101
+ auth_password:"",
102
+ enable_express: enable_express,
103
+ enable_socks5: enable_socks5,
104
+ socks5_port: socks5Port,
105
+ enable_webinterface: enable_webinterface,
106
+ devices: []
107
+ };
108
+
109
+ try {
110
+ if (fs.existsSync(configPath)) {
111
+ const loadedConfig = await _fs.readConfig();
112
+
113
+ // 更新全局变量
114
+ if (loadedConfig.block_hosts) {
115
+ // 原始信息
116
+ config.block_hosts = [...loadedConfig.block_hosts];
117
+ // 缓存正则表达式
118
+ blockHosts = loadedConfig.block_hosts.map(item => {
119
+ // 如果是对象格式且包含 filter_match_rule,则预编译正则
120
+ if (typeof item === 'object' && item.filter_match_rule && item.filter_match_rule.trim() !== '') {
121
+ try {
122
+ // 预编译正则表达式
123
+ item.compiledFilterRegexp = new RegExp(item.filter_match_rule);
124
+ } catch (e) {
125
+ console.error(`Invalid regex in block rule: "${item.filter_match_rule}", skipping compilation. Error:`, e.message);
126
+ // 如果正则无效,可以设置一个永远不匹配的正则,或者标记此项无效
127
+ item.compiledFilterRegexp = /^$/; // 一个永远不匹配非空字符串的正则
128
+ // 或者 item.compiledFilterRegexp = null; 然后在 shouldBlockHost 中检查 if (!blockItem.compiledFilterRegexp) return false;
129
+ }
130
+ }
131
+ return item;
132
+ });
133
+ }
134
+
135
+ vpn_proxy = loadedConfig.vpn_proxy;
136
+ config.vpn_proxy = vpn_proxy;
137
+
138
+ auth_username = loadedConfig.auth_username;
139
+ config.auth_username = auth_username;
140
+
141
+ auth_password = loadedConfig.auth_password;
142
+ config.auth_password = auth_password;
143
+
144
+ progress_time_stamp = loadedConfig.progress_time_stamp;
145
+ config.progress_time_stamp = progress_time_stamp;
146
+
147
+ network_scanning_status = loadedConfig.network_scanning_status;
148
+ config.network_scanning_status = network_scanning_status;
149
+
150
+ enable_express = loadedConfig.enable_express;
151
+ config.enable_express = enable_express;
152
+
153
+ enable_socks5 = loadedConfig.enable_socks5;
154
+ config.enable_socks5 = enable_socks5;
155
+
156
+ enable_webinterface = loadedConfig.enable_webinterface;
157
+ config.enable_webinterface = enable_webinterface;
158
+
159
+ socks5Port = loadedConfig.socks5_port;
160
+ config.socks5_port = socks5Port;
161
+
162
+ your_domain = loadedConfig.your_domain;
163
+ config.your_domain = your_domain;
164
+
165
+ if (loadedConfig.proxy_port) {
166
+ proxyPort = loadedConfig.proxy_port;
167
+ config.proxy_port = proxyPort;
168
+ }
169
+
170
+ if (loadedConfig.web_interface_port) {
171
+ webInterfacePort = loadedConfig.web_interface_port;
172
+ config.web_interface_port = webInterfacePort;
173
+ }
174
+
175
+ if (loadedConfig.devices) {
176
+ devices = loadedConfig.devices;
177
+ config.devices = devices;
178
+ }
179
+
180
+ } else {
181
+ // 如果配置文件不存在,则创建默认配置文件
182
+ _fs.writeConfig({
183
+ network_scanning_status:network_scanning_status,
184
+ progress_time_stamp: progress_time_stamp,
185
+ block_hosts: blockHosts,
186
+ proxy_port: proxyPort,
187
+ web_interface_port: webInterfacePort,
188
+ auth_password:"",
189
+ auth_username:"",
190
+ enable_express: enable_express,
191
+ your_domain: your_domain,
192
+ socks5_port: socks5Port,
193
+ enable_socks5: enable_socks5,
194
+ enable_webinterface: enable_webinterface,
195
+ vpn_proxy: ""
196
+ });
197
+ // fs.writeFileSync(configPath, JSON.stringify({
198
+ // }, null, 2));
199
+ console.log('Created default config.json file');
200
+ }
201
+ } catch (err) {
202
+ console.error('Error reading config file, using default config:', err);
203
+ }
204
+
205
+ return config;
206
+ }
207
+
208
+ async function updateWanIp() {
209
+ // var ips = await domain.getDomainIP(your_domain);
210
+ var ip = await wanip.getPublicIp();
211
+ if (ip === null) {
212
+ ip = "0.0.0.0";
213
+ }
214
+ wan_ip = ip;
215
+ }
216
+
217
+ // F4:6B:8c:90:29:5 -> f4:6b:8c:90:29:05
218
+ function normalizeMacAddress(mac) {
219
+ // 去除可能的空格,并转为小写
220
+ const cleaned = mac.trim().toLowerCase();
221
+
222
+ // 按冒号分割成6个部分
223
+ const parts = cleaned.split(':');
224
+
225
+ // 验证是否为6段
226
+ if (parts.length !== 6) {
227
+ throw new Error('Invalid MAC address: must have 6 parts separated by colons');
228
+ }
229
+
230
+ // 对每一段:补前导零(确保长度为2),并验证是否为合法十六进制
231
+ const normalized = parts.map(part => {
232
+ if (!/^[0-9a-f]{1,2}$/.test(part)) {
233
+ throw new Error(`Invalid hex part: "${part}"`);
234
+ }
235
+ return part.padStart(2, '0'); // 补0到长度为2
236
+ });
237
+
238
+ return normalized.join(':');
239
+ }
240
+
241
+ // 根据来源 ip 来遍历当前 blockList,把对应mac拦截配置匹配的项都找出来
242
+ function getBlockRules(ip) {
243
+ // 获得ip对应的 mac 地址
244
+ const mac = getMacByIp(ip);
245
+ var currBlockList = [];
246
+ blockHosts.forEach(function(item, index){
247
+ try {
248
+ if (item.filter_mac === undefined || item.filter_mac == "") {
249
+ currBlockList.push(item);
250
+ } else if (item.filter_mac != "" && normalizeMacAddress(item.filter_mac) === normalizeMacAddress(mac)) {
251
+ currBlockList.push(item);
252
+ }
253
+ } catch(e) {
254
+ // 如果mac地址格式不正确,则跳过
255
+ }
256
+ });
257
+ return currBlockList;
258
+ }
259
+
260
+ // 检查是否应当拦截 host & match_rule & url
261
+ // match_rule 为空,只拦截 host
262
+ // url 不为空,域名和 url 同时匹配时拦截
263
+ // url 为空,匹配域名拦截
264
+ function shouldBlockHost(host, blockList, url) {
265
+ if (!host) return false;
266
+
267
+ // 获取当前时间信息
268
+ const now = new Date();
269
+ const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
270
+ const currentDay = now.getDay() === 0 ? 7 : now.getDay(); // 转换为 1-7,周日为7
271
+
272
+ return blockList.some(blockItem => {
273
+ // 兼容旧格式(字符串格式)
274
+ if (typeof blockItem === 'string') {
275
+ return host.includes(blockItem);
276
+ }
277
+
278
+ // 新格式(对象格式)
279
+ if (typeof blockItem === 'object' && blockItem.filter_host) {
280
+
281
+ if (!host.includes(blockItem.filter_host)) {
282
+ return false;
283
+ }
284
+ // console.log('访问网址 === 配置网址')
285
+
286
+ // 检查星期几是否匹配
287
+ if (blockItem.filter_weekday && Array.isArray(blockItem.filter_weekday)) {
288
+ if (!blockItem.filter_weekday.includes(currentDay)) {
289
+ return false;
290
+ }
291
+ }
292
+
293
+ // 在传入 url 的情况下检查路径名是否匹配(新增功能)
294
+ if (url != "" && blockItem.filter_match_rule && blockItem.filter_match_rule.trim() !== '') {
295
+ // 匹配拦截规则,拦截
296
+ if (blockItem.compiledFilterRegexp && blockItem.compiledFilterRegexp.test(url)) { // 优化后
297
+ // do nothing
298
+ } else { // 不匹配拦截规则,不拦截
299
+ return false;
300
+ }
301
+ }
302
+
303
+ // 如果没有设置时间段,则始终拦截
304
+ if (!blockItem.filter_start_time || !blockItem.filter_end_time) {
305
+ return true;
306
+ }
307
+
308
+ // 检查当前时间是否在拦截时间段内
309
+ const startTime = blockItem.filter_start_time;
310
+ const endTime = blockItem.filter_end_time;
311
+
312
+ // 处理跨天的情况(例如 22:00 到 06:00)
313
+ if (startTime > endTime) {
314
+ return currentTime >= startTime || currentTime <= endTime;
315
+ } else {
316
+ return currentTime >= startTime && currentTime <= endTime;
317
+ }
318
+ }
319
+
320
+ return false;
321
+ });
322
+ }
323
+
324
+ // 获得 body 的长度,入参可以是Buffer也可以是字符串
325
+ function getContentLength(body) {
326
+ let contentLength = 0;
327
+ if (Buffer.isBuffer(body)) {
328
+ contentLength = body.length;
329
+ } else if (typeof body === 'string') {
330
+ // 如果是字符串,按 utf-8 编码转换为字节
331
+ let encoder = new TextEncoder();
332
+ let uint8Array = encoder.encode(body);
333
+ contentLength = uint8Array.byteLength;
334
+ }
335
+ return contentLength;
336
+ }
337
+
338
+ // 得到本机 Mac 地址
339
+ function getLocalMacAddress() {
340
+ const interfaces = os.networkInterfaces();
341
+ for (const name of Object.keys(interfaces)) {
342
+ const nets = interfaces[name];
343
+ for (var net of nets) {
344
+ // 跳过回环地址和 IPv6
345
+ if (net.family === 'IPv4' && !net.internal) {
346
+ return net.mac; // 返回第一个非回环 IPv4 网卡的 MAC
347
+ }
348
+ }
349
+ }
350
+ return null;
351
+ }
352
+
353
+ function getLocalIp() {
354
+ const interfaces = os.networkInterfaces();
355
+ for (const name of Object.keys(interfaces)) {
356
+ for (const iface of interfaces[name]) {
357
+ // 跳过 IPv6 和内部回环地址
358
+ if (iface.family === 'IPv4' && !iface.internal) {
359
+ return iface.address;
360
+ }
361
+ }
362
+ }
363
+ return '127.0.0.1'; // fallback
364
+ }
365
+
366
+ function getMacByIp(ipAddress) {
367
+ // 从 devices 中查询 ip 对应的 mac 地址,否则返回空
368
+ if (!ipAddress || !devices || !Array.isArray(devices)) {
369
+ return "";
370
+ }
371
+
372
+ if (ipAddress == "127.0.0.1") {
373
+ return localMac;
374
+ }
375
+
376
+ const device = devices.find(device => device.ip === ipAddress);
377
+ return device ? device.mac : "";
378
+ }
379
+
380
+ // 保存代理服务器实例的变量
381
+ let proxyServerInstance = null;
382
+
383
+ function startProxyServer() {
384
+ // Check if root CA is needed
385
+ if (!AnyProxy.utils.certMgr.ifRootCAFileExists()) {
386
+ AnyProxy.utils.certMgr.generateRootCA((error, keyPath) => {
387
+ if (!error) {
388
+ console.log('Root CA generated successfully, please install the certificate');
389
+ console.log('Certificate path:', keyPath);
390
+ startProxyServer();
391
+ } else {
392
+ console.error('Failed to generate root CA:', error);
393
+ }
394
+ });
395
+ } else {
396
+ // Start proxy server
397
+ let options = getAnyProxyOptions();
398
+ proxyServerInstance = new AnyProxy.ProxyServer(options);
399
+
400
+ proxyServerInstance.on('ready', () => {
401
+ console.log(`✅ Proxy server started on port ${proxyPort}`);
402
+ if (enable_webinterface == "1") {
403
+ console.log(`✅ Web interface available on port ${webInterfacePort}`);
404
+ }
405
+ console.log('Intercepting requests to hosts:', blockHosts.join(', '));
406
+ console.log('All other requests will be passed through without HTTPS interception');
407
+ });
408
+
409
+ proxyServerInstance.on('error', (e) => {
410
+ console.error('Proxy server error:', e);
411
+ });
412
+
413
+ // 添加服务器关闭事件监听
414
+ proxyServerInstance.on('close', () => {
415
+ console.log('代理服务器已关闭');
416
+ setTimeout(() => {
417
+ // 需要判断是主动关闭还是意外关闭
418
+ // proxyServerInstance.start();
419
+ }, 3000);
420
+ });
421
+
422
+ proxyServerInstance.start();
423
+
424
+ return proxyServerInstance;
425
+ }
426
+ }
427
+
428
+ // console.log(normalizeIP('::ffff:192.168.124.118')); // "192.168.124.118"
429
+ // console.log(normalizeIP('::FFFF:10.0.0.5')); // "10.0.0.5"
430
+ // console.log(normalizeIP('192.168.1.100')); // "192.168.1.100"
431
+ // console.log(normalizeIP('[::ffff:172.16.0.10]:3000')); // "172.16.0.10"
432
+ // console.log(normalizeIP('::1')); // "::1"
433
+ function normalizeIP(rawIP) {
434
+ if (typeof rawIP !== 'string') return rawIP;
435
+
436
+ // 处理 [::ffff:192.168.1.1]:8080 这类格式(来自 req.url 或 proxy)
437
+ let ip = rawIP;
438
+ if (ip.startsWith('[')) {
439
+ const match = ip.match(/^\[([^\]]+)\]/);
440
+ if (match) ip = match[1];
441
+ }
442
+
443
+ // 移除 ::ffff: 前缀(忽略大小写)
444
+ return ip.replace(/^::ffff:/i, '');
445
+ }
446
+
447
+ // 暂时只支持 IPv4
448
+ function getRemoteAddressFromReq(requestDetail) {
449
+ var rawIP = requestDetail?._req?.client?.remoteAddress;
450
+ if (rawIP === undefined) {
451
+ return "0.0.0.0";
452
+ } else {
453
+ return normalizeIP(rawIP);
454
+ }
455
+ }
456
+
457
+ // 获得 Symbol 实例的属性
458
+ function getSymbolProperty(obj, symbolDescription) {
459
+ if (typeof obj !== 'object' || obj === null) {
460
+ return undefined;
461
+ }
462
+
463
+ const symbols = Object.getOwnPropertySymbols(obj);
464
+ for (var sym of symbols) {
465
+ if (sym.description === symbolDescription) {
466
+ return obj[sym];
467
+ }
468
+ }
469
+ return undefined;
470
+ }
471
+
472
+ // "192.168.1.1:8001" → { ip, port }
473
+ function parseAddress(str) {
474
+ const [ip, portStr] = str.split(':');
475
+ const port = portStr ? parseInt(portStr, 10) : null;
476
+ if (isNaN(port)) {
477
+ throw new Error('Invalid port');
478
+ }
479
+ return { ip, port };
480
+ }
481
+
482
+ /**
483
+ * 使用本地 HTTP 代理 (127.0.0.1:1087) 转发请求
484
+ */
485
+ async function forwardViaLocalProxy(url, requestOptions, body = null, proxyConfig) {
486
+ const isHttps = url.startsWith('https:') ? true : false;
487
+
488
+ // 构造目标 URL(必须是完整 URL)
489
+ // const protocol = isHttps ? 'https:' : 'http:'; // 这行没用到,可以删掉
490
+ // const hostname = requestOptions.hostname || requestOptions.host; // 这行没用到,可以删掉
491
+ // const port = requestOptions.port || (isHttps ? 443 : 80); // 这行没用到,可以删掉
492
+ // const path = requestOptions.path || '/'; // 这行没用到,可以删掉
493
+ const proxyUrl = `http://${proxyConfig.ip}:${proxyConfig.port}`;
494
+ const agentOptions = {
495
+ keepAlive: true,
496
+ rejectUnauthorized: false // 忽略 SSL 证书错误
497
+ };
498
+ const agent = isHttps ? new HttpsProxyAgent(proxyUrl, agentOptions) : new HttpProxyAgent(proxyUrl, agentOptions);
499
+
500
+ // 注意:url 已包含 query string(如 /search?q=1)
501
+ var targetUrl = url;
502
+ // const parsedTargetUrl = new URL(targetUrl); // 这行没用到,可以删掉
503
+ const finalHeaders = { ...requestOptions.headers };
504
+ // finalHeaders['host'] = hostname; // 通常不需要手动设置 Host,axios/http(s) 会根据 URL 自动设置。删除这行。
505
+
506
+ // 准备 axios 配置
507
+ const axiosConfig = {
508
+ url: targetUrl,
509
+ method: requestOptions.method || 'GET',
510
+ headers: finalHeaders,
511
+ data: body, // 确保 body 是 Buffer 或其他 Axios 支持的格式 (String, Stream)
512
+ httpAgent: agent,
513
+ httpsAgent: agent,
514
+ responseType: 'stream', // 正确处理二进制响应
515
+ maxRedirects: 21
516
+ // validateStatus: () => true, // 如果你想自己处理所有状态码,取消注释
517
+ };
518
+
519
+ try {
520
+ // console.log('---------------------->');
521
+ // console.log(targetUrl, finalHeaders['accept']);
522
+ const response = await axios(axiosConfig);
523
+
524
+ // 将响应流读取为 Buffer
525
+ const chunks = [];
526
+ for await (const chunk of response.data) {
527
+ chunks.push(chunk);
528
+ }
529
+ const responseBody = Buffer.concat(chunks);
530
+
531
+ // console.log('<-----------------------');
532
+ // console.log(targetUrl, response.status, response.headers);
533
+
534
+ return {
535
+ statusCode: response.status,
536
+ headers: response.headers,
537
+ body: responseBody // 返回 Buffer
538
+ };
539
+ } catch (error) {
540
+ if (error.code === "ERR_BAD_RESPONSE") {
541
+ return {
542
+ statusCode: error.response?.status,
543
+ headers: error.response?.headers,
544
+ body: error.response?.statusText // 返回错误响应的 Buffer 体
545
+ };
546
+ } else if (error.response) {
547
+ // 服务器返回了错误状态码(如 4xx, 5xx)
548
+ // 错误响应体也可能是二进制 (Protobuf)
549
+ let errorResponseBody = Buffer.alloc(0); // 初始化为空 Buffer
550
+
551
+ if (error.response.data) {
552
+ // Axios 在 responseType: 'stream' 时,即使出错,error.response.data 也可能是一个 Stream
553
+ if (error.response.data.readable === true) { // 检查是否是可读流
554
+ const errorChunks = [];
555
+ try {
556
+ for await (const chunk of error.response.data) {
557
+ errorChunks.push(chunk);
558
+ }
559
+ errorResponseBody = Buffer.concat(errorChunks);
560
+ } catch (streamErr) {
561
+ console.error("Error reading error response stream:", streamErr);
562
+ // 即使读取出错,我们也返回已收集的部分或空 Buffer
563
+ errorResponseBody = Buffer.concat(errorChunks); // 尽力而为
564
+ }
565
+ } else if (typeof error.response.data === 'string') {
566
+ // 理论上在 responseType: 'stream' 下不太可能出现这种情况,但以防万一
567
+ errorResponseBody = Buffer.from(error.response.data, 'utf-8');
568
+ } else if (Buffer.isBuffer(error.response.data)) {
569
+ // 如果 Axios 以某种方式直接给了 Buffer (不太常见)
570
+ errorResponseBody = error.response.data;
571
+ }
572
+ // 如果都不是,则保持 errorResponseBody 为空 Buffer
573
+ }
574
+
575
+ return {
576
+ statusCode: error.response.status,
577
+ headers: error.response.headers,
578
+ body: errorResponseBody // 返回错误响应的 Buffer 体
579
+ };
580
+ }
581
+
582
+ // 网络错误(如 ECONNREFUSED, ETIMEDOUT)
583
+ console.error("Network error in forwardViaLocalProxy:", error.message);
584
+ throw error; // 重新抛出网络错误,让上游处理
585
+ }
586
+ }
587
+
588
+ // Rule 里的匹配规则在这里被依次处理
589
+ // type: beforeSendResponse 和 beforeSendRequest
590
+ // 常规的 reject - 200 直接在后台界面里配就可以,复杂逻辑用 Rule
591
+ async function MITMHandler(type, url, request, response) {
592
+ var responseResult = null;
593
+ var Ms = [];
594
+
595
+ Object.keys(Rule).forEach(key => {
596
+ Ms = Ms.concat(Rule[key]);
597
+ });
598
+
599
+ for (const item of Ms) {
600
+ // type 匹配
601
+ // 域名匹配
602
+ // 正则匹配
603
+ if (item['type'].toLowerCase() == type.toLowerCase() &&
604
+ new URL(url).hostname.toLowerCase().endsWith(item['host'].toLowerCase()) &&
605
+ item.compiledRegexp.test(url)) {
606
+ // new RegExp(item['regexp']).test(url)) {
607
+ // 只对需要 MITM 的 beforeSendResponse 启用解压缩,确保 MITM 处理的逻辑是解压后的明文
608
+ if (type == "beforeSendResponse" && !isEmpty(response)) {
609
+ try {
610
+ response = await parseResponseFromZippedChunk(response);
611
+ } catch (e) {
612
+ console.log(e);
613
+ }
614
+ }
615
+ responseResult = await item.callback(url, request, response);
616
+ break;
617
+ } else {
618
+ continue;
619
+ }
620
+ }
621
+
622
+ // 要么是重写后的 response 对象,要么是 null
623
+ // beforeSendResponse 中应当返回原 response,应当在 callback 中处理
624
+ return responseResult;
625
+ }
626
+
627
+ // 为 MITM 处理响应结果 body 的解压缩
628
+ // 之前是在 Anyproxy 里做,每个 response 都处理解压缩,目的是为了返回明文,抓包看明文用的,这里没必要
629
+ // 只需对 mitm 做解压就可以,其他的不需要解压缩的就完全透给客户端
630
+ // requestHandler.js 120 行 request() 里的逻辑
631
+ function parseResponseFromZippedChunk(response) {
632
+ return new Promise((resolve, reject) => {
633
+ var resHeader = response.header;
634
+ const contentEncoding = resHeader['content-encoding'] || resHeader['Content-Encoding'];
635
+ const ifServerGzipped = /gzip/i.test(contentEncoding);
636
+ const isServerDeflated = /deflate/i.test(contentEncoding);
637
+ const isBrotlied = /br/i.test(contentEncoding);
638
+
639
+ const serverResData = response.body ? response.body : Buffer.alloc(0);
640
+ const originContentLen = Buffer.byteLength(serverResData);
641
+ resHeader['x-anyproxy-origin-content-length'] = originContentLen;
642
+
643
+ const refactContentEncoding = () => {
644
+ if (contentEncoding) {
645
+ resHeader['x-anyproxy-origin-content-encoding'] = contentEncoding;
646
+ delete resHeader['content-encoding'];
647
+ delete resHeader['Content-Encoding'];
648
+ }
649
+ }
650
+
651
+ const formatResponse = (newBody) => {
652
+ return {
653
+ ...response,
654
+ header: resHeader,
655
+ body: newBody,
656
+ // rawBody: rawResChunks,
657
+ // _res: res
658
+ };
659
+ }
660
+
661
+ if (ifServerGzipped && originContentLen) {
662
+ refactContentEncoding();
663
+ zlib.gunzip(serverResData, (err, buff) => {
664
+ if (err) {
665
+ reject(err);
666
+ } else {
667
+ resolve(formatResponse(buff));
668
+ }
669
+ });
670
+ } else if (isServerDeflated && originContentLen) {
671
+ refactContentEncoding();
672
+ zlib.inflate(serverResData, (err, buff) => {
673
+ if (err) {
674
+ reject(err);
675
+ } else {
676
+ resolve(formatResponse(buff));
677
+ }
678
+ });
679
+ } else if (isBrotlied && originContentLen) {
680
+ refactContentEncoding();
681
+ zlib.brotliDecompress(serverResData, (err, buff) => {
682
+ if (err) {
683
+ reject(err);
684
+ } else {
685
+ resolve(formatResponse(buff));
686
+ }
687
+ });
688
+ } else {
689
+ resolve(formatResponse(serverResData));
690
+ }
691
+ });
692
+ }
693
+
694
+ async function rewriteRuleBeforeResponse(host, url, request, response) {
695
+ var responseResult = null;
696
+ responseResult = await MITMHandler('beforeSendResponse', url, request, response);
697
+ if (responseResult === null) {
698
+ return false;
699
+ } else {
700
+ return responseResult;
701
+ }
702
+ }
703
+
704
+ async function rewriteRuleBeforeRequest(host, url, request) {
705
+ var responseResult = null;
706
+ responseResult = await MITMHandler('beforeSendRequest', url, request, {});
707
+ if (responseResult === null) {
708
+ return false;
709
+ } else {
710
+ return responseResult;
711
+ }
712
+ }
713
+
714
+ // a.com:443 → a.com
715
+ function trimHost(host) {
716
+ if (/:\d+$/.test(host)) {
717
+ host = host.split(":")[0];
718
+ }
719
+ return host;
720
+ }
721
+
722
+ // 需要强制拆包的域名从 Rule 里获得
723
+ // host: a.com
724
+ // a.com:443
725
+ function shouldMitm(host) {
726
+ host = trimHost(host);
727
+ var should = false;
728
+ var mitm_list = [];
729
+ Object.keys(Rule).forEach(key => {
730
+ if (Rule.hasOwnProperty(key)) {
731
+ Rule[key].forEach(function(item) {
732
+ mitm_list.push(item.host);
733
+ });
734
+ }
735
+ });
736
+
737
+ var MITM_LIST = [...new Set(mitm_list)];
738
+ MITM_LIST.some(function(item) {
739
+ if (host.toLowerCase().endsWith(item.toLowerCase())) {
740
+ should = true;
741
+ return true;
742
+ } else {
743
+ return false;
744
+ }
745
+ });
746
+ return should;
747
+ }
748
+
749
+ // 监听 progress_time_stamp 是否有变化,有的话就重启代理服务
750
+ var oldTimeStamp = progress_time_stamp;
751
+ var restartTimer = null;
752
+ function restartProxyListener() {
753
+ fs.watch(configPath, async (eventType, filename) => {
754
+ var newConfig = await loadConfig();
755
+ var newTimeStamp = newConfig.progress_time_stamp;
756
+ if (newTimeStamp === oldTimeStamp) {
757
+ return false;
758
+ } else {
759
+ // 防止重复启动
760
+ if (restartTimer == null) {
761
+ restartTimer = setTimeout(async () => {
762
+ await LocalProxy.restart(() => {});
763
+ restartTimer = null;
764
+ }, 200);
765
+ }
766
+ oldTimeStamp = newTimeStamp;
767
+ }
768
+ });
769
+ }
770
+
771
+ // 将 rawHeaders 转换为对象的辅助函数
772
+ function parseHeaders(rawHeaders) {
773
+ const headers = {};
774
+ for (let i = 0; i < rawHeaders.length; i += 2) {
775
+ const key = rawHeaders[i].toLowerCase(); // 转换为小写以匹配标准
776
+ const value = rawHeaders[i + 1];
777
+ headers[key] = value;
778
+ }
779
+ return headers;
780
+ }
781
+
782
+ function getProxyAuthConfig() {
783
+ return {
784
+ auth_username,
785
+ auth_password
786
+ };
787
+ }
788
+
789
+ // 对于一些流媒体的链接不支持 407 的情况要排除验证
790
+ // host 可能携带端口:a.com:443
791
+ function authPass(protocol, host, url) {
792
+ const passHosts = [
793
+ "googlevideo.com", // Toutube 视频流
794
+ "dns.weixin.qq.com.cn", // 微信的 dns 预解析
795
+ "weixin.qq.com",
796
+ ...filtered_mitm_domains
797
+ ];
798
+ // 基于 http 传输的流
799
+ const passUrl = [
800
+ /\.(m3u8|mp4|mpd|ts|webm|avi|mkv)$/i
801
+ ];
802
+
803
+ host = trimHost(host);
804
+
805
+ var pass = false;
806
+
807
+ // 检查流媒体域名的排除项
808
+ passHosts.some(function(item) {
809
+ if (host.toLowerCase().endsWith(item.toLowerCase())) {
810
+ pass = true;
811
+ return true;
812
+ } else {
813
+ return false;
814
+ }
815
+ });
816
+
817
+ if (pass) {
818
+ return pass;
819
+ }
820
+
821
+ // 检查流媒体类型的排除项
822
+ if (url != null) {
823
+ passUrl.some(function(item) {
824
+ if (item.test(url)) {
825
+ pass = true;
826
+ return true;
827
+ } else {
828
+ return false;
829
+ }
830
+ });
831
+ }
832
+
833
+ return pass;
834
+ }
835
+
836
+ function passRequestWithHttpAgent(requestDetail, isHttps) {
837
+ return {
838
+ ...requestDetail,
839
+ requestOptions: {
840
+ ...requestDetail.requestOptions,
841
+ agent: isHttps ? httpsAgent : httpAgent,
842
+ }
843
+ };
844
+ }
845
+
846
+ function getAnyProxyOptions() {
847
+ return {
848
+ port: proxyPort,
849
+ rule: {
850
+ // 验证 Proxy-Authorization
851
+ // protocol: http, https
852
+ // req: 原始的 Request
853
+ // url: 拆包后的 URL,如果是 Connect 环节校验则为 null
854
+ checkProxyAuth(protocol, req, sourceIp, url) {
855
+ // 如果是 Socks 端口转发的请求,一律放行,身份校验在 Socks 代理做了
856
+ // 这里不用在做一次身份校验了
857
+ if (sourceIp === "127.0.0.1") {
858
+ return true;
859
+ }
860
+
861
+ const authConfig = getProxyAuthConfig();
862
+ if (authConfig.auth_username === undefined) {
863
+ console.log("authConfig.auth_username 为空,检查下 config.json 完整性");
864
+ }
865
+ const expectedUser = authConfig.auth_username;
866
+ const expectedPass = authConfig.auth_password;
867
+ // 如果 auth_username 为空,则始终验证通过
868
+ if (expectedUser === "") {
869
+ return true;
870
+ }
871
+
872
+ // 恶意扫描 IP 始终拒绝
873
+ if (sourceIp != "127.0.0.1" &&
874
+ sourceIp != "255.255.255.254" &&
875
+ !sourceIp.startsWith("192.168.") &&
876
+ attacker.isBadGuy(sourceIp)) {
877
+ console.log('[🚫]>> 拦截 badguy', sourceIp);
878
+ return this.sendAuthRequired();
879
+ }
880
+
881
+ const headers = parseHeaders(req.rawHeaders);
882
+
883
+ // 对于一些特殊情况需要放行的
884
+ if (authPass(protocol, headers.host, url)) {
885
+ attacker.setGoodGuy(sourceIp);
886
+ return true;
887
+ }
888
+
889
+ const authHeader = headers['proxy-authorization'];
890
+
891
+ // Hack:
892
+ // xiaohongshu.com:443,小红书App和知乎 App 里发起带端口的请求,收到 407 后第二次
893
+ // 请求不会带上authentication,这是 App 的 bug,为了避免功能不可用,这里统一 Hack 掉。
894
+ if (/:\d+$/ig.test(headers['host'])) {
895
+ return true;
896
+ }
897
+
898
+ if (!authHeader || !authHeader.startsWith('Basic ')) {
899
+ return this.sendAuthRequired();
900
+ }
901
+
902
+ const credentials = authHeader.substring(6); // 去掉 'Basic ' 前缀
903
+ let decoded;
904
+ try {
905
+ decoded = Buffer.from(credentials, 'base64').toString('utf8');
906
+ } catch (e) {
907
+ return this.sendAuthRequired();
908
+ }
909
+
910
+ const [user, pass] = decoded.split(':');
911
+ if (user !== expectedUser || pass !== expectedPass) {
912
+ return this.sendAuthRequired();
913
+ }
914
+
915
+ attacker.setGoodGuy(sourceIp);
916
+ return true; // 验证通过
917
+ },
918
+
919
+ // 返回 407 Proxy Authentication Required
920
+ sendAuthRequired() {
921
+ var body = "Proxy authentication required..";
922
+ return {
923
+ response: {
924
+ statusCode: 407,
925
+ header: {
926
+ 'Proxy-Authenticate': 'Basic realm="AnyProxy Secure Proxy"',
927
+ 'Connection': 'close',
928
+ 'Content-Length': getContentLength(body)
929
+ },
930
+ body: body
931
+ }
932
+ };
933
+ },
934
+
935
+ send407bySocket(socket, sourceIp) {
936
+ // 拒绝一次记录一次
937
+ var counter = attacker.countIPAccess(sourceIp);
938
+
939
+ var body = "Proxy authentication required.";
940
+ const response407 = [
941
+ 'HTTP/1.1 407 Proxy Authentication Required',
942
+ 'Proxy-Authenticate: Basic realm="AnyProxy Secure Proxy"',
943
+ 'Content-Type: text/plain; charset=utf-8',
944
+ 'Content-Length: ' + getContentLength(body), // 'Proxy authentication required.'.length
945
+ 'Connection: close', // 明确指示关闭连接
946
+ '', // 空行表示头部结束
947
+ body // 响应体
948
+ ].join('\r\n');
949
+ const clientSocket = socket; // 获取原始客户端 socket
950
+ clientSocket.write(response407, () => {
951
+ clientSocket.destroy(); // 确保 write 完成后再关闭
952
+ });
953
+ },
954
+ // 只对特定域名启用 HTTPS 拦截,无规则时直接四层转发
955
+ async beforeDealHttpsRequest(requestDetail, next) {
956
+ const clientIp = getRemoteAddressFromReq(requestDetail);
957
+ // 如果配置了 vpn_proxy,全部走解密逻辑,仅调试使用
958
+ if (vpn_proxy != "" && vpn_proxy !== undefined) {
959
+ return true;
960
+ }
961
+
962
+ var authResult = this.checkProxyAuth('https', requestDetail._req, clientIp, null);
963
+
964
+ if (authResult !== true) {
965
+ // 认证失败,立即发送 407 并关闭连接
966
+ this.send407bySocket(requestDetail._req.socket, clientIp);
967
+ return false; // 兜底逻辑,阻止调用 beforeSendRequest
968
+ }
969
+
970
+ const blockRules = getBlockRules(clientIp);
971
+ // requestDetail.host 是域名+端口的形式
972
+ const host = requestDetail.host.split(":")[0];
973
+
974
+ // rewrite 规则判断
975
+ if (shouldMitm(host)) {
976
+ return true; // 强制 MITM
977
+ }
978
+
979
+ // HTTPS 这里只判断 ip 源和域名
980
+ // 域名不匹配的就直接转发
981
+ // 域名匹配的情况下,再去看 match_rule 的判断,放到 beforeSendRequest 中
982
+
983
+ // 如果是裸IP请求,全部放行
984
+ if (net.isIPv4(host) || net.isIPv6(host)) {
985
+ return false;
986
+ }
987
+
988
+ // 如果没有对应ip的匹配规则
989
+ let shouldBlock = shouldBlockHost(host, blockRules, "");
990
+ if (blockRules.length === 0) {
991
+ return false;
992
+ } else if (shouldBlock) {
993
+ console.log('https 拦截', host, '接下来判断是否根据 match_rule 进行拦截');
994
+ // 只对配置中的域名进行 HTTPS 拦截
995
+ return true; // 允许 HTTPS 拦截
996
+ }
997
+ return false; // 不拦截 HTTPS
998
+ },
999
+
1000
+ // 拦截 HTTP 请求以及 HTTPS 拆包的请求
1001
+ async beforeSendRequest(requestDetail) {
1002
+ const { url, requestOptions } = requestDetail;
1003
+ const clientIp = requestDetail._req?.sourceIp || '127.0.0.1';
1004
+ const host = requestDetail.requestOptions.hostname;
1005
+ const blockRules = getBlockRules(clientIp);
1006
+ const pathname = requestDetail.requestOptions.path?.split('?')[0];;
1007
+ const body = requestDetail.requestData;
1008
+ const isHttps = url.startsWith('https:') ? true : false;
1009
+ const isHttp = !isHttps;
1010
+
1011
+ // 如果直接访问当前 IP 的代理端口
1012
+ // 如果请求的目的是自己,防止代理回环
1013
+ // 这里没办法穷举,只能约定防火墙里绑定的转发端口和 AnyProxy 的代理端口保持一致
1014
+ // 只要不绑定其他端口,就绝对不会陷入回环问题
1015
+ const isDocker = is_running_in_docker;
1016
+ var myIp = isDocker ? docker_host_IP : localIp;
1017
+ if ((myIp.includes(requestOptions.hostname) && requestOptions.port == proxyPort.toString()) ||
1018
+ (requestOptions.hostname == your_domain && requestOptions.port == proxyPort.toString()) ||
1019
+ (requestOptions.hostname == wan_ip && requestOptions.port == proxyPort.toString()) ||
1020
+ // 如果这里收到了 localhost 或 127.0.0.1 的访问,一定是本机访问,其他机器访问 localhost 是不会走远端代理的
1021
+ (host == "localhost" || host == "127.0.0.1")
1022
+ ) {
1023
+ if (pathname === "/favicon.ico") {
1024
+ return {
1025
+ response:{
1026
+ statusCode:200,
1027
+ body: Buffer.alloc(0)
1028
+ }
1029
+ };
1030
+ } else if (pathname == "/restart_docker") {
1031
+ if (isDocker) {
1032
+ var msg = "请手动重启 Docker 容器。";
1033
+ } else {
1034
+ var msg = "当前程序不在 Docker 容器内,请在终端终止程序后再 npm run start 启动。";
1035
+ }
1036
+ return {
1037
+ response: {
1038
+ statusCode: 200,
1039
+ header: { 'Content-Type': 'text/plain; charset=utf-8' },
1040
+ body: msg
1041
+ }
1042
+ };
1043
+ } else if (pathname == "/enable_express") {
1044
+ var configData = await _fs.readConfig();
1045
+ _fs.writeConfig({
1046
+ ...configData,
1047
+ enable_express: "1"
1048
+ });
1049
+ return {
1050
+ response: {
1051
+ statusCode: 200,
1052
+ header: { 'Content-Type': 'text/plain; charset=utf-8' },
1053
+ body: "开启 express 后台设置成功,请重启 Docker。"
1054
+ }
1055
+ };
1056
+ } else if (pathname == "/disable_express") {
1057
+ var configData = await _fs.readConfig();
1058
+ _fs.writeConfig({
1059
+ ...configData,
1060
+ enable_express: "0"
1061
+ });
1062
+ return {
1063
+ response: {
1064
+ statusCode: 200,
1065
+ header: { 'Content-Type': 'text/plain; charset=utf-8' },
1066
+ body: "关闭 express 后台设置成功,请重启 Docker。"
1067
+ }
1068
+ };
1069
+ } else if (pathname == "/enable_socks5") {
1070
+ var configData = await _fs.readConfig();
1071
+ _fs.writeConfig({
1072
+ ...configData,
1073
+ enable_socks5: "1"
1074
+ });
1075
+ return {
1076
+ response: {
1077
+ statusCode: 200,
1078
+ header: { 'Content-Type': 'text/plain; charset=utf-8' },
1079
+ body: "开启 socks5 成功,请重启 Docker。"
1080
+ }
1081
+ };
1082
+ } else if (pathname == "/disable_socks5") {
1083
+ var configData = await _fs.readConfig();
1084
+ _fs.writeConfig({
1085
+ ...configData,
1086
+ enable_socks5: "0"
1087
+ });
1088
+ return {
1089
+ response: {
1090
+ statusCode: 200,
1091
+ header: { 'Content-Type': 'text/plain; charset=utf-8' },
1092
+ body: "关闭 socks5 成功,请重启 Docker。"
1093
+ }
1094
+ };
1095
+ } else if (pathname == "/disable_webinterface") {
1096
+ var configData = await _fs.readConfig();
1097
+ _fs.writeConfig({
1098
+ ...configData,
1099
+ enable_webinterface: "0"
1100
+ });
1101
+ return {
1102
+ response: {
1103
+ statusCode: 200,
1104
+ header: { 'Content-Type': 'text/plain; charset=utf-8' },
1105
+ body: "关闭 webinterface 成功,请重启 Docker。"
1106
+ }
1107
+ };
1108
+ } else if (pathname == "/enable_webinterface") {
1109
+ var configData = await _fs.readConfig();
1110
+ _fs.writeConfig({
1111
+ ...configData,
1112
+ enable_webinterface: "1"
1113
+ });
1114
+ return {
1115
+ response: {
1116
+ statusCode: 200,
1117
+ header: { 'Content-Type': 'text/plain; charset=utf-8' },
1118
+ body: "启用 webinterface 成功,请重启 Docker。"
1119
+ }
1120
+ };
1121
+ } else {
1122
+ return {
1123
+ response: {
1124
+ statusCode: 200,
1125
+ header: { 'Content-Type': 'text/html; charset=utf-8' },
1126
+ body: '<pre>' + await monitor.getSystemMonitorInfo(proxyPort) + '</pre>'
1127
+ }
1128
+ };
1129
+ }
1130
+ }
1131
+
1132
+ // 如果是裸IP请求,全部放行
1133
+ if (net.isIPv4(host) || net.isIPv6(host)) {
1134
+ return passRequestWithHttpAgent(requestDetail, isHttps);
1135
+ }
1136
+
1137
+ // Hack, 根据 UA 判断是否符合放行条件,比如 Youtube 的 MITM 只对 App 生效,则浏览器的 UA 就需要放行
1138
+ if (uaFilter.match(requestOptions.headers, host)) {
1139
+ return passRequestWithHttpAgent(requestDetail, isHttps);
1140
+ }
1141
+
1142
+ // 这里验证只能处理 HTTP 请求,HTTPs 里 _req 携带的请求头是不包含验证字段的,因为
1143
+ // https 内的 header 是五层信息,proxy-Authenticate 信息属于四层,这里看不到
1144
+ if (isHttp) {
1145
+ var authResult = this.checkProxyAuth('http',requestDetail._req, clientIp, url);
1146
+ if (authResult === true) {
1147
+ // 验证通过,do Nothing
1148
+ } else {
1149
+ // 验证不通过,返回 407
1150
+ this.send407bySocket(requestDetail._req.socket, clientIp);
1151
+ return authResult; // 兜底逻辑,强制返回
1152
+ }
1153
+ }
1154
+
1155
+ var _request = { ...requestDetail.requestOptions };
1156
+ _request.host = requestDetail.requestOptions.hostname;
1157
+ _request.url = requestDetail.url;
1158
+ _request.body = requestDetail.requestData;
1159
+ _request.protocol = requestDetail.protocol;
1160
+
1161
+ // 如果是 http 请求,没有经过 beforeDealHttpsRequest,因此clientIp是真实的
1162
+ // 执行逻辑:
1163
+ // 1. http 协议请求到这里
1164
+ // 2. https 协议根据域名需要拦截,转发到这里,到这里已经完成了拆包
1165
+ // 这里统一做:域名 + match_rule + mac 的拦截和重写
1166
+ // 3. 在拦截之后,判断是否匹配重写规则,有则执行重写规则
1167
+ // 4. 开启 vpn_proxy 时,所有请求都走这里,主要是方便调试用,正是环境不要打开 vpn_proxy
1168
+
1169
+ // 如果当前 IP 没有配置拦截规则,检查重写逻辑并判断直接放行
1170
+ if (blockRules.length === 0) {
1171
+ // 先匹配重写规则
1172
+ var rewriteResult = await rewriteRuleBeforeRequest(host, url, _request);
1173
+ if (rewriteResult !== false) {
1174
+ return rewriteResult;
1175
+ } else if (vpn_proxy != "" && vpn_proxy !== undefined) {
1176
+ // 如果配置了 vpn_proxy,不匹配拦截的情况,所有请求都通过 proxy 做七层转发
1177
+ // TODO: 需要调试所有请求转发的失败的情况,所有请求都应该转发成功
1178
+ const { ip, port } = parseAddress(vpn_proxy);
1179
+ const result = await forwardViaLocalProxy(url, requestOptions, body, {
1180
+ ip: ip, port: port
1181
+ });
1182
+ return {
1183
+ response: {
1184
+ statusCode: result.statusCode,
1185
+ header: result.headers,
1186
+ body: result.body
1187
+ }
1188
+ };
1189
+ } else {
1190
+ // 其他情况一律放行
1191
+ /// console.log("[✅] 1 " + url);
1192
+ return passRequestWithHttpAgent(requestDetail, isHttps);
1193
+ }
1194
+ }
1195
+ // 如果当前 IP 有针对域名和 url 匹配 matchRule 的规则,则拦截
1196
+ if (shouldBlockHost(host, blockRules, url)) {
1197
+ // 如果是列表中的域名则拦截
1198
+ /// console.log(`[⭕️] ${url}`);
1199
+ // 为被拦截的域名返回自定义响应
1200
+ let customHosts = filtered_mitm_domains;
1201
+ if (customHosts.some(domain => host.endsWith(domain) || host === domain)) {
1202
+ // 如果是拦截域名
1203
+ return {
1204
+ response: {
1205
+ statusCode: 200,
1206
+ header: {
1207
+ 'Content-Type': 'text/html; charset=UTF-8',
1208
+ Pragma: 'no-cache',
1209
+ Expires: 'Fri, 01 Jan 1990 00:00:00 GMT',
1210
+ 'Cache-Control': 'no-cache, must-revalidate',
1211
+ 'X-Content-Type-Options': 'nosniff',
1212
+ 'Server':'Video Stats Server',
1213
+ 'Content-Length': 0,
1214
+ 'X-XSS-Protection': 0,
1215
+ 'X-Frame-Options': 'SAMEORIGIN',
1216
+ 'Alt-Svc': 'h3=":443"; ma=2592000,h3-29=":443"; ma=2592000'
1217
+ },
1218
+ body: Buffer.alloc(0)
1219
+ }
1220
+ };
1221
+ } else {
1222
+ // 如果是其他域名
1223
+ var customBody = "Blocked by anyproxy."
1224
+ return {
1225
+ response: {
1226
+ statusCode: 200,
1227
+ header: {
1228
+ 'Content-Type': 'text/plain; charset=UTF-8',
1229
+ 'Content-Length': getContentLength(customBody)
1230
+ },
1231
+ body: customBody
1232
+ }
1233
+ };
1234
+ }
1235
+ }
1236
+
1237
+ // 最后做一轮重写逻辑检查
1238
+ var rewriteResult = await rewriteRuleBeforeRequest(host, url, _request);
1239
+ if (rewriteResult !== false) {
1240
+ return rewriteResult;
1241
+ }
1242
+ /// console.log("[✅] 2 " + url);
1243
+ // 如果重写逻辑也不匹配,则请求放行
1244
+ return passRequestWithHttpAgent(requestDetail, isHttps);
1245
+ },
1246
+
1247
+ async beforeSendResponse(requestDetail, responseDetail) {
1248
+ /// console.log(`[↩️] ${requestDetail.url}`);
1249
+ const host = requestDetail.requestOptions.hostname;
1250
+
1251
+ var _request = { ...requestDetail.requestOptions };
1252
+ _request.host = requestDetail.requestOptions.hostname;
1253
+ _request.url = requestDetail.url;
1254
+ _request.body = requestDetail.requestData;
1255
+ _request.protocol = requestDetail.protocol;
1256
+
1257
+ var rewriteResult = await rewriteRuleBeforeResponse(host, requestDetail.url, _request, responseDetail.response);
1258
+ if (rewriteResult !== false) {
1259
+ return rewriteResult;
1260
+ }
1261
+ return null;
1262
+ },
1263
+
1264
+ async onError(requestDetail, error) {
1265
+ // 资源不可达
1266
+ if (error.code == "ENETUNREACH") {
1267
+ return {
1268
+ response: {
1269
+ statusCode: 404,
1270
+ header: { 'Content-Type': 'text/plain; charset=utf-8' },
1271
+ body: `AnyProxy Error: ${error.code}`
1272
+ }
1273
+ };
1274
+ } else if (error.code == "ENOTFOUND") {
1275
+ // DNS 请求出错
1276
+ return {
1277
+ response: {
1278
+ statusCode: 502,
1279
+ header: {
1280
+ 'Content-Type': 'text/plain; charset=utf-8',
1281
+ 'Connection': 'close',
1282
+ 'x-blockproxy-errorcode':"ENOTFOUND"
1283
+ },
1284
+ body: 'DNS_PROBE_FINISHED_NXDOMAIN, DNS lookup error.'
1285
+ }
1286
+ };
1287
+ } else if (error.code =="HPE_INVALID_VERSION") {
1288
+ // 请求的返回是http 0.0版本:
1289
+ // HTTP/0.0 307 Temporary Redirect\r\n
1290
+ // Location: https://a.yui.cool:88/\r\n
1291
+ // Content-Length: 0\r\n
1292
+ // \r\n
1293
+ const result = await _request(requestDetail.requestOptions);
1294
+ // console.log(result);
1295
+ return {
1296
+ response: {
1297
+ statusCode: result.statusCode,
1298
+ header: {
1299
+ ...result.headers,
1300
+ 'x-blockproxy-transfer': "true",
1301
+ 'x-blockproxy-errorcode':"HPE_INVALID_VERSION"
1302
+ },
1303
+ body: result.body
1304
+ }
1305
+ };
1306
+ } else if (error.code == "HPE_INVALID_CONTENT_LENGTH" || error.code == "HPE_UNEXPECTED_CONTENT_LENGTH") {
1307
+ // HPE_INVALID_CONTENT_LENGTH 是 http 的响应同时包含了 content-length
1308
+ // 和 Transfer-Encoding: chunked 时的报错,这类响应不符合 http 的规范
1309
+ // AnyProxy 中的 http.request 报此错误。
1310
+ // 但为了保证兼容性,对于这类错误的请求也应当转发,只要是浏览器能处理的
1311
+ // Proxy 代理就应当转发。因此重写了不校验 header 字段的 _request 方法
1312
+ // 重新请求目标资源,并直接返回
1313
+ //
1314
+ // 这个方法仍可能会有错误,尽管做了转发,由于 content-length 不能保证
1315
+ // 完全正确,所以 socket 根据 content-length 截断的时机可能不对,这里
1316
+ // 不做更多的报错,一并把错误的结果发给客户端
1317
+ const result = await _request(requestDetail.requestOptions);
1318
+ return {
1319
+ response: {
1320
+ statusCode: result.statusCode,
1321
+ header: {
1322
+ ...result.headers,
1323
+ 'x-blockproxy-transfer': "true",
1324
+ 'x-blockproxy-errorcode':"HPE_INVALID_CONTENT_LENGTH"
1325
+ },
1326
+ body: result.body
1327
+ }
1328
+ };
1329
+ }
1330
+ },
1331
+
1332
+ async onConnectError(requestDetail, error) {
1333
+ return null;
1334
+ },
1335
+
1336
+ },
1337
+ webInterface: {
1338
+ enable: enable_webinterface == "1" ? true : false,
1339
+ webPort: webInterfacePort
1340
+ },
1341
+ throttle: 800 * 1024 * 1024, // 800 Mbps
1342
+ forceProxyHttps: false, // 关闭全局 HTTPS 拦截
1343
+ wsIntercept: false,
1344
+ silent: true,
1345
+ timeout: 60 * 1000 // 60秒
1346
+ };
1347
+ }
1348
+
1349
+ function delay(ms) {
1350
+ return new Promise(resolve => setTimeout(resolve, ms));
1351
+ }
1352
+
1353
+ var LocalProxy = {
1354
+ updateDevices: async function() {
1355
+ var configData = await loadConfig();
1356
+ var oldRouterMap = configData.devices || []; // 确保旧路由表是数组
1357
+ var newRouterMap = []
1358
+ try {
1359
+ newRouterMap = await scanNetwork();
1360
+ } catch (e) {
1361
+ newRouterMap = [];
1362
+ }
1363
+
1364
+ var mergedRouterMap = [];
1365
+ // 把新的路由表中变更和新增的部分增补到 oldRouterMap 中
1366
+ // 形成新的 mergedRouterMap
1367
+
1368
+ // 创建一个以IP为键的映射表,用于快速查找现有设备
1369
+ const oldDeviceMap = {};
1370
+ oldRouterMap.forEach(device => {
1371
+ oldDeviceMap[device.ip] = device;
1372
+ });
1373
+
1374
+ // 初始化合并后的设备列表为旧设备列表
1375
+ mergedRouterMap = [...oldRouterMap];
1376
+
1377
+ // 处理每一个新扫描到的设备
1378
+ newRouterMap.forEach(newDevice => {
1379
+ const existingDevice = oldDeviceMap[newDevice.ip];
1380
+
1381
+ // 如果这是一个新设备(IP不存在于旧列表中)
1382
+ if (!existingDevice) {
1383
+ mergedRouterMap.push(newDevice);
1384
+ console.log(`新增设备: ${newDevice.ip} (${newDevice.mac})`);
1385
+ }
1386
+ // 如果设备已存在但MAC地址发生了变化
1387
+ else if (existingDevice.mac !== newDevice.mac) {
1388
+ // 找到该设备在合并列表中的索引
1389
+ const index = mergedRouterMap.findIndex(device => device.ip === newDevice.ip);
1390
+ // 更新设备信息
1391
+ mergedRouterMap[index] = newDevice;
1392
+ console.log(`更新设备: ${newDevice.ip} (${existingDevice.mac} -> ${newDevice.mac})`);
1393
+ }
1394
+ });
1395
+
1396
+ _fs.writeConfig({
1397
+ ...configData,
1398
+ devices: mergedRouterMap
1399
+ });
1400
+ // fs.writeFileSync(configPath, JSON.stringify({
1401
+ // }, null, 2));
1402
+ devices = mergedRouterMap;
1403
+ console.log('Devices updated!');
1404
+ },
1405
+ start: async function(callback) {
1406
+ // 每次启动时都重新加载配置
1407
+ const config = await loadConfig();
1408
+
1409
+ // 如果代理服务器已在运行,先停止它
1410
+ if (proxyServerInstance && proxyServerInstance.httpProxyServer && proxyServerInstance.httpProxyServer.listening) {
1411
+ proxyServerInstance.close();
1412
+ proxyServerInstance = null;
1413
+ await delay(1000);
1414
+ console.log('重新启动代理服务器');
1415
+ startProxyServer();
1416
+ if (typeof callback === 'function') {
1417
+ callback();
1418
+ }
1419
+ } else {
1420
+ startProxyServer();
1421
+ if (typeof callback === 'function') {
1422
+ callback();
1423
+ }
1424
+ }
1425
+ },
1426
+ restart: async function(callback) {
1427
+ // 实现重启功能
1428
+ if (proxyServerInstance) {
1429
+ console.log('Restarting proxy server...');
1430
+ proxyServerInstance.close();
1431
+ await delay(1000);
1432
+ console.log('重新启动代理服务器');
1433
+ await this.start(callback);
1434
+ } else {
1435
+ // 如果没有运行中的实例,直接启动
1436
+ await this.start(callback);
1437
+ }
1438
+ },
1439
+
1440
+ // 代理服务启动,并同时启动定时任务
1441
+ init: async function() {
1442
+ var that = this;
1443
+ if (anyproxy_started === true) {
1444
+ console.log('代理服务已经启动,跳过 LocalProxy.init() ');
1445
+ return;
1446
+ }
1447
+
1448
+ console.log('启动代理服务 LocalProxy.init() ');
1449
+ console.log('Dev server started, starting LocalProxy...');
1450
+ is_running_in_docker = _util.isRunningInDocker();
1451
+ if (is_running_in_docker) {
1452
+ try {
1453
+ docker_host_IP = _util.getDockerHostIP();
1454
+ } catch (e) {
1455
+ docker_host_IP = getLocalIp();
1456
+ }
1457
+ }
1458
+ await that.start(() => {});
1459
+ await that.updateDevices();
1460
+ console.log('local network devices updated!');
1461
+ await delay(1000);
1462
+ // restartProxyListener();
1463
+ // 设置定时任务,每两小时更新一次设备信息
1464
+ setInterval(async () => {
1465
+ try {
1466
+ await that.updateDevices();
1467
+ console.log('Network devices updated automatically every 2 hours');
1468
+ } catch (error) {
1469
+ console.error('Failed to automatically update network devices:', error);
1470
+ }
1471
+ await updateWanIp();
1472
+ }, 2 * 60 * 60 * 1000); // 2小时 = 2 * 60 * 60 * 1000 毫秒
1473
+
1474
+ // 设置定时任务,每2分钟清理一次超过 10 分钟未活动的攻击 IP
1475
+ setInterval(async () => {
1476
+ attacker.cleanupInactiveIPs();
1477
+ }, 2 * 60 * 1000);
1478
+
1479
+ anyproxy_started = true;
1480
+ }
1481
+ };
1482
+
1483
+ // 预编译 MITM Rule 的正则
1484
+ (function() {
1485
+ preCompileRuleRegexp();
1486
+ })();
1487
+
1488
+ module.exports = LocalProxy;