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
@@ -0,0 +1,72 @@
1
+ import json
2
+ import os
3
+ import tempfile
4
+ import pytest
5
+ import sys
6
+
7
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
8
+ from config import Config, DEFAULT_CONFIG
9
+
10
+
11
+ class TestConfig:
12
+ def setup_method(self):
13
+ self.tmp_dir = tempfile.mkdtemp()
14
+ self.config_path = os.path.join(self.tmp_dir, "config.json")
15
+ self.config = Config(config_path=self.config_path)
16
+
17
+ def test_default_config_created_on_first_load(self):
18
+ data = self.config.load()
19
+ assert data["server"]["port"] == 8002
20
+ assert data["server"]["tls"] is True
21
+ assert data["server"]["allowInsecure"] is True
22
+ assert data["local"]["socks_port"] == 1080
23
+ assert data["local"]["http_port"] == 1087
24
+ assert data["local"]["udp"] is True
25
+ assert data["mode"] == "global"
26
+ assert os.path.exists(self.config_path)
27
+
28
+ def test_save_and_load_roundtrip(self):
29
+ self.config.load()
30
+ self.config.data["server"]["address"] = "10.0.0.1"
31
+ self.config.data["server"]["port"] = 9002
32
+ self.config.data["server"]["username"] = "user1"
33
+ self.config.data["server"]["password"] = "pass1"
34
+ self.config.save()
35
+
36
+ config2 = Config(config_path=self.config_path)
37
+ data = config2.load()
38
+ assert data["server"]["address"] == "10.0.0.1"
39
+ assert data["server"]["port"] == 9002
40
+ assert data["server"]["username"] == "user1"
41
+ assert data["server"]["password"] == "pass1"
42
+
43
+ def test_load_existing_config(self):
44
+ existing = {
45
+ "server": {
46
+ "address": "example.com",
47
+ "port": 443,
48
+ "username": "abc",
49
+ "password": "def",
50
+ "tls": False,
51
+ "allowInsecure": False,
52
+ },
53
+ "local": {"socks_port": 2080, "http_port": 2087, "udp": False},
54
+ "mode": "manual",
55
+ }
56
+ with open(self.config_path, "w") as f:
57
+ json.dump(existing, f)
58
+
59
+ data = self.config.load()
60
+ assert data["server"]["address"] == "example.com"
61
+ assert data["server"]["tls"] is False
62
+ assert data["local"]["socks_port"] == 2080
63
+ assert data["mode"] == "manual"
64
+
65
+ def test_is_configured_false_when_no_address(self):
66
+ self.config.load()
67
+ assert self.config.is_configured() is False
68
+
69
+ def test_is_configured_true_when_address_set(self):
70
+ self.config.load()
71
+ self.config.data["server"]["address"] = "10.0.0.1"
72
+ assert self.config.is_configured() is True
@@ -0,0 +1,69 @@
1
+ import pytest
2
+ import sys
3
+ import os
4
+ from unittest.mock import patch, call, MagicMock
5
+
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
7
+ from system_proxy import SystemProxy
8
+
9
+
10
+ class TestSystemProxy:
11
+ def setup_method(self):
12
+ self.proxy = SystemProxy()
13
+
14
+ @patch("system_proxy.SystemProxy._get_active_interfaces")
15
+ @patch("system_proxy.subprocess.run")
16
+ def test_enable_sets_all_proxy_types(self, mock_run, mock_interfaces):
17
+ mock_interfaces.return_value = ["Wi-Fi"]
18
+ mock_run.return_value = MagicMock(returncode=0, stdout="Enabled: Yes\n", stderr="")
19
+
20
+ self.proxy.enable(socks_port=1080, http_port=1087)
21
+
22
+ expected_calls = [
23
+ call(["networksetup", "-setsocksfirewallproxy", "Wi-Fi", "127.0.0.1", "1080"], capture_output=True, text=True),
24
+ call(["networksetup", "-setsocksfirewallproxystate", "Wi-Fi", "on"], capture_output=True, text=True),
25
+ call(["networksetup", "-setwebproxy", "Wi-Fi", "127.0.0.1", "1087"], capture_output=True, text=True),
26
+ call(["networksetup", "-setwebproxystate", "Wi-Fi", "on"], capture_output=True, text=True),
27
+ call(["networksetup", "-setsecurewebproxy", "Wi-Fi", "127.0.0.1", "1087"], capture_output=True, text=True),
28
+ call(["networksetup", "-setsecurewebproxystate", "Wi-Fi", "on"], capture_output=True, text=True),
29
+ ]
30
+ mock_run.assert_has_calls(expected_calls, any_order=True)
31
+
32
+ @patch("system_proxy.SystemProxy._get_active_interfaces")
33
+ @patch("system_proxy.subprocess.run")
34
+ def test_disable_clears_all_proxy_types(self, mock_run, mock_interfaces):
35
+ mock_interfaces.return_value = ["Wi-Fi"]
36
+ mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
37
+
38
+ self.proxy.disable()
39
+
40
+ expected_calls = [
41
+ call(["networksetup", "-setsocksfirewallproxystate", "Wi-Fi", "off"], capture_output=True, text=True),
42
+ call(["networksetup", "-setwebproxystate", "Wi-Fi", "off"], capture_output=True, text=True),
43
+ call(["networksetup", "-setsecurewebproxystate", "Wi-Fi", "off"], capture_output=True, text=True),
44
+ ]
45
+ mock_run.assert_has_calls(expected_calls, any_order=True)
46
+
47
+ @patch("system_proxy.SystemProxy._get_active_interfaces")
48
+ @patch("system_proxy.subprocess.run")
49
+ def test_enable_multiple_interfaces(self, mock_run, mock_interfaces):
50
+ mock_interfaces.return_value = ["Wi-Fi", "Ethernet"]
51
+ mock_run.return_value = MagicMock(returncode=0, stdout="Enabled: Yes\n", stderr="")
52
+
53
+ self.proxy.enable(socks_port=1080, http_port=1087)
54
+
55
+ # 6 set calls per interface x 2 + 3 verify calls per interface x 2 = 18
56
+ assert mock_run.call_count == 18
57
+
58
+ @patch("system_proxy.subprocess.run")
59
+ def test_get_active_interfaces(self, mock_run):
60
+ mock_run.return_value = MagicMock(
61
+ stdout="An asterisk (*) denotes that a network service is disabled.\nWi-Fi\n*Bluetooth PAN\nEthernet\n",
62
+ returncode=0,
63
+ )
64
+
65
+ interfaces = self.proxy._get_active_interfaces()
66
+
67
+ assert "Wi-Fi" in interfaces
68
+ assert "Ethernet" in interfaces
69
+ assert "Bluetooth PAN" not in interfaces
@@ -0,0 +1,31 @@
1
+ const { execSync } = require("child_process");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+
5
+ const ICONS_DIR = path.join(__dirname, "icons");
6
+ const SOURCES = ["socks_on_G.png", "socks_on_M.png"];
7
+
8
+ function resize(source) {
9
+ const name = path.basename(source, ".png");
10
+ const dest = path.join(ICONS_DIR, `${name}_bar.png`);
11
+ execSync(`sips -z 40 39 "${source}" --out "${dest}"`, { stdio: "ignore" });
12
+ console.log(`${new Date().toLocaleTimeString()} regenerated ${name}_bar.png`);
13
+ }
14
+
15
+ function regenAll() {
16
+ SOURCES.forEach((f) => resize(path.join(ICONS_DIR, f)));
17
+ }
18
+
19
+ const cmd = process.argv[2];
20
+ if (cmd === "--watch") {
21
+ console.log("Watching icons for changes...");
22
+ regenAll();
23
+ SOURCES.forEach((f) => {
24
+ const filePath = path.join(ICONS_DIR, f);
25
+ fs.watch(filePath, (eventType) => {
26
+ if (eventType === "change") resize(filePath);
27
+ });
28
+ });
29
+ } else {
30
+ regenAll();
31
+ }
package/config.json CHANGED
@@ -1,202 +1,7 @@
1
1
  {
2
- "network_scanning_status": "1",
3
- "progress_time_stamp": "1766571511705",
4
- "block_hosts": [
5
- {
6
- "filter_host": "bilibili.com",
7
- "filter_match_rule": "",
8
- "filter_start_time": "00:00",
9
- "filter_end_time": "23:59",
10
- "filter_weekday": [
11
- 1,
12
- 2,
13
- 3,
14
- 4,
15
- 5,
16
- 6,
17
- 7
18
- ],
19
- "filter_mac": "6E:FB:18:4D:9C:3E"
20
- },
21
- {
22
- "filter_host": "doubao.com",
23
- "filter_match_rule": "",
24
- "filter_start_time": "00:00",
25
- "filter_end_time": "23:59",
26
- "filter_weekday": [
27
- 1,
28
- 2,
29
- 3,
30
- 4,
31
- 5,
32
- 6,
33
- 7
34
- ],
35
- "filter_mac": "6E:FB:18:4D:9C:3E"
36
- },
37
- {
38
- "filter_host": "dburl.net",
39
- "filter_match_rule": "",
40
- "filter_start_time": "00:00",
41
- "filter_end_time": "23:59",
42
- "filter_weekday": [
43
- 1,
44
- 2,
45
- 3,
46
- 4,
47
- 5,
48
- 6,
49
- 7
50
- ],
51
- "filter_mac": "6E:FB:18:4D:9C:3E"
52
- },
53
- {
54
- "filter_host": "baidu.com",
55
- "filter_match_rule": "",
56
- "filter_start_time": "00:00",
57
- "filter_end_time": "23:59",
58
- "filter_weekday": [
59
- 1,
60
- 2,
61
- 3,
62
- 4,
63
- 5,
64
- 6,
65
- 7
66
- ],
67
- "filter_mac": "6E:FB:18:4D:9C:3E"
68
- },
69
- {
70
- "filter_host": "youtube.com",
71
- "filter_match_rule": "^https?:\\/\\/(www|s)\\.youtube\\.com\\/(pagead|ptracking)",
72
- "filter_start_time": "00:00",
73
- "filter_end_time": "23:59",
74
- "filter_weekday": [
75
- 1,
76
- 2,
77
- 3,
78
- 4,
79
- 5,
80
- 6,
81
- 7
82
- ],
83
- "compiledFilterRegexp": {}
84
- },
85
- {
86
- "filter_host": "youtube.com",
87
- "filter_match_rule": "^https?:\\/\\/s\\.youtube\\.com\\/api\\/stats\\/qoe\\?adcontext",
88
- "filter_start_time": "00:00",
89
- "filter_end_time": "23:59",
90
- "filter_weekday": [
91
- 1,
92
- 2,
93
- 3,
94
- 4,
95
- 5,
96
- 6,
97
- 7
98
- ],
99
- "compiledFilterRegexp": {}
100
- },
101
- {
102
- "filter_host": "youtube.com",
103
- "filter_match_rule": "^https?:\\/\\/(www|s)\\.youtube\\.com\\/api\\/stats\\/ads",
104
- "filter_start_time": "00:00",
105
- "filter_end_time": "23:59",
106
- "filter_weekday": [
107
- 1,
108
- 2,
109
- 3,
110
- 4,
111
- 5,
112
- 6,
113
- 7
114
- ],
115
- "compiledFilterRegexp": {}
116
- },
117
- {
118
- "filter_host": "googlevideo.com",
119
- "filter_match_rule": "^https?:\\/\\/[\\w-]+\\.googlevideo\\.com\\/(?!(dclk_video_ads|videoplayback\\?)).+&oad",
120
- "filter_start_time": "00:00",
121
- "filter_end_time": "23:59",
122
- "filter_weekday": [
123
- 1,
124
- 2,
125
- 3,
126
- 4,
127
- 5,
128
- 6,
129
- 7
130
- ],
131
- "compiledFilterRegexp": {}
132
- }
133
- ],
2
+ "block_hosts": [],
134
3
  "proxy_port": 8001,
135
- "web_interface_port": 8003,
136
- "your_domain": "yui.cool",
137
- "vpn_proxy": "",
138
- "auth_username": "lijing",
139
- "auth_password": "lijing",
140
- "enable_express": "1",
141
- "enable_socks5": "1",
142
4
  "socks5_port": 8002,
143
- "enable_webinterface": "1",
144
- "devices": [
145
- {
146
- "ip": "192.168.124.1",
147
- "mac": "FA:27:3C:E5:31:5F"
148
- },
149
- {
150
- "ip": "192.168.124.2",
151
- "mac": "7C:DE:78:A9:83:A0"
152
- },
153
- {
154
- "ip": "192.168.124.6",
155
- "mac": "0:DD:B6:EB:26:5C"
156
- },
157
- {
158
- "ip": "192.168.124.10",
159
- "mac": "FA:27:3C:E5:31:5F"
160
- },
161
- {
162
- "ip": "192.168.124.34",
163
- "mac": "D6:A0:61:69:67:F6"
164
- },
165
- {
166
- "ip": "192.168.124.37",
167
- "mac": "DE:1E:87:ED:17:8B"
168
- },
169
- {
170
- "ip": "192.168.124.49",
171
- "mac": "62:A1:CC:42:55:E4"
172
- },
173
- {
174
- "ip": "192.168.124.107",
175
- "mac": "6:26:EA:5A:9E:6C"
176
- },
177
- {
178
- "ip": "192.168.124.111",
179
- "mac": "74:3F:C2:67:74:98"
180
- },
181
- {
182
- "ip": "192.168.124.125",
183
- "mac": "14:C0:50:14:6E:A5"
184
- },
185
- {
186
- "ip": "192.168.124.128",
187
- "mac": "48:F3:F3:CA:1D:E"
188
- },
189
- {
190
- "ip": "192.168.124.200",
191
- "mac": "54:52:84:95:DF:5E"
192
- },
193
- {
194
- "ip": "192.168.124.240",
195
- "mac": "F4:6B:8C:90:29:5"
196
- },
197
- {
198
- "ip": "192.168.124.251",
199
- "mac": "6E:FB:18:4D:9C:3E"
200
- }
201
- ]
202
- }
5
+ "auth_username": "admin",
6
+ "auth_password": "admin"
7
+ }