block-proxy 0.1.12 → 0.1.14

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 (58) hide show
  1. package/.claude/settings.local.json +33 -1
  2. package/.claude/skills/build-client/skill.md +24 -0
  3. package/.claude/skills/release-client/skill.md +68 -0
  4. package/CLAUDE.md +69 -67
  5. package/Dockerfile +1 -1
  6. package/README.md +38 -24
  7. package/build/asset-manifest.json +6 -6
  8. package/build/index.html +1 -1
  9. package/build/static/css/main.3f317ce6.css +2 -0
  10. package/build/static/css/main.3f317ce6.css.map +1 -0
  11. package/build/static/js/{main.2247fb80.js → main.af1923ea.js} +3 -3
  12. package/build/static/js/main.af1923ea.js.map +1 -0
  13. package/client/app.py +312 -0
  14. package/client/build.sh +84 -0
  15. package/client/config.py +49 -0
  16. package/client/config_window.py +155 -0
  17. package/client/icons/app.icns +0 -0
  18. package/client/icons/app_example.png +0 -0
  19. package/client/icons/app_icon.png +0 -0
  20. package/client/icons/backup/app_example.png +0 -0
  21. package/client/icons/backup/christmas-sock_dark.png +0 -0
  22. package/client/icons/backup/christmas-sock_light.png +0 -0
  23. package/client/icons/backup/socks_on_G.png +0 -0
  24. package/client/icons/backup/socks_on_M.png +0 -0
  25. package/client/icons/christmas-sock_dark.png +0 -0
  26. package/client/icons/christmas-sock_light.png +0 -0
  27. package/client/icons/christmas-sock_light_bar.png +0 -0
  28. package/client/icons/socks_on_G.png +0 -0
  29. package/client/icons/socks_on_G_bar.png +0 -0
  30. package/client/icons/socks_on_M.png +0 -0
  31. package/client/icons/socks_on_M_bar.png +0 -0
  32. package/client/main.py +28 -0
  33. package/client/proxy_core.py +475 -0
  34. package/client/requirements.txt +3 -0
  35. package/client/scripts/download_xray.sh +30 -0
  36. package/client/setup.py +30 -0
  37. package/client/system_proxy.py +94 -0
  38. package/client/tests/__init__.py +0 -0
  39. package/client/tests/test_config.py +72 -0
  40. package/client/tests/test_system_proxy.py +69 -0
  41. package/client/watch-icons.js +31 -0
  42. package/config.json +4 -199
  43. package/docs/superpowers/plans/2026-05-27-blockproxyclient.md +1274 -0
  44. package/docs/superpowers/specs/2026-05-27-blockproxyclient-design.md +264 -0
  45. package/package.json +11 -5
  46. package/proxy/proxy.js +19 -34
  47. package/server/express.js +17 -1
  48. package/src/App.css +596 -276
  49. package/src/App.js +25 -32
  50. package/src/index.css +3 -4
  51. package/test/lib/mock-server.js +133 -0
  52. package/test/proxy-tests.js +708 -0
  53. package/test/run.js +330 -0
  54. package/build/static/css/main.8bfa3d5f.css +0 -2
  55. package/build/static/css/main.8bfa3d5f.css.map +0 -1
  56. package/build/static/js/main.2247fb80.js.map +0 -1
  57. package/hack-of-anyproxy/lib/requestHandler.js +0 -1060
  58. /package/build/static/js/{main.2247fb80.js.LICENSE.txt → main.af1923ea.js.LICENSE.txt} +0 -0
package/src/App.js CHANGED
@@ -20,7 +20,6 @@ function App() {
20
20
  block_hosts: [],
21
21
  proxy_port: 8001,
22
22
  socks5_port:8002,
23
- web_interface_port: 8003,
24
23
  auth_username: "",
25
24
  auth_password: "",
26
25
  });
@@ -78,7 +77,7 @@ function App() {
78
77
  } else if (data.ips.length >= 1) {
79
78
  localIp = data.ips[0].address;
80
79
  }
81
- QRCode.toCanvas(document.getElementById('qrcode'), `http://${localIp}:${config.web_interface_port}/fetchCrtFile`);
80
+ QRCode.toCanvas(document.getElementById('qrcode'), `http://${localIp}:8003/fetchCrtFile`);
82
81
  } else {
83
82
  showToast('获取服务器IP失败', 'error');
84
83
  }
@@ -580,15 +579,6 @@ function App() {
580
579
  onChange={(e) => setConfig({...config, socks5_port: parseInt(e.target.value) || 8002})}
581
580
  />
582
581
  </div>
583
-
584
- <div className="setting-row">
585
- <label>Anyproxy 监控端口:</label>
586
- <input
587
- type="number"
588
- value={config.web_interface_port}
589
- onChange={(e) => setConfig({...config, web_interface_port: parseInt(e.target.value) || 8003})}
590
- />
591
- </div>
592
582
 
593
583
  <div className="setting-row">
594
584
  <label>代理用户名:</label>
@@ -624,7 +614,17 @@ function App() {
624
614
  value={config.vpn_proxy}
625
615
  onChange={(e) => setConfig({...config, vpn_proxy: e.target.value || ""})}
626
616
  />
627
- <div className="help-text">(格式:“127.0.0.1:1087”,仅调试用)</div>
617
+ <div className="help-text">(格式:"127.0.0.1:1087",仅调试用)</div>
618
+ </div>
619
+ <div className="setting-row">
620
+ <label>HTTPS MITM 解密:</label>
621
+ <select
622
+ value={config.enable_mitm || "1"}
623
+ onChange={(e) => setConfig({...config, enable_mitm: e.target.value})}
624
+ >
625
+ <option value="1">开启(需安装证书,支持 URL 路径过滤 + 广告重写)</option>
626
+ <option value="0">关闭(纯隧道转发,不拦截,零证书错误)</option>
627
+ </select>
628
628
  </div>
629
629
  <div className="setting-row actions">
630
630
  <button
@@ -635,11 +635,10 @@ function App() {
635
635
  {loading ? '保存中...' : '保存配置'}
636
636
  </button>
637
637
 
638
- <button
639
- onClick={handleUpdateDevices}
638
+ <button
639
+ onClick={handleUpdateDevices}
640
640
  disabled={loading}
641
- className="restart-btn"
642
- style={{ backgroundColor: '#17a2b8', color: 'white' }}
641
+ className="refresh-btn"
643
642
  >
644
643
  {loading ? '刷新中...' : '刷新路由表'}
645
644
  </button>
@@ -680,6 +679,12 @@ function App() {
680
679
  config.enable_socks5 === "1" ? "开启" : "关闭"
681
680
  }</span>
682
681
  </p>
682
+ <p>
683
+ <b>HTTPS MITM 解密:</b>
684
+ <span>{
685
+ (config.enable_mitm || "1") === "1" ? "开启(需安装证书,支持 URL 路径过滤 + 广告重写)" : "关闭(纯隧道转发,不拦截)"
686
+ }</span>
687
+ </p>
683
688
  <p>
684
689
  <b>用户名</b>:{config.auth_username === "" ? (
685
690
  <span>无</span>
@@ -694,22 +699,10 @@ function App() {
694
699
  config.auth_password
695
700
  )}
696
701
  </p>
697
- <p>
698
- <b>AnyProxy 监控地址:</b>
699
- {serverIPs.length > 0 ? (
700
- <span>
701
- <a href={`http://${getIpAddress()}:${config.web_interface_port}`} target="_blank" rel="noopener noreferrer">
702
- {`http://${getIpAddress()}:${config.web_interface_port}`}
703
- </a>
704
- </span>
705
- ) : (
706
- <span>正在获取服务器IP地址...</span>
707
- )}
708
- </p>
709
702
  <p>
710
703
  <span>
711
704
  扫码安装证书:
712
- <a href={`http://${getIpAddress()}:${config.web_interface_port}/fetchCrtFile`} target="_blank">下载</a>
705
+ <a href={`http://${getIpAddress()}:8003/fetchCrtFile`} target="_blank">下载</a>
713
706
  </span><br />
714
707
  <canvas id="qrcode"></canvas>
715
708
  </p>
@@ -750,10 +743,10 @@ function App() {
750
743
  ) : (
751
744
  <p>暂无设备信息</p>
752
745
  )}
753
- <button
754
- onClick={fetchConfig}
746
+ <button
747
+ onClick={fetchConfig}
755
748
  disabled={loading}
756
- style={{ marginTop: '10px' }}
749
+ className="refresh-table-btn"
757
750
  >
758
751
  {loading ? '刷新中...' : '更新路由表'}
759
752
  </button>
package/src/index.css CHANGED
@@ -1,13 +1,12 @@
1
1
  body {
2
2
  margin: 0;
3
3
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5
- sans-serif;
4
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
6
5
  -webkit-font-smoothing: antialiased;
7
6
  -moz-osx-font-smoothing: grayscale;
7
+ background: #f8fafc;
8
8
  }
9
9
 
10
10
  code {
11
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12
- monospace;
11
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
13
12
  }
@@ -0,0 +1,133 @@
1
+ const http = require('http');
2
+
3
+ function getLanIp() {
4
+ const os = require('os');
5
+ const interfaces = os.networkInterfaces();
6
+ for (const name of Object.keys(interfaces)) {
7
+ for (const iface of interfaces[name]) {
8
+ if (iface.family === 'IPv4' && !iface.internal) {
9
+ return iface.address;
10
+ }
11
+ }
12
+ }
13
+ return '127.0.0.1';
14
+ }
15
+
16
+ class MockServer {
17
+ constructor() {
18
+ this.server = null;
19
+ this.port = 0;
20
+ this.host = '0.0.0.0';
21
+ this.lanIp = getLanIp();
22
+ }
23
+
24
+ get baseUrl() {
25
+ return `http://${this.lanIp}:${this.port}`;
26
+ }
27
+
28
+ async start() {
29
+ return new Promise((resolve, reject) => {
30
+ this.server = http.createServer((req, res) => {
31
+ try {
32
+ this._handle(req, res);
33
+ } catch (_err) {
34
+ if (!res.headersSent) {
35
+ res.writeHead(500);
36
+ res.end('Internal Server Error');
37
+ }
38
+ }
39
+ });
40
+
41
+ this.server.on('error', reject);
42
+ this.server.listen(0, this.host, () => {
43
+ this.port = this.server.address().port;
44
+ resolve(this.port);
45
+ });
46
+ });
47
+ }
48
+
49
+ _handle(req, res) {
50
+ const url = new URL(req.url, `http://${this.host}`);
51
+ const path = url.pathname;
52
+
53
+ if (path === '/ping') {
54
+ res.writeHead(200, { 'Content-Type': 'text/plain', 'Content-Length': '4' });
55
+ res.end('pong');
56
+ return;
57
+ }
58
+
59
+ if (path.startsWith('/size/')) {
60
+ const bytes = parseInt(path.split('/')[2], 10);
61
+ if (isNaN(bytes) || bytes < 0 || bytes > 100 * 1024 * 1024) {
62
+ res.writeHead(400);
63
+ res.end('Invalid size (0-104857600)');
64
+ return;
65
+ }
66
+ const buf = Buffer.alloc(bytes, 0x58);
67
+ res.writeHead(200, {
68
+ 'Content-Type': 'application/octet-stream',
69
+ 'Content-Length': String(bytes),
70
+ });
71
+ res.end(buf);
72
+ return;
73
+ }
74
+
75
+ if (path.startsWith('/delay/')) {
76
+ const ms = parseInt(path.split('/')[2], 10);
77
+ if (isNaN(ms) || ms < 0 || ms > 30000) {
78
+ res.writeHead(400);
79
+ res.end('Invalid delay (0-30000)');
80
+ return;
81
+ }
82
+ setTimeout(() => {
83
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
84
+ res.end(`delayed ${ms}ms`);
85
+ }, ms);
86
+ return;
87
+ }
88
+
89
+ if (path.startsWith('/status/')) {
90
+ const code = parseInt(path.split('/')[2], 10) || 200;
91
+ res.writeHead(code);
92
+ res.end(`Status: ${code}`);
93
+ return;
94
+ }
95
+
96
+ if (path === '/echo' && req.method === 'POST') {
97
+ const body = [];
98
+ req.on('data', (c) => body.push(c));
99
+ req.on('end', () => {
100
+ const buf = Buffer.concat(body);
101
+ res.writeHead(200, { 'Content-Type': 'application/json' });
102
+ res.end(JSON.stringify({
103
+ method: req.method,
104
+ url: req.url,
105
+ headers: req.headers,
106
+ bodyLength: buf.length,
107
+ }));
108
+ });
109
+ return;
110
+ }
111
+
112
+ // default: dump request info
113
+ res.writeHead(200, { 'Content-Type': 'application/json' });
114
+ res.end(JSON.stringify({
115
+ method: req.method,
116
+ url: req.url,
117
+ path,
118
+ headers: req.headers,
119
+ }));
120
+ }
121
+
122
+ async stop() {
123
+ return new Promise((resolve) => {
124
+ if (this.server) {
125
+ this.server.close(() => resolve());
126
+ } else {
127
+ resolve();
128
+ }
129
+ });
130
+ }
131
+ }
132
+
133
+ module.exports = MockServer;