tanbushi commited on
Commit
52bfd89
·
1 Parent(s): 72a5949

refactor: 改用HTTP CONNECT代理替代wstunnel, 适配HF Space代理层

Browse files
Files changed (1) hide show
  1. server.py +80 -53
server.py CHANGED
@@ -2,48 +2,70 @@ import asyncio
2
  import os
3
  import signal
4
  import sys
 
5
 
6
- WSTUNNEL_PORT = 7861
7
  PROXY_PORT = 7860
8
- WSTUNNEL_BIN = "/usr/local/bin/wstunnel"
9
 
10
 
11
- async def health_check(reader, writer):
12
  writer.write(b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nOK\n")
13
  await writer.drain()
14
  writer.close()
15
 
16
 
17
- async def tcp_proxy(reader, writer, initial_data=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  try:
19
  backend_reader, backend_writer = await asyncio.wait_for(
20
- asyncio.open_connection("127.0.0.1", WSTUNNEL_PORT), timeout=5
21
  )
22
  except (OSError, asyncio.TimeoutError):
23
- writer.write(b"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 15\r\n\r\nBackend unavailable\n")
24
- await writer.drain()
25
- writer.close()
26
  return
27
 
28
- if initial_data:
29
- backend_writer.write(initial_data)
30
- await backend_writer.drain()
31
 
32
- async def forward(src, dst):
33
- try:
34
- while True:
35
- data = await src.read(65536)
36
- if not data:
37
- break
38
- dst.write(data)
39
- await dst.drain()
40
- except (ConnectionResetError, BrokenPipeError, OSError):
41
- pass
42
- finally:
43
- try:
44
- dst.close()
45
- except OSError:
46
- pass
 
 
47
 
48
  await asyncio.gather(
49
  forward(reader, backend_writer),
@@ -57,12 +79,38 @@ async def handle_client(reader, writer):
57
  if not data:
58
  writer.close()
59
  return
60
- path = data.split(b" ", 2)[1] if data.count(b" ") >= 2 else b""
61
- path = path.split(b"?")[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  if path in (b"/", b"/health"):
63
- await health_check(reader, writer)
64
- else:
65
- await tcp_proxy(reader, writer, initial_data=data)
 
 
 
 
 
 
 
 
 
66
  except (ConnectionResetError, BrokenPipeError, OSError, asyncio.TimeoutError):
67
  pass
68
  finally:
@@ -72,33 +120,12 @@ async def handle_client(reader, writer):
72
  pass
73
 
74
 
75
- async def run_wstunnel():
76
- token = os.environ.get("PROXY_TOKEN", "")
77
- args = [WSTUNNEL_BIN, "server"]
78
- if token:
79
- args += ["--restrict-http-upgrade-path-prefix", token]
80
- args += [f"ws://0.0.0.0:{WSTUNNEL_PORT}"]
81
-
82
- proc = await asyncio.create_subprocess_exec(*args, stdout=sys.stderr, stderr=sys.stderr)
83
- return proc
84
-
85
-
86
  async def main():
87
- print(f"starting wstunnel on port {WSTUNNEL_PORT}...", flush=True)
88
- wstunnel_proc = await run_wstunnel()
89
- await asyncio.sleep(0.5)
90
-
91
- if wstunnel_proc.returncode is not None:
92
- print(f"ERROR: wstunnel exited with code {wstunnel_proc.returncode}", flush=True)
93
- sys.exit(1)
94
-
95
- print(f"starting TCP proxy on port {PROXY_PORT}...", flush=True)
96
  server = await asyncio.start_server(handle_client, "0.0.0.0", PROXY_PORT)
97
 
98
  async def shutdown():
99
  server.close()
100
- wstunnel_proc.terminate()
101
- await wstunnel_proc.wait()
102
 
103
  loop = asyncio.get_event_loop()
104
  for sig in (signal.SIGTERM, signal.SIGINT):
 
2
  import os
3
  import signal
4
  import sys
5
+ from urllib.parse import urlparse
6
 
 
7
  PROXY_PORT = 7860
 
8
 
9
 
10
+ async def health_check(writer):
11
  writer.write(b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nOK\n")
12
  await writer.drain()
13
  writer.close()
14
 
15
 
16
+ async def error(writer, status, message):
17
+ body = message.encode()
18
+ writer.write(f"HTTP/1.1 {status} {message}\r\nContent-Length: {len(body)}\r\n\r\n{message}\n".encode())
19
+ await writer.drain()
20
+ writer.close()
21
+
22
+
23
+ async def forward(src, dst):
24
+ try:
25
+ while True:
26
+ data = await src.read(65536)
27
+ if not data:
28
+ break
29
+ dst.write(data)
30
+ await dst.drain()
31
+ except (ConnectionResetError, BrokenPipeError, OSError):
32
+ pass
33
+ finally:
34
+ try:
35
+ dst.close()
36
+ except OSError:
37
+ pass
38
+
39
+
40
+ async def handle_tunnel(target_host, target_port, reader, writer):
41
  try:
42
  backend_reader, backend_writer = await asyncio.wait_for(
43
+ asyncio.open_connection(target_host, target_port), timeout=15
44
  )
45
  except (OSError, asyncio.TimeoutError):
46
+ await error(writer, 502, "Cannot connect to target")
 
 
47
  return
48
 
49
+ writer.write(b"HTTP/1.1 200 Connection Established\r\n\r\n")
50
+ await writer.drain()
 
51
 
52
+ await asyncio.gather(
53
+ forward(reader, backend_writer),
54
+ forward(backend_reader, writer),
55
+ )
56
+
57
+
58
+ async def forward_request(host, port, data, reader, writer):
59
+ try:
60
+ backend_reader, backend_writer = await asyncio.wait_for(
61
+ asyncio.open_connection(host, port), timeout=15
62
+ )
63
+ except (OSError, asyncio.TimeoutError):
64
+ await error(writer, 502, "Cannot connect to target")
65
+ return
66
+
67
+ backend_writer.write(data)
68
+ await backend_writer.drain()
69
 
70
  await asyncio.gather(
71
  forward(reader, backend_writer),
 
79
  if not data:
80
  writer.close()
81
  return
82
+
83
+ parts = data.split(b" ", 2)
84
+ if len(parts) < 2:
85
+ writer.close()
86
+ return
87
+
88
+ method = parts[0]
89
+ raw_path = parts[1]
90
+
91
+ if method == b"CONNECT":
92
+ target = raw_path.split(b":")
93
+ if len(target) == 2:
94
+ try:
95
+ await handle_tunnel(target[0].decode(), int(target[1]), reader, writer)
96
+ return
97
+ except (ValueError, UnicodeDecodeError):
98
+ pass
99
+
100
+ path = raw_path.split(b"?")[0]
101
  if path in (b"/", b"/health"):
102
+ await health_check(writer)
103
+ return
104
+
105
+ if raw_path.startswith(b"http://") or raw_path.startswith(b"https://"):
106
+ parsed = urlparse(raw_path.decode())
107
+ host = parsed.hostname
108
+ port = parsed.port or (443 if parsed.scheme == "https" else 80)
109
+ if host:
110
+ await forward_request(host, port, data, reader, writer)
111
+ return
112
+
113
+ await error(writer, 400, "Bad Request")
114
  except (ConnectionResetError, BrokenPipeError, OSError, asyncio.TimeoutError):
115
  pass
116
  finally:
 
120
  pass
121
 
122
 
 
 
 
 
 
 
 
 
 
 
 
123
  async def main():
124
+ print(f"starting proxy on port {PROXY_PORT}...", flush=True)
 
 
 
 
 
 
 
 
125
  server = await asyncio.start_server(handle_client, "0.0.0.0", PROXY_PORT)
126
 
127
  async def shutdown():
128
  server.close()
 
 
129
 
130
  loop = asyncio.get_event_loop()
131
  for sig in (signal.SIGTERM, signal.SIGINT):