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/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()
@@ -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"
@@ -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
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()