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.
- package/.agents/skills/commit/skill.md +40 -0
- package/.claude/settings.local.json +29 -1
- package/.claude/skills/build-client/skill.md +24 -0
- package/.claude/skills/commit/skill.md +34 -26
- package/.claude/skills/release-client/skill.md +68 -0
- package/CLAUDE.md +109 -47
- package/Dockerfile +1 -1
- package/README.md +69 -60
- package/build/asset-manifest.json +6 -6
- package/build/index.html +1 -1
- package/build/static/css/main.3f317ce6.css +2 -0
- package/build/static/css/main.3f317ce6.css.map +1 -0
- package/build/static/js/{main.2247fb80.js → main.68f66be0.js} +3 -3
- package/build/static/js/main.68f66be0.js.map +1 -0
- package/client/app.py +312 -0
- package/client/build.sh +84 -0
- package/client/config.py +49 -0
- package/client/config_window.py +155 -0
- package/client/icons/app.icns +0 -0
- package/client/icons/app_example.png +0 -0
- package/client/icons/app_icon.png +0 -0
- package/client/icons/backup/app_example.png +0 -0
- package/client/icons/backup/christmas-sock_dark.png +0 -0
- package/client/icons/backup/christmas-sock_light.png +0 -0
- package/client/icons/backup/socks_on_G.png +0 -0
- package/client/icons/backup/socks_on_M.png +0 -0
- package/client/icons/christmas-sock_dark.png +0 -0
- package/client/icons/christmas-sock_light.png +0 -0
- package/client/icons/christmas-sock_light_bar.png +0 -0
- package/client/icons/socks_on_G.png +0 -0
- package/client/icons/socks_on_G_bar.png +0 -0
- package/client/icons/socks_on_M.png +0 -0
- package/client/icons/socks_on_M_bar.png +0 -0
- package/client/main.py +28 -0
- package/client/proxy_core.py +475 -0
- package/client/requirements.txt +3 -0
- package/client/scripts/download_xray.sh +30 -0
- package/client/setup.py +30 -0
- package/client/system_proxy.py +94 -0
- package/client/tests/__init__.py +0 -0
- package/client/tests/test_config.py +72 -0
- package/client/tests/test_system_proxy.py +69 -0
- package/client/watch-icons.js +31 -0
- package/config.json +82 -5
- package/docs/superpowers/plans/2026-05-27-blockproxyclient.md +1274 -0
- package/docs/superpowers/specs/2026-05-27-blockproxyclient-design.md +264 -0
- package/package.json +11 -5
- package/proxy/proxy.js +70 -18
- package/server/express.js +17 -1
- package/skills-lock.json +11 -0
- package/socks5/server.js +2 -2
- package/src/App.css +596 -276
- package/src/App.js +25 -22
- package/src/index.css +3 -4
- package/test/lib/mock-server.js +133 -0
- package/test/proxy-tests.js +708 -0
- package/test/run.js +330 -0
- package/build/static/css/main.8bfa3d5f.css +0 -2
- package/build/static/css/main.8bfa3d5f.css.map +0 -1
- package/build/static/js/main.2247fb80.js.map +0 -1
- package/hack-of-anyproxy/lib/requestHandler.js +0 -1060
- /package/build/static/js/{main.2247fb80.js.LICENSE.txt → main.68f66be0.js.LICENSE.txt} +0 -0
package/client/app.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import subprocess
|
|
4
|
+
import threading
|
|
5
|
+
import platform
|
|
6
|
+
import rumps
|
|
7
|
+
from PyObjCTools import AppHelper
|
|
8
|
+
from Foundation import NSObject
|
|
9
|
+
from config import Config
|
|
10
|
+
from proxy_core import ProxyCore
|
|
11
|
+
from system_proxy import SystemProxy
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _is_tahoe_or_newer():
|
|
15
|
+
try:
|
|
16
|
+
ver = tuple(map(int, platform.mac_ver()[0].split(".")))
|
|
17
|
+
return ver >= (26, 0)
|
|
18
|
+
except Exception:
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _MenuOpenDelegate(NSObject):
|
|
23
|
+
def menuWillOpen_(self, menu):
|
|
24
|
+
cb = getattr(self, "_on_open", None)
|
|
25
|
+
if cb:
|
|
26
|
+
cb()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SocksClient(rumps.App):
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super().__init__("SocksClient", quit_button=None)
|
|
32
|
+
self.template = _is_tahoe_or_newer()
|
|
33
|
+
|
|
34
|
+
self.config = Config()
|
|
35
|
+
self.config.load()
|
|
36
|
+
self.proxy = ProxyCore()
|
|
37
|
+
self.sys_proxy = SystemProxy()
|
|
38
|
+
self.connected = False
|
|
39
|
+
|
|
40
|
+
self._measuring = False
|
|
41
|
+
self._build_menu()
|
|
42
|
+
self._setup_menu_delegate()
|
|
43
|
+
self._update_icon()
|
|
44
|
+
self._start_health_check()
|
|
45
|
+
|
|
46
|
+
def run(self, **options):
|
|
47
|
+
if not _is_tahoe_or_newer():
|
|
48
|
+
def _fix_highlight():
|
|
49
|
+
try:
|
|
50
|
+
self._nsapp.nsstatusitem.setHighlightMode_(False)
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
AppHelper.callAfter(_fix_highlight)
|
|
54
|
+
super().run(**options)
|
|
55
|
+
|
|
56
|
+
def _build_menu(self):
|
|
57
|
+
self.toggle_item = rumps.MenuItem("启动代理", callback=self.toggle_proxy)
|
|
58
|
+
self.config_item = rumps.MenuItem("Socks 节点配置...", callback=self.open_config)
|
|
59
|
+
|
|
60
|
+
self.global_item = rumps.MenuItem("全局代理(设置系统代理)", callback=self.set_global_mode)
|
|
61
|
+
self.manual_item = rumps.MenuItem("手动模式(关闭系统代理)", callback=self.set_manual_mode)
|
|
62
|
+
|
|
63
|
+
self.about_item = rumps.MenuItem("关于", callback=self.show_about)
|
|
64
|
+
self.quit_item = rumps.MenuItem("退出", callback=self.quit_app)
|
|
65
|
+
|
|
66
|
+
self.menu = [
|
|
67
|
+
self.toggle_item,
|
|
68
|
+
self.config_item,
|
|
69
|
+
None,
|
|
70
|
+
self.global_item,
|
|
71
|
+
self.manual_item,
|
|
72
|
+
None,
|
|
73
|
+
self.about_item,
|
|
74
|
+
None,
|
|
75
|
+
self.quit_item,
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
from AppKit import NSCommandKeyMask
|
|
80
|
+
self.quit_item._menuitem.setKeyEquivalent_("q")
|
|
81
|
+
self.quit_item._menuitem.setKeyEquivalentModifierMask_(NSCommandKeyMask)
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
self._update_mode_menu()
|
|
86
|
+
|
|
87
|
+
def _update_mode_menu(self):
|
|
88
|
+
is_global = self.config.data["mode"] == "global"
|
|
89
|
+
self.global_item.state = 1 if is_global else 0
|
|
90
|
+
self.manual_item.state = 1 if not is_global else 0
|
|
91
|
+
|
|
92
|
+
def _update_icon(self):
|
|
93
|
+
if self.connected:
|
|
94
|
+
icon_name = "socks_on_G_bar.png" if self.config.data["mode"] == "global" else "socks_on_M_bar.png"
|
|
95
|
+
else:
|
|
96
|
+
icon_name = "christmas-sock_light_bar.png"
|
|
97
|
+
icon_path = os.path.join(self._icon_dir(), icon_name)
|
|
98
|
+
if os.path.exists(icon_path):
|
|
99
|
+
self.icon = icon_path
|
|
100
|
+
self.title = None
|
|
101
|
+
|
|
102
|
+
def _is_compiled(self):
|
|
103
|
+
return "__compiled__" in globals() or getattr(sys, "frozen", False)
|
|
104
|
+
|
|
105
|
+
def _bundle_resource_dir(self):
|
|
106
|
+
if self._is_compiled():
|
|
107
|
+
return os.path.dirname(sys.executable)
|
|
108
|
+
return os.path.dirname(os.path.abspath(__file__))
|
|
109
|
+
|
|
110
|
+
def _icon_dir(self):
|
|
111
|
+
return os.path.join(self._bundle_resource_dir(), "icons")
|
|
112
|
+
|
|
113
|
+
def toggle_proxy(self, sender):
|
|
114
|
+
if self.connected:
|
|
115
|
+
self._disconnect()
|
|
116
|
+
else:
|
|
117
|
+
self._connect()
|
|
118
|
+
|
|
119
|
+
def _connect(self):
|
|
120
|
+
if not self.config.is_configured():
|
|
121
|
+
rumps.alert("请先配置节点信息")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
def _start():
|
|
125
|
+
try:
|
|
126
|
+
self.proxy.start(self.config.data)
|
|
127
|
+
except OSError as e:
|
|
128
|
+
if e.errno == 48:
|
|
129
|
+
rumps.notification(
|
|
130
|
+
"SocksClient", "启动失败",
|
|
131
|
+
f"端口被占用,请检查端口是否已被其他程序使用",
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
rumps.notification(
|
|
135
|
+
"SocksClient", "启动失败", str(e),
|
|
136
|
+
)
|
|
137
|
+
return
|
|
138
|
+
if self.config.data["mode"] == "global":
|
|
139
|
+
try:
|
|
140
|
+
self.sys_proxy.enable(
|
|
141
|
+
socks_port=self.config.data["local"]["socks_port"],
|
|
142
|
+
http_port=self.config.data["local"]["http_port"],
|
|
143
|
+
)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
rumps.notification(
|
|
146
|
+
"SocksClient", "系统代理设置失败", str(e),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _update_ui():
|
|
150
|
+
self.connected = True
|
|
151
|
+
self.toggle_item.title = "关闭代理"
|
|
152
|
+
self._update_icon()
|
|
153
|
+
|
|
154
|
+
AppHelper.callAfter(_update_ui)
|
|
155
|
+
|
|
156
|
+
threading.Thread(target=_start, daemon=True).start()
|
|
157
|
+
|
|
158
|
+
def _disconnect(self):
|
|
159
|
+
self.sys_proxy.disable()
|
|
160
|
+
self.proxy.stop()
|
|
161
|
+
self.connected = False
|
|
162
|
+
self.toggle_item.title = "启动代理"
|
|
163
|
+
self._update_icon()
|
|
164
|
+
|
|
165
|
+
def open_config(self, sender):
|
|
166
|
+
self._show_config_window()
|
|
167
|
+
|
|
168
|
+
def _find_python(self):
|
|
169
|
+
for p in [
|
|
170
|
+
"/Library/Frameworks/Python.framework/Versions/3.13/bin/python3",
|
|
171
|
+
"/usr/local/bin/python3",
|
|
172
|
+
"/usr/bin/python3",
|
|
173
|
+
]:
|
|
174
|
+
if os.path.exists(p):
|
|
175
|
+
return p
|
|
176
|
+
return "python3"
|
|
177
|
+
|
|
178
|
+
def _show_config_window(self):
|
|
179
|
+
self.config.save()
|
|
180
|
+
script_path = os.path.join(self._bundle_resource_dir(), "config_window.py")
|
|
181
|
+
python_path = self._find_python() if self._is_compiled() else sys.executable
|
|
182
|
+
proc = subprocess.Popen([python_path, script_path, self.config.config_path])
|
|
183
|
+
|
|
184
|
+
def _reload_after_window():
|
|
185
|
+
proc.wait()
|
|
186
|
+
old_data = self.config.data.copy()
|
|
187
|
+
self.config.load()
|
|
188
|
+
if self.connected and self.config.data != old_data:
|
|
189
|
+
self._disconnect()
|
|
190
|
+
self._connect()
|
|
191
|
+
|
|
192
|
+
threading.Thread(target=_reload_after_window, daemon=True).start()
|
|
193
|
+
|
|
194
|
+
def set_global_mode(self, sender):
|
|
195
|
+
self.config.data["mode"] = "global"
|
|
196
|
+
self.config.save()
|
|
197
|
+
self._update_mode_menu()
|
|
198
|
+
self._update_icon()
|
|
199
|
+
if self.connected:
|
|
200
|
+
self.sys_proxy.enable(
|
|
201
|
+
socks_port=self.config.data["local"]["socks_port"],
|
|
202
|
+
http_port=self.config.data["local"]["http_port"],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def set_manual_mode(self, sender):
|
|
206
|
+
self.config.data["mode"] = "manual"
|
|
207
|
+
self.config.save()
|
|
208
|
+
self._update_mode_menu()
|
|
209
|
+
self._update_icon()
|
|
210
|
+
if self.connected:
|
|
211
|
+
self.sys_proxy.disable()
|
|
212
|
+
|
|
213
|
+
def show_about(self, sender):
|
|
214
|
+
try:
|
|
215
|
+
from AppKit import (
|
|
216
|
+
NSAlert, NSTextField, NSMutableAttributedString,
|
|
217
|
+
NSAttributedString, NSFont, NSColor, NSMakeRect,
|
|
218
|
+
NSApp,
|
|
219
|
+
)
|
|
220
|
+
from Foundation import NSURL, NSRange
|
|
221
|
+
|
|
222
|
+
NSApp.activateIgnoringOtherApps_(True)
|
|
223
|
+
alert = NSAlert.alloc().init()
|
|
224
|
+
alert.setMessageText_("关于 SocksClient")
|
|
225
|
+
alert.addButtonWithTitle_("好")
|
|
226
|
+
|
|
227
|
+
url = "https://github.com/jayli/block-proxy"
|
|
228
|
+
text = f"项目:block-proxy\n作者:lijing00333\n地址:{url}\n版本:v0.1.0"
|
|
229
|
+
|
|
230
|
+
attr_str = NSMutableAttributedString.alloc().initWithString_(text)
|
|
231
|
+
full_range = NSRange(0, len(text))
|
|
232
|
+
font = NSFont.systemFontOfSize_(13)
|
|
233
|
+
attr_str.addAttribute_value_range_("NSFont", font, full_range)
|
|
234
|
+
|
|
235
|
+
link_start = text.index(url)
|
|
236
|
+
link_range = NSRange(link_start, len(url))
|
|
237
|
+
attr_str.addAttribute_value_range_("NSLink", NSURL.URLWithString_(url), link_range)
|
|
238
|
+
attr_str.addAttribute_value_range_("NSColor", NSColor.linkColor(), link_range)
|
|
239
|
+
|
|
240
|
+
text_field = NSTextField.wrappingLabelWithString_("")
|
|
241
|
+
text_field.setAttributedStringValue_(attr_str)
|
|
242
|
+
text_field.setAllowsEditingTextAttributes_(True)
|
|
243
|
+
text_field.setSelectable_(True)
|
|
244
|
+
text_field.setFrame_(NSMakeRect(0, 0, 300, 80))
|
|
245
|
+
|
|
246
|
+
alert.setAccessoryView_(text_field)
|
|
247
|
+
alert.runModal()
|
|
248
|
+
except Exception:
|
|
249
|
+
rumps.alert(
|
|
250
|
+
title="关于 SocksClient",
|
|
251
|
+
message=(
|
|
252
|
+
"项目:block-proxy\n"
|
|
253
|
+
"作者:lijing00333\n"
|
|
254
|
+
"地址:https://github.com/jayli/block-proxy\n"
|
|
255
|
+
"版本:v0.1.0"
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def quit_app(self, sender):
|
|
260
|
+
if self.connected:
|
|
261
|
+
self.sys_proxy.disable()
|
|
262
|
+
threading.Thread(target=self.proxy.stop, daemon=True).start()
|
|
263
|
+
rumps.quit_application()
|
|
264
|
+
|
|
265
|
+
def _setup_menu_delegate(self):
|
|
266
|
+
try:
|
|
267
|
+
delegate = _MenuOpenDelegate.alloc().init()
|
|
268
|
+
delegate._on_open = self._on_menu_open
|
|
269
|
+
self._menu._menu.setDelegate_(delegate)
|
|
270
|
+
self._menu_delegate = delegate
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
def _on_menu_open(self):
|
|
275
|
+
if not self.connected or self._measuring:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
self._measuring = True
|
|
279
|
+
|
|
280
|
+
def _check():
|
|
281
|
+
try:
|
|
282
|
+
latency = self.proxy.measure_latency()
|
|
283
|
+
|
|
284
|
+
def _update():
|
|
285
|
+
if not self.connected:
|
|
286
|
+
return
|
|
287
|
+
if latency is not None:
|
|
288
|
+
self.toggle_item.title = f"关闭代理({latency}ms)"
|
|
289
|
+
else:
|
|
290
|
+
self.toggle_item.title = "关闭代理(超时)"
|
|
291
|
+
|
|
292
|
+
AppHelper.callAfter(_update)
|
|
293
|
+
finally:
|
|
294
|
+
self._measuring = False
|
|
295
|
+
|
|
296
|
+
threading.Thread(target=_check, daemon=True).start()
|
|
297
|
+
|
|
298
|
+
def _start_health_check(self):
|
|
299
|
+
def check():
|
|
300
|
+
while True:
|
|
301
|
+
import time
|
|
302
|
+
time.sleep(5)
|
|
303
|
+
if self.connected and not self.proxy.is_running():
|
|
304
|
+
self._disconnect()
|
|
305
|
+
rumps.notification(
|
|
306
|
+
"SocksClient",
|
|
307
|
+
"代理已断开",
|
|
308
|
+
"代理进程意外退出",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
t = threading.Thread(target=check, daemon=True)
|
|
312
|
+
t.start()
|
package/client/build.sh
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
APP_NAME="SocksClient"
|
|
5
|
+
BUNDLE_ID="com.jaylli.socksclient"
|
|
6
|
+
VERSION="0.1.0"
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
DIST_DIR="$SCRIPT_DIR/dist"
|
|
9
|
+
APP_DIR="$DIST_DIR/$APP_NAME.app"
|
|
10
|
+
|
|
11
|
+
# Detect architecture
|
|
12
|
+
ARCH=$(uname -m)
|
|
13
|
+
ZIP_NAME="SocksClient-macos-${ARCH}.zip"
|
|
14
|
+
|
|
15
|
+
echo "==> Detected architecture: $ARCH"
|
|
16
|
+
|
|
17
|
+
echo "==> Cleaning old build..."
|
|
18
|
+
rm -rf "$DIST_DIR" "$SCRIPT_DIR/main.build" "$SCRIPT_DIR/main.dist"
|
|
19
|
+
rm -f "$DIST_DIR"/*.zip
|
|
20
|
+
|
|
21
|
+
# Find python3 from PATH
|
|
22
|
+
PYTHON=$(command -v python3 || true)
|
|
23
|
+
if [ -z "$PYTHON" ]; then
|
|
24
|
+
echo "ERROR: python3 not found in PATH"
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
echo "==> Using Python: $PYTHON ($($PYTHON --version))"
|
|
28
|
+
|
|
29
|
+
# Check nuitka
|
|
30
|
+
if ! $PYTHON -m nuitka --version &>/dev/null; then
|
|
31
|
+
echo "ERROR: nuitka not installed. Run: $PYTHON -m pip install nuitka"
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
echo "==> Generating app.icns from app_icon.png..."
|
|
36
|
+
ICONSET=$(mktemp -d)/app.iconset
|
|
37
|
+
mkdir -p "$ICONSET"
|
|
38
|
+
sips -z 16 16 "$SCRIPT_DIR/icons/app_icon.png" --out "$ICONSET/icon_16x16.png" &>/dev/null
|
|
39
|
+
sips -z 32 32 "$SCRIPT_DIR/icons/app_icon.png" --out "$ICONSET/icon_16x16@2x.png" &>/dev/null
|
|
40
|
+
sips -z 32 32 "$SCRIPT_DIR/icons/app_icon.png" --out "$ICONSET/icon_32x32.png" &>/dev/null
|
|
41
|
+
sips -z 64 64 "$SCRIPT_DIR/icons/app_icon.png" --out "$ICONSET/icon_32x32@2x.png" &>/dev/null
|
|
42
|
+
sips -z 128 128 "$SCRIPT_DIR/icons/app_icon.png" --out "$ICONSET/icon_128x128.png" &>/dev/null
|
|
43
|
+
sips -z 256 256 "$SCRIPT_DIR/icons/app_icon.png" --out "$ICONSET/icon_128x128@2x.png" &>/dev/null
|
|
44
|
+
sips -z 256 256 "$SCRIPT_DIR/icons/app_icon.png" --out "$ICONSET/icon_256x256.png" &>/dev/null
|
|
45
|
+
sips -z 512 512 "$SCRIPT_DIR/icons/app_icon.png" --out "$ICONSET/icon_256x256@2x.png" &>/dev/null
|
|
46
|
+
iconutil -c icns "$ICONSET" -o "$SCRIPT_DIR/icons/app.icns"
|
|
47
|
+
rm -rf "$(dirname "$ICONSET")"
|
|
48
|
+
|
|
49
|
+
echo "==> Building with Nuitka..."
|
|
50
|
+
cd "$SCRIPT_DIR"
|
|
51
|
+
$PYTHON -m nuitka \
|
|
52
|
+
--standalone \
|
|
53
|
+
--macos-create-app-bundle \
|
|
54
|
+
--macos-app-name="$APP_NAME" \
|
|
55
|
+
--macos-app-icon=icons/app.icns \
|
|
56
|
+
--macos-app-mode=ui-element \
|
|
57
|
+
--include-data-dir=icons=icons \
|
|
58
|
+
--include-data-files=config_window.py=config_window.py \
|
|
59
|
+
--enable-plugin=no-qt \
|
|
60
|
+
--output-dir="$DIST_DIR" \
|
|
61
|
+
main.py
|
|
62
|
+
|
|
63
|
+
echo "==> Renaming app bundle..."
|
|
64
|
+
mv "$DIST_DIR/main.app" "$APP_DIR"
|
|
65
|
+
|
|
66
|
+
echo "==> Fixing executable name..."
|
|
67
|
+
mv "$APP_DIR/Contents/MacOS/main" "$APP_DIR/Contents/MacOS/$APP_NAME"
|
|
68
|
+
|
|
69
|
+
echo "==> Patching Info.plist..."
|
|
70
|
+
plutil -replace CFBundleExecutable -string "$APP_NAME" "$APP_DIR/Contents/Info.plist"
|
|
71
|
+
plutil -replace CFBundleIdentifier -string "$BUNDLE_ID" "$APP_DIR/Contents/Info.plist"
|
|
72
|
+
plutil -replace CFBundleVersion -string "$VERSION" "$APP_DIR/Contents/Info.plist"
|
|
73
|
+
plutil -replace CFBundleShortVersionString -string "$VERSION" "$APP_DIR/Contents/Info.plist"
|
|
74
|
+
|
|
75
|
+
echo "==> Cleaning Nuitka build artifacts..."
|
|
76
|
+
rm -rf "$DIST_DIR/main.build" "$DIST_DIR/main.dist"
|
|
77
|
+
|
|
78
|
+
echo "==> Packaging..."
|
|
79
|
+
cd "$DIST_DIR"
|
|
80
|
+
rm -f "$ZIP_NAME"
|
|
81
|
+
zip -r -q "$ZIP_NAME" "$APP_NAME.app"
|
|
82
|
+
|
|
83
|
+
echo "==> Build complete: $APP_DIR"
|
|
84
|
+
echo "==> Package: $DIST_DIR/$ZIP_NAME"
|
package/client/config.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
DEFAULT_CONFIG = {
|
|
6
|
+
"server": {
|
|
7
|
+
"address": "",
|
|
8
|
+
"port": 8002,
|
|
9
|
+
"username": "",
|
|
10
|
+
"password": "",
|
|
11
|
+
"tls": True,
|
|
12
|
+
"allowInsecure": True,
|
|
13
|
+
},
|
|
14
|
+
"local": {
|
|
15
|
+
"socks_port": 1080,
|
|
16
|
+
"http_port": 1087,
|
|
17
|
+
"udp": True,
|
|
18
|
+
},
|
|
19
|
+
"mode": "global",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
DEFAULT_CONFIG_DIR = os.path.expanduser(
|
|
23
|
+
"~/Library/Application Support/SocksClient"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Config:
|
|
28
|
+
def __init__(self, config_path=None):
|
|
29
|
+
if config_path is None:
|
|
30
|
+
config_path = os.path.join(DEFAULT_CONFIG_DIR, "config.json")
|
|
31
|
+
self.config_path = config_path
|
|
32
|
+
self.data = None
|
|
33
|
+
|
|
34
|
+
def load(self):
|
|
35
|
+
if os.path.exists(self.config_path):
|
|
36
|
+
with open(self.config_path, "r") as f:
|
|
37
|
+
self.data = json.load(f)
|
|
38
|
+
else:
|
|
39
|
+
self.data = copy.deepcopy(DEFAULT_CONFIG)
|
|
40
|
+
self.save()
|
|
41
|
+
return self.data
|
|
42
|
+
|
|
43
|
+
def save(self):
|
|
44
|
+
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
|
|
45
|
+
with open(self.config_path, "w") as f:
|
|
46
|
+
json.dump(self.data, f, indent=2)
|
|
47
|
+
|
|
48
|
+
def is_configured(self):
|
|
49
|
+
return bool(self.data and self.data["server"]["address"])
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import platform
|
|
3
|
+
import sys
|
|
4
|
+
import tkinter as tk
|
|
5
|
+
from tkinter import ttk
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _macos_setup():
|
|
9
|
+
try:
|
|
10
|
+
from AppKit import NSApp
|
|
11
|
+
NSApp.setActivationPolicy_(1) # NSApplicationActivationPolicyAccessory
|
|
12
|
+
NSApp.activateIgnoringOtherApps_(True)
|
|
13
|
+
except ImportError:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _center_on_mouse_screen(w, h):
|
|
18
|
+
if platform.system() == "Darwin":
|
|
19
|
+
try:
|
|
20
|
+
from AppKit import NSScreen, NSEvent
|
|
21
|
+
mouse_loc = NSEvent.mouseLocation()
|
|
22
|
+
primary_h = NSScreen.screens()[0].frame().size.height
|
|
23
|
+
for screen in NSScreen.screens():
|
|
24
|
+
sf = screen.frame()
|
|
25
|
+
if (sf.origin.x <= mouse_loc.x < sf.origin.x + sf.size.width and
|
|
26
|
+
sf.origin.y <= mouse_loc.y < sf.origin.y + sf.size.height):
|
|
27
|
+
vf = screen.visibleFrame()
|
|
28
|
+
x = int(vf.origin.x + (vf.size.width - w) / 2)
|
|
29
|
+
y = int(primary_h - vf.origin.y - vf.size.height +
|
|
30
|
+
(vf.size.height - h) / 2)
|
|
31
|
+
return x, y
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def show_config_window(config_path):
|
|
38
|
+
with open(config_path, "r") as f:
|
|
39
|
+
config = json.load(f)
|
|
40
|
+
|
|
41
|
+
def save_and_close():
|
|
42
|
+
config["server"]["address"] = entries["address"].get()
|
|
43
|
+
config["server"]["port"] = int(entries["port"].get())
|
|
44
|
+
config["server"]["username"] = entries["username"].get()
|
|
45
|
+
config["server"]["password"] = entries["password"].get()
|
|
46
|
+
config["server"]["tls"] = tls_var.get()
|
|
47
|
+
config["server"]["allowInsecure"] = insecure_var.get() == "true"
|
|
48
|
+
config["local"]["socks_port"] = int(entries["socks_port"].get())
|
|
49
|
+
config["local"]["http_port"] = int(entries["http_port"].get())
|
|
50
|
+
config["local"]["udp"] = udp_var.get()
|
|
51
|
+
config["local"]["proxy_private"] = proxy_private_var.get()
|
|
52
|
+
config["autostart"] = autostart_var.get()
|
|
53
|
+
|
|
54
|
+
with open(config_path, "w") as f:
|
|
55
|
+
json.dump(config, f, indent=2)
|
|
56
|
+
root.destroy()
|
|
57
|
+
|
|
58
|
+
pos = _center_on_mouse_screen(400, 460)
|
|
59
|
+
|
|
60
|
+
root = tk.Tk()
|
|
61
|
+
root.title("Socks 节点配置")
|
|
62
|
+
root.resizable(False, False)
|
|
63
|
+
w, h = 400, 460
|
|
64
|
+
if pos:
|
|
65
|
+
x, y = pos
|
|
66
|
+
else:
|
|
67
|
+
x = (root.winfo_screenwidth() - w) // 2
|
|
68
|
+
y = (root.winfo_screenheight() - h) // 2
|
|
69
|
+
root.geometry(f"{w}x{h}+{x}+{y}")
|
|
70
|
+
|
|
71
|
+
if platform.system() == "Darwin":
|
|
72
|
+
root.after(50, _macos_setup)
|
|
73
|
+
|
|
74
|
+
frame = ttk.Frame(root, padding=20)
|
|
75
|
+
frame.pack(fill="both", expand=True)
|
|
76
|
+
frame.grid_columnconfigure(1, weight=1)
|
|
77
|
+
|
|
78
|
+
entries = {}
|
|
79
|
+
fields = [
|
|
80
|
+
("address", "地址:", config["server"]["address"]),
|
|
81
|
+
("port", "端口:", str(config["server"]["port"])),
|
|
82
|
+
("username", "用户名:", config["server"]["username"]),
|
|
83
|
+
("password", "密码:", config["server"]["password"]),
|
|
84
|
+
("socks_port", "本地SOCKS端口:", str(config["local"]["socks_port"])),
|
|
85
|
+
("http_port", "本地HTTP端口:", str(config["local"]["http_port"])),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
for i, (key, label, default) in enumerate(fields):
|
|
89
|
+
ttk.Label(frame, text=label).grid(row=i, column=0, sticky="w", pady=4, padx=(0, 8))
|
|
90
|
+
entry = ttk.Entry(frame)
|
|
91
|
+
entry.insert(0, default)
|
|
92
|
+
entry.grid(row=i, column=1, sticky="ew", pady=4)
|
|
93
|
+
entries[key] = entry
|
|
94
|
+
|
|
95
|
+
row = len(fields)
|
|
96
|
+
|
|
97
|
+
tls_var = tk.BooleanVar(value=config["server"]["tls"])
|
|
98
|
+
ttk.Label(frame, text="启用 TLS:").grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
|
|
99
|
+
ttk.Checkbutton(frame, variable=tls_var).grid(
|
|
100
|
+
row=row, column=1, sticky="w", pady=4
|
|
101
|
+
)
|
|
102
|
+
row += 1
|
|
103
|
+
|
|
104
|
+
ttk.Label(frame, text="allowInsecure:").grid(row=row, column=0, sticky="w", pady=4)
|
|
105
|
+
insecure_var = tk.StringVar(
|
|
106
|
+
value="true" if config["server"]["allowInsecure"] else "false"
|
|
107
|
+
)
|
|
108
|
+
insecure_combo = ttk.Combobox(
|
|
109
|
+
frame, textvariable=insecure_var, values=["true", "false"], state="readonly", width=10
|
|
110
|
+
)
|
|
111
|
+
insecure_combo.grid(row=row, column=1, sticky="w", pady=4)
|
|
112
|
+
row += 1
|
|
113
|
+
|
|
114
|
+
udp_var = tk.BooleanVar(value=config["local"]["udp"])
|
|
115
|
+
ttk.Label(frame, text="启用 UDP:").grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
|
|
116
|
+
ttk.Checkbutton(frame, variable=udp_var).grid(
|
|
117
|
+
row=row, column=1, sticky="w", pady=4
|
|
118
|
+
)
|
|
119
|
+
row += 1
|
|
120
|
+
|
|
121
|
+
proxy_private_var = tk.BooleanVar(value=config["local"].get("proxy_private", False))
|
|
122
|
+
ttk.Label(frame, text="代理私有地址段:").grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
|
|
123
|
+
ttk.Checkbutton(frame, variable=proxy_private_var).grid(
|
|
124
|
+
row=row, column=1, sticky="w", pady=4
|
|
125
|
+
)
|
|
126
|
+
row += 1
|
|
127
|
+
|
|
128
|
+
ttk.Separator(frame, orient="horizontal").grid(
|
|
129
|
+
row=row, column=0, columnspan=2, sticky="ew", pady=10
|
|
130
|
+
)
|
|
131
|
+
row += 1
|
|
132
|
+
|
|
133
|
+
autostart_var = tk.BooleanVar(value=config.get("autostart", False))
|
|
134
|
+
ttk.Label(frame, text="开机启动:").grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
|
|
135
|
+
ttk.Checkbutton(frame, variable=autostart_var).grid(
|
|
136
|
+
row=row, column=1, sticky="w", pady=4
|
|
137
|
+
)
|
|
138
|
+
row += 1
|
|
139
|
+
|
|
140
|
+
ttk.Button(frame, text="保存", command=save_and_close).grid(
|
|
141
|
+
row=row, column=0, columnspan=2, pady=15
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
root.lift()
|
|
145
|
+
root.attributes("-topmost", True)
|
|
146
|
+
if platform.system() != "Darwin":
|
|
147
|
+
root.after(100, lambda: root.focus_force())
|
|
148
|
+
root.mainloop()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
if len(sys.argv) != 2:
|
|
153
|
+
print("Usage: python config_window.py <config_path>")
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
show_config_window(sys.argv[1])
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/client/main.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import fcntl
|
|
4
|
+
|
|
5
|
+
LOCK_PATH = os.path.expanduser("~/Library/Application Support/SocksClient/.lock")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def acquire_lock():
|
|
9
|
+
os.makedirs(os.path.dirname(LOCK_PATH), exist_ok=True)
|
|
10
|
+
fp = open(LOCK_PATH, "w")
|
|
11
|
+
try:
|
|
12
|
+
fcntl.flock(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
13
|
+
except OSError:
|
|
14
|
+
sys.exit(0)
|
|
15
|
+
fp.write(str(os.getpid()))
|
|
16
|
+
fp.flush()
|
|
17
|
+
return fp
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
lock = acquire_lock()
|
|
22
|
+
from app import SocksClient
|
|
23
|
+
client = SocksClient()
|
|
24
|
+
client.run()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
main()
|