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.
- package/.claude/settings.local.json +33 -1
- package/.claude/skills/build-client/skill.md +24 -0
- package/.claude/skills/release-client/skill.md +68 -0
- package/CLAUDE.md +69 -67
- package/Dockerfile +1 -1
- package/README.md +38 -24
- 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.af1923ea.js} +3 -3
- package/build/static/js/main.af1923ea.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 +4 -199
- 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 +19 -34
- package/server/express.js +17 -1
- package/src/App.css +596 -276
- package/src/App.js +25 -32
- 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.af1923ea.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import ipaddress
|
|
4
|
+
import logging
|
|
5
|
+
import ssl
|
|
6
|
+
import struct
|
|
7
|
+
import threading
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("proxy_core")
|
|
10
|
+
|
|
11
|
+
PRIVATE_NETWORKS = [
|
|
12
|
+
ipaddress.ip_network("127.0.0.0/8"),
|
|
13
|
+
ipaddress.ip_network("10.0.0.0/8"),
|
|
14
|
+
ipaddress.ip_network("172.16.0.0/12"),
|
|
15
|
+
ipaddress.ip_network("192.168.0.0/16"),
|
|
16
|
+
ipaddress.ip_network("::1/128"),
|
|
17
|
+
ipaddress.ip_network("fc00::/7"),
|
|
18
|
+
ipaddress.ip_network("fe80::/10"),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@functools.lru_cache(maxsize=256)
|
|
23
|
+
def is_private_ip(host):
|
|
24
|
+
try:
|
|
25
|
+
addr = ipaddress.ip_address(host)
|
|
26
|
+
return any(addr in net for net in PRIVATE_NETWORKS)
|
|
27
|
+
except ValueError:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
RELAY_IDLE_TIMEOUT = 300
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def relay(reader, writer):
|
|
35
|
+
try:
|
|
36
|
+
while True:
|
|
37
|
+
data = await asyncio.wait_for(reader.read(65536), timeout=RELAY_IDLE_TIMEOUT)
|
|
38
|
+
if not data:
|
|
39
|
+
break
|
|
40
|
+
writer.write(data)
|
|
41
|
+
if writer.transport.get_write_buffer_size() > 65536:
|
|
42
|
+
await writer.drain()
|
|
43
|
+
except asyncio.TimeoutError:
|
|
44
|
+
pass
|
|
45
|
+
except (ConnectionResetError, BrokenPipeError, OSError):
|
|
46
|
+
pass
|
|
47
|
+
finally:
|
|
48
|
+
try:
|
|
49
|
+
if writer.can_write_eof():
|
|
50
|
+
writer.write_eof()
|
|
51
|
+
else:
|
|
52
|
+
writer.close()
|
|
53
|
+
await writer.wait_closed()
|
|
54
|
+
except OSError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
CONNECT_TIMEOUT = 10
|
|
59
|
+
HANDSHAKE_TIMEOUT = 10
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def connect_upstream_socks5(server_config, dest_addr, dest_port, ssl_ctx=None):
|
|
63
|
+
host = server_config["address"]
|
|
64
|
+
port = server_config["port"]
|
|
65
|
+
username = server_config["username"]
|
|
66
|
+
password = server_config["password"]
|
|
67
|
+
use_tls = server_config["tls"]
|
|
68
|
+
|
|
69
|
+
reader, writer = await asyncio.wait_for(
|
|
70
|
+
asyncio.open_connection(
|
|
71
|
+
host, port, ssl=ssl_ctx if use_tls else None,
|
|
72
|
+
server_hostname=host if use_tls else None,
|
|
73
|
+
),
|
|
74
|
+
timeout=CONNECT_TIMEOUT,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def _handshake():
|
|
78
|
+
if username and password:
|
|
79
|
+
writer.write(b"\x05\x01\x02")
|
|
80
|
+
else:
|
|
81
|
+
writer.write(b"\x05\x01\x00")
|
|
82
|
+
await writer.drain()
|
|
83
|
+
|
|
84
|
+
resp = await reader.readexactly(2)
|
|
85
|
+
if resp[0] != 0x05:
|
|
86
|
+
raise Exception("SOCKS5 version mismatch")
|
|
87
|
+
|
|
88
|
+
if resp[1] == 0x02:
|
|
89
|
+
uname = username.encode("utf-8")
|
|
90
|
+
passwd = password.encode("utf-8")
|
|
91
|
+
writer.write(
|
|
92
|
+
b"\x01"
|
|
93
|
+
+ struct.pack("B", len(uname))
|
|
94
|
+
+ uname
|
|
95
|
+
+ struct.pack("B", len(passwd))
|
|
96
|
+
+ passwd
|
|
97
|
+
)
|
|
98
|
+
await writer.drain()
|
|
99
|
+
auth_resp = await reader.readexactly(2)
|
|
100
|
+
if auth_resp[1] != 0x00:
|
|
101
|
+
raise Exception("SOCKS5 auth failed")
|
|
102
|
+
elif resp[1] == 0xFF:
|
|
103
|
+
raise Exception("SOCKS5 no acceptable auth method")
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
addr = ipaddress.ip_address(dest_addr)
|
|
107
|
+
if isinstance(addr, ipaddress.IPv4Address):
|
|
108
|
+
addr_data = b"\x01" + addr.packed
|
|
109
|
+
else:
|
|
110
|
+
addr_data = b"\x04" + addr.packed
|
|
111
|
+
except ValueError:
|
|
112
|
+
encoded = dest_addr.encode("utf-8")
|
|
113
|
+
addr_data = b"\x03" + struct.pack("B", len(encoded)) + encoded
|
|
114
|
+
|
|
115
|
+
writer.write(
|
|
116
|
+
b"\x05\x01\x00" + addr_data + struct.pack("!H", dest_port)
|
|
117
|
+
)
|
|
118
|
+
await writer.drain()
|
|
119
|
+
|
|
120
|
+
reply = await reader.readexactly(4)
|
|
121
|
+
if reply[1] != 0x00:
|
|
122
|
+
raise Exception(f"SOCKS5 CONNECT failed: {reply[1]:#x}")
|
|
123
|
+
|
|
124
|
+
if reply[3] == 0x01:
|
|
125
|
+
await reader.readexactly(4 + 2)
|
|
126
|
+
elif reply[3] == 0x03:
|
|
127
|
+
length = (await reader.readexactly(1))[0]
|
|
128
|
+
await reader.readexactly(length + 2)
|
|
129
|
+
elif reply[3] == 0x04:
|
|
130
|
+
await reader.readexactly(16 + 2)
|
|
131
|
+
|
|
132
|
+
await asyncio.wait_for(_handshake(), timeout=HANDSHAKE_TIMEOUT)
|
|
133
|
+
|
|
134
|
+
return reader, writer
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def connect_direct(dest_addr, dest_port):
|
|
138
|
+
return await asyncio.open_connection(dest_addr, dest_port)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
MAX_CONCURRENT = 256
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ProxyCore:
|
|
145
|
+
def __init__(self):
|
|
146
|
+
self._loop = None
|
|
147
|
+
self._thread = None
|
|
148
|
+
self._socks_server = None
|
|
149
|
+
self._http_server = None
|
|
150
|
+
self._running = False
|
|
151
|
+
self._server_config = None
|
|
152
|
+
self._proxy_private = False
|
|
153
|
+
self._socks_port = 1080
|
|
154
|
+
self._http_port = 1087
|
|
155
|
+
self._ssl_ctx = None
|
|
156
|
+
self._semaphore = None
|
|
157
|
+
|
|
158
|
+
def _build_ssl_context(self):
|
|
159
|
+
server = self._server_config
|
|
160
|
+
if not server["tls"]:
|
|
161
|
+
self._ssl_ctx = None
|
|
162
|
+
return
|
|
163
|
+
ctx = ssl.create_default_context()
|
|
164
|
+
if server["allowInsecure"]:
|
|
165
|
+
ctx.check_hostname = False
|
|
166
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
167
|
+
self._ssl_ctx = ctx
|
|
168
|
+
|
|
169
|
+
def start(self, user_config):
|
|
170
|
+
if self._running:
|
|
171
|
+
self.stop()
|
|
172
|
+
|
|
173
|
+
self._server_config = user_config["server"]
|
|
174
|
+
local = user_config["local"]
|
|
175
|
+
self._socks_port = local["socks_port"]
|
|
176
|
+
self._http_port = local["http_port"]
|
|
177
|
+
self._proxy_private = local.get("proxy_private", False)
|
|
178
|
+
self._build_ssl_context()
|
|
179
|
+
|
|
180
|
+
self._loop = asyncio.new_event_loop()
|
|
181
|
+
started = threading.Event()
|
|
182
|
+
self._start_error = None
|
|
183
|
+
self._thread = threading.Thread(
|
|
184
|
+
target=self._run_loop, args=(started,), daemon=True
|
|
185
|
+
)
|
|
186
|
+
self._thread.start()
|
|
187
|
+
started.wait(timeout=5)
|
|
188
|
+
if self._start_error:
|
|
189
|
+
raise self._start_error
|
|
190
|
+
self._running = True
|
|
191
|
+
|
|
192
|
+
def stop(self):
|
|
193
|
+
if not self._running:
|
|
194
|
+
return
|
|
195
|
+
self._running = False
|
|
196
|
+
if self._loop and self._loop.is_running():
|
|
197
|
+
def _shutdown():
|
|
198
|
+
if self._socks_server:
|
|
199
|
+
self._socks_server.close()
|
|
200
|
+
if self._http_server:
|
|
201
|
+
self._http_server.close()
|
|
202
|
+
self._loop.stop()
|
|
203
|
+
self._loop.call_soon_threadsafe(_shutdown)
|
|
204
|
+
if self._thread:
|
|
205
|
+
self._thread.join(timeout=2)
|
|
206
|
+
if self._loop:
|
|
207
|
+
self._loop.close()
|
|
208
|
+
self._loop = None
|
|
209
|
+
self._thread = None
|
|
210
|
+
|
|
211
|
+
def is_running(self):
|
|
212
|
+
return self._running and self._thread is not None and self._thread.is_alive()
|
|
213
|
+
|
|
214
|
+
async def _measure_latency(self):
|
|
215
|
+
import time
|
|
216
|
+
start = time.monotonic()
|
|
217
|
+
try:
|
|
218
|
+
reader, writer = await asyncio.wait_for(
|
|
219
|
+
asyncio.open_connection(
|
|
220
|
+
self._server_config["address"],
|
|
221
|
+
self._server_config["port"],
|
|
222
|
+
ssl=self._ssl_ctx if self._server_config["tls"] else None,
|
|
223
|
+
server_hostname=self._server_config["address"] if self._server_config["tls"] else None,
|
|
224
|
+
),
|
|
225
|
+
timeout=5,
|
|
226
|
+
)
|
|
227
|
+
username = self._server_config.get("username", "")
|
|
228
|
+
password = self._server_config.get("password", "")
|
|
229
|
+
if username and password:
|
|
230
|
+
writer.write(b"\x05\x01\x02")
|
|
231
|
+
else:
|
|
232
|
+
writer.write(b"\x05\x01\x00")
|
|
233
|
+
await writer.drain()
|
|
234
|
+
resp = await asyncio.wait_for(reader.readexactly(2), timeout=5)
|
|
235
|
+
if resp[0] != 0x05:
|
|
236
|
+
writer.close()
|
|
237
|
+
return None
|
|
238
|
+
if resp[1] == 0x02 and username and password:
|
|
239
|
+
uname = username.encode("utf-8")
|
|
240
|
+
passwd = password.encode("utf-8")
|
|
241
|
+
writer.write(
|
|
242
|
+
b"\x01"
|
|
243
|
+
+ struct.pack("B", len(uname)) + uname
|
|
244
|
+
+ struct.pack("B", len(passwd)) + passwd
|
|
245
|
+
)
|
|
246
|
+
await writer.drain()
|
|
247
|
+
auth_resp = await asyncio.wait_for(reader.readexactly(2), timeout=5)
|
|
248
|
+
if auth_resp[1] != 0x00:
|
|
249
|
+
writer.close()
|
|
250
|
+
return None
|
|
251
|
+
elapsed = time.monotonic() - start
|
|
252
|
+
writer.close()
|
|
253
|
+
try:
|
|
254
|
+
await writer.wait_closed()
|
|
255
|
+
except OSError:
|
|
256
|
+
pass
|
|
257
|
+
return int(elapsed * 1000)
|
|
258
|
+
except Exception:
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
def measure_latency(self):
|
|
262
|
+
if not self._running or not self._loop or not self._loop.is_running():
|
|
263
|
+
return None
|
|
264
|
+
future = asyncio.run_coroutine_threadsafe(self._measure_latency(), self._loop)
|
|
265
|
+
try:
|
|
266
|
+
return future.result(timeout=6)
|
|
267
|
+
except Exception:
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
def _run_loop(self, started_event):
|
|
271
|
+
asyncio.set_event_loop(self._loop)
|
|
272
|
+
try:
|
|
273
|
+
self._loop.run_until_complete(self._start_servers())
|
|
274
|
+
except OSError as e:
|
|
275
|
+
self._start_error = e
|
|
276
|
+
started_event.set()
|
|
277
|
+
return
|
|
278
|
+
started_event.set()
|
|
279
|
+
self._loop.run_forever()
|
|
280
|
+
|
|
281
|
+
async def _start_servers(self):
|
|
282
|
+
self._semaphore = asyncio.Semaphore(MAX_CONCURRENT)
|
|
283
|
+
self._socks_server = await asyncio.start_server(
|
|
284
|
+
self._handle_socks, "127.0.0.1", self._socks_port
|
|
285
|
+
)
|
|
286
|
+
self._http_server = await asyncio.start_server(
|
|
287
|
+
self._handle_http, "127.0.0.1", self._http_port
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
async def _stop_servers(self):
|
|
291
|
+
if self._socks_server:
|
|
292
|
+
self._socks_server.close()
|
|
293
|
+
await self._socks_server.wait_closed()
|
|
294
|
+
if self._http_server:
|
|
295
|
+
self._http_server.close()
|
|
296
|
+
await self._http_server.wait_closed()
|
|
297
|
+
|
|
298
|
+
def _should_direct(self, host):
|
|
299
|
+
if self._proxy_private:
|
|
300
|
+
return False
|
|
301
|
+
return is_private_ip(host)
|
|
302
|
+
|
|
303
|
+
async def _connect_target(self, dest_addr, dest_port):
|
|
304
|
+
if self._should_direct(dest_addr):
|
|
305
|
+
return await connect_direct(dest_addr, dest_port)
|
|
306
|
+
return await connect_upstream_socks5(
|
|
307
|
+
self._server_config, dest_addr, dest_port, ssl_ctx=self._ssl_ctx
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
async def _handle_socks(self, client_reader, client_writer):
|
|
311
|
+
async with self._semaphore:
|
|
312
|
+
await self._do_handle_socks(client_reader, client_writer)
|
|
313
|
+
|
|
314
|
+
async def _do_handle_socks(self, client_reader, client_writer):
|
|
315
|
+
try:
|
|
316
|
+
header = await client_reader.readexactly(2)
|
|
317
|
+
ver, nmethods = header
|
|
318
|
+
if ver != 0x05:
|
|
319
|
+
client_writer.close()
|
|
320
|
+
return
|
|
321
|
+
await client_reader.readexactly(nmethods)
|
|
322
|
+
|
|
323
|
+
client_writer.write(b"\x05\x00")
|
|
324
|
+
await client_writer.drain()
|
|
325
|
+
|
|
326
|
+
req = await client_reader.readexactly(4)
|
|
327
|
+
ver, cmd, _, atyp = req
|
|
328
|
+
|
|
329
|
+
if cmd != 0x01:
|
|
330
|
+
client_writer.write(
|
|
331
|
+
b"\x05\x07\x00\x01" + b"\x00" * 4 + b"\x00\x00"
|
|
332
|
+
)
|
|
333
|
+
await client_writer.drain()
|
|
334
|
+
client_writer.close()
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
if atyp == 0x01:
|
|
338
|
+
raw = await client_reader.readexactly(4)
|
|
339
|
+
dest_addr = str(ipaddress.IPv4Address(raw))
|
|
340
|
+
elif atyp == 0x03:
|
|
341
|
+
length = (await client_reader.readexactly(1))[0]
|
|
342
|
+
dest_addr = (await client_reader.readexactly(length)).decode("utf-8")
|
|
343
|
+
elif atyp == 0x04:
|
|
344
|
+
raw = await client_reader.readexactly(16)
|
|
345
|
+
dest_addr = str(ipaddress.IPv6Address(raw))
|
|
346
|
+
else:
|
|
347
|
+
client_writer.write(
|
|
348
|
+
b"\x05\x08\x00\x01" + b"\x00" * 4 + b"\x00\x00"
|
|
349
|
+
)
|
|
350
|
+
await client_writer.drain()
|
|
351
|
+
client_writer.close()
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
port_data = await client_reader.readexactly(2)
|
|
355
|
+
dest_port = struct.unpack("!H", port_data)[0]
|
|
356
|
+
|
|
357
|
+
remote_reader, remote_writer = await self._connect_target(
|
|
358
|
+
dest_addr, dest_port
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
client_writer.write(
|
|
362
|
+
b"\x05\x00\x00\x01" + b"\x00" * 4 + b"\x00\x00"
|
|
363
|
+
)
|
|
364
|
+
await client_writer.drain()
|
|
365
|
+
|
|
366
|
+
await asyncio.gather(
|
|
367
|
+
relay(client_reader, remote_writer),
|
|
368
|
+
relay(remote_reader, client_writer),
|
|
369
|
+
)
|
|
370
|
+
except Exception:
|
|
371
|
+
pass
|
|
372
|
+
finally:
|
|
373
|
+
try:
|
|
374
|
+
client_writer.close()
|
|
375
|
+
await client_writer.wait_closed()
|
|
376
|
+
except OSError:
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
async def _handle_http(self, client_reader, client_writer):
|
|
380
|
+
async with self._semaphore:
|
|
381
|
+
await self._do_handle_http(client_reader, client_writer)
|
|
382
|
+
|
|
383
|
+
async def _do_handle_http(self, client_reader, client_writer):
|
|
384
|
+
try:
|
|
385
|
+
raw_line = await client_reader.readline()
|
|
386
|
+
if not raw_line:
|
|
387
|
+
client_writer.close()
|
|
388
|
+
return
|
|
389
|
+
line = raw_line.decode("utf-8", errors="replace").strip()
|
|
390
|
+
parts = line.split()
|
|
391
|
+
if len(parts) < 3:
|
|
392
|
+
client_writer.close()
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
method = parts[0].upper()
|
|
396
|
+
|
|
397
|
+
if method == "CONNECT":
|
|
398
|
+
target = parts[1]
|
|
399
|
+
if ":" in target:
|
|
400
|
+
host, port_str = target.rsplit(":", 1)
|
|
401
|
+
port = int(port_str)
|
|
402
|
+
else:
|
|
403
|
+
host = target
|
|
404
|
+
port = 443
|
|
405
|
+
|
|
406
|
+
while True:
|
|
407
|
+
header_line = await client_reader.readline()
|
|
408
|
+
if header_line in (b"\r\n", b"\n", b""):
|
|
409
|
+
break
|
|
410
|
+
|
|
411
|
+
remote_reader, remote_writer = await self._connect_target(
|
|
412
|
+
host, port
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
client_writer.write(
|
|
416
|
+
b"HTTP/1.1 200 Connection Established\r\n\r\n"
|
|
417
|
+
)
|
|
418
|
+
await client_writer.drain()
|
|
419
|
+
|
|
420
|
+
await asyncio.gather(
|
|
421
|
+
relay(client_reader, remote_writer),
|
|
422
|
+
relay(remote_reader, client_writer),
|
|
423
|
+
)
|
|
424
|
+
else:
|
|
425
|
+
url = parts[1]
|
|
426
|
+
if url.startswith("http://"):
|
|
427
|
+
url_body = url[7:]
|
|
428
|
+
slash_idx = url_body.find("/")
|
|
429
|
+
if slash_idx == -1:
|
|
430
|
+
host_part = url_body
|
|
431
|
+
path = "/"
|
|
432
|
+
else:
|
|
433
|
+
host_part = url_body[:slash_idx]
|
|
434
|
+
path = url_body[slash_idx:]
|
|
435
|
+
|
|
436
|
+
if ":" in host_part:
|
|
437
|
+
host, port_str = host_part.rsplit(":", 1)
|
|
438
|
+
port = int(port_str)
|
|
439
|
+
else:
|
|
440
|
+
host = host_part
|
|
441
|
+
port = 80
|
|
442
|
+
|
|
443
|
+
headers = []
|
|
444
|
+
while True:
|
|
445
|
+
header_line = await client_reader.readline()
|
|
446
|
+
if header_line in (b"\r\n", b"\n", b""):
|
|
447
|
+
break
|
|
448
|
+
headers.append(header_line)
|
|
449
|
+
|
|
450
|
+
remote_reader, remote_writer = await self._connect_target(
|
|
451
|
+
host, port
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
request_line = f"{method} {path} {parts[2]}\r\n".encode()
|
|
455
|
+
remote_writer.write(request_line)
|
|
456
|
+
for h in headers:
|
|
457
|
+
remote_writer.write(h)
|
|
458
|
+
remote_writer.write(b"\r\n")
|
|
459
|
+
await remote_writer.drain()
|
|
460
|
+
|
|
461
|
+
await asyncio.gather(
|
|
462
|
+
relay(remote_reader, client_writer),
|
|
463
|
+
relay(client_reader, remote_writer),
|
|
464
|
+
)
|
|
465
|
+
else:
|
|
466
|
+
client_writer.close()
|
|
467
|
+
return
|
|
468
|
+
except Exception:
|
|
469
|
+
pass
|
|
470
|
+
finally:
|
|
471
|
+
try:
|
|
472
|
+
client_writer.close()
|
|
473
|
+
await client_writer.wait_closed()
|
|
474
|
+
except OSError:
|
|
475
|
+
pass
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
XRAY_VERSION="v25.5.16"
|
|
5
|
+
PLATFORM="macos"
|
|
6
|
+
ARCH=$(uname -m)
|
|
7
|
+
|
|
8
|
+
if [ "$ARCH" = "arm64" ]; then
|
|
9
|
+
FILENAME="Xray-macos-arm64-v8a.zip"
|
|
10
|
+
elif [ "$ARCH" = "x86_64" ]; then
|
|
11
|
+
FILENAME="Xray-macos-64.zip"
|
|
12
|
+
else
|
|
13
|
+
echo "Unsupported architecture: $ARCH"
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
URL="https://github.com/XTLS/Xray-core/releases/download/${XRAY_VERSION}/${FILENAME}"
|
|
18
|
+
DEST_DIR="$(cd "$(dirname "$0")/../resources" && pwd)"
|
|
19
|
+
|
|
20
|
+
echo "Downloading xray-core ${XRAY_VERSION} for ${ARCH}..."
|
|
21
|
+
curl -L -o /tmp/xray.zip "$URL"
|
|
22
|
+
|
|
23
|
+
echo "Extracting..."
|
|
24
|
+
unzip -o /tmp/xray.zip xray -d /tmp
|
|
25
|
+
mv /tmp/xray "$DEST_DIR/core"
|
|
26
|
+
chmod +x "$DEST_DIR/core"
|
|
27
|
+
rm /tmp/xray.zip
|
|
28
|
+
|
|
29
|
+
echo "Done: $DEST_DIR/core"
|
|
30
|
+
"$DEST_DIR/core" version
|
package/client/setup.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
|
|
3
|
+
APP = ["main.py"]
|
|
4
|
+
DATA_FILES = [
|
|
5
|
+
("icons", [
|
|
6
|
+
"icons/socks_on_G.png",
|
|
7
|
+
"icons/socks_on_M.png",
|
|
8
|
+
"icons/christmas-sock_light.png",
|
|
9
|
+
]),
|
|
10
|
+
]
|
|
11
|
+
OPTIONS = {
|
|
12
|
+
"argv_emulation": False,
|
|
13
|
+
"iconfile": "icons/app_example.png",
|
|
14
|
+
"plist": {
|
|
15
|
+
"LSUIElement": True,
|
|
16
|
+
"CFBundleName": "SocksClient",
|
|
17
|
+
"CFBundleIdentifier": "com.jaylli.socksclient",
|
|
18
|
+
"CFBundleVersion": "1.0.0",
|
|
19
|
+
"CFBundleShortVersionString": "1.0.0",
|
|
20
|
+
},
|
|
21
|
+
"packages": ["rumps"],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setup(
|
|
25
|
+
name="SocksClient",
|
|
26
|
+
app=APP,
|
|
27
|
+
data_files=DATA_FILES,
|
|
28
|
+
options={"py2app": OPTIONS},
|
|
29
|
+
setup_requires=["py2app"],
|
|
30
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
import atexit
|
|
4
|
+
import signal
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("system_proxy")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run_networksetup(args):
|
|
11
|
+
result = subprocess.run(args, capture_output=True, text=True)
|
|
12
|
+
if result.returncode != 0:
|
|
13
|
+
logger.warning("networksetup failed: %s -> %s", " ".join(args), result.stderr.strip())
|
|
14
|
+
return result.returncode == 0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SystemProxy:
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._enabled = False
|
|
20
|
+
self._interfaces = []
|
|
21
|
+
atexit.register(self._cleanup)
|
|
22
|
+
signal.signal(signal.SIGTERM, self._signal_handler)
|
|
23
|
+
|
|
24
|
+
def _signal_handler(self, signum, frame):
|
|
25
|
+
self._cleanup()
|
|
26
|
+
raise SystemExit(0)
|
|
27
|
+
|
|
28
|
+
def _cleanup(self):
|
|
29
|
+
if self._enabled:
|
|
30
|
+
try:
|
|
31
|
+
self.disable()
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def _get_active_interfaces(self):
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["networksetup", "-listallnetworkservices"],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
)
|
|
41
|
+
interfaces = []
|
|
42
|
+
for line in result.stdout.strip().split("\n"):
|
|
43
|
+
if line.startswith("*") or line.startswith("An asterisk"):
|
|
44
|
+
continue
|
|
45
|
+
if line.strip():
|
|
46
|
+
interfaces.append(line.strip())
|
|
47
|
+
return interfaces
|
|
48
|
+
|
|
49
|
+
def enable(self, socks_port, http_port):
|
|
50
|
+
self._interfaces = self._get_active_interfaces()
|
|
51
|
+
tasks = []
|
|
52
|
+
for iface in self._interfaces:
|
|
53
|
+
tasks.extend([
|
|
54
|
+
["networksetup", "-setsocksfirewallproxy", iface, "127.0.0.1", str(socks_port)],
|
|
55
|
+
["networksetup", "-setsocksfirewallproxystate", iface, "on"],
|
|
56
|
+
["networksetup", "-setwebproxy", iface, "127.0.0.1", str(http_port)],
|
|
57
|
+
["networksetup", "-setwebproxystate", iface, "on"],
|
|
58
|
+
["networksetup", "-setsecurewebproxy", iface, "127.0.0.1", str(http_port)],
|
|
59
|
+
["networksetup", "-setsecurewebproxystate", iface, "on"],
|
|
60
|
+
])
|
|
61
|
+
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
62
|
+
list(pool.map(_run_networksetup, tasks))
|
|
63
|
+
self._enabled = True
|
|
64
|
+
self._verify(socks_port, http_port)
|
|
65
|
+
|
|
66
|
+
def _verify(self, socks_port, http_port):
|
|
67
|
+
checks = [
|
|
68
|
+
("-getsocksfirewallproxy", "-setsocksfirewallproxystate"),
|
|
69
|
+
("-getwebproxy", "-setwebproxystate"),
|
|
70
|
+
("-getsecurewebproxy", "-setsecurewebproxystate"),
|
|
71
|
+
]
|
|
72
|
+
for iface in self._interfaces:
|
|
73
|
+
for get_cmd, set_cmd in checks:
|
|
74
|
+
result = subprocess.run(
|
|
75
|
+
["networksetup", get_cmd, iface],
|
|
76
|
+
capture_output=True, text=True,
|
|
77
|
+
)
|
|
78
|
+
if "Enabled: No" in result.stdout:
|
|
79
|
+
logger.warning("proxy not enabled after setting: %s %s, retrying", get_cmd, iface)
|
|
80
|
+
_run_networksetup(["networksetup", set_cmd, iface, "on"])
|
|
81
|
+
|
|
82
|
+
def disable(self):
|
|
83
|
+
interfaces = self._interfaces or self._get_active_interfaces()
|
|
84
|
+
tasks = []
|
|
85
|
+
for iface in interfaces:
|
|
86
|
+
tasks.extend([
|
|
87
|
+
["networksetup", "-setsocksfirewallproxystate", iface, "off"],
|
|
88
|
+
["networksetup", "-setwebproxystate", iface, "off"],
|
|
89
|
+
["networksetup", "-setsecurewebproxystate", iface, "off"],
|
|
90
|
+
])
|
|
91
|
+
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
92
|
+
list(pool.map(_run_networksetup, tasks))
|
|
93
|
+
self._enabled = False
|
|
94
|
+
self._interfaces = []
|
|
File without changes
|