block-proxy 0.1.15 → 0.1.16

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.
@@ -39,12 +39,13 @@ def show_config_window(config_path):
39
39
  config = json.load(f)
40
40
 
41
41
  def save_and_close():
42
+ config["server"]["protocol"] = protocol_var.get()
42
43
  config["server"]["address"] = entries["address"].get()
43
44
  config["server"]["port"] = int(entries["port"].get())
44
45
  config["server"]["username"] = entries["username"].get()
45
46
  config["server"]["password"] = entries["password"].get()
46
47
  config["server"]["tls"] = tls_var.get()
47
- config["server"]["allowInsecure"] = insecure_var.get() == "true"
48
+ config["server"]["allowInsecure"] = insecure_var.get()
48
49
  config["local"]["socks_port"] = int(entries["socks_port"].get())
49
50
  config["local"]["http_port"] = int(entries["http_port"].get())
50
51
  config["local"]["udp"] = udp_var.get()
@@ -55,12 +56,12 @@ def show_config_window(config_path):
55
56
  json.dump(config, f, indent=2)
56
57
  root.destroy()
57
58
 
58
- pos = _center_on_mouse_screen(400, 460)
59
+ pos = _center_on_mouse_screen(400, 490)
59
60
 
60
61
  root = tk.Tk()
61
- root.title("Socks 节点配置")
62
+ root.title("节点配置")
62
63
  root.resizable(False, False)
63
- w, h = 400, 460
64
+ w, h = 400, 490
64
65
  if pos:
65
66
  x, y = pos
66
67
  else:
@@ -76,6 +77,16 @@ def show_config_window(config_path):
76
77
  frame.grid_columnconfigure(1, weight=1)
77
78
 
78
79
  entries = {}
80
+ row = 0
81
+
82
+ ttk.Label(frame, text="协议:").grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
83
+ protocol_var = tk.StringVar(value=config["server"].get("protocol", "socks5"))
84
+ protocol_combo = ttk.Combobox(
85
+ frame, textvariable=protocol_var, values=["socks5", "http"], state="readonly", width=10
86
+ )
87
+ protocol_combo.grid(row=row, column=1, sticky="w", pady=4)
88
+ row += 1
89
+
79
90
  fields = [
80
91
  ("address", "地址:", config["server"]["address"]),
81
92
  ("port", "端口:", str(config["server"]["port"])),
@@ -85,44 +96,43 @@ def show_config_window(config_path):
85
96
  ("http_port", "本地HTTP端口:", str(config["local"]["http_port"])),
86
97
  ]
87
98
 
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))
99
+ for key, label, default in fields:
100
+ ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
90
101
  entry = ttk.Entry(frame)
91
102
  entry.insert(0, default)
92
- entry.grid(row=i, column=1, sticky="ew", pady=4)
103
+ entry.grid(row=row, column=1, sticky="ew", pady=4)
93
104
  entries[key] = entry
94
-
95
- row = len(fields)
105
+ row += 1
96
106
 
97
107
  tls_var = tk.BooleanVar(value=config["server"]["tls"])
98
108
  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
- )
109
+ tls_frame = ttk.Frame(frame)
110
+ tls_frame.grid(row=row, column=1, sticky="w", pady=4)
111
+ ttk.Checkbutton(tls_frame, variable=tls_var).pack(side="left")
112
+ ttk.Label(tls_frame, text="(需节点服务器支持)", foreground="gray").pack(side="left")
102
113
  row += 1
103
114
 
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)
115
+ insecure_var = tk.BooleanVar(value=config["server"]["allowInsecure"])
116
+ ttk.Label(frame, text="允许不安全连接:").grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
117
+ insecure_frame = ttk.Frame(frame)
118
+ insecure_frame.grid(row=row, column=1, sticky="w", pady=4)
119
+ ttk.Checkbutton(insecure_frame, variable=insecure_var).pack(side="left")
120
+ ttk.Label(insecure_frame, text="(跳过证书验证)", foreground="gray").pack(side="left")
112
121
  row += 1
113
122
 
114
123
  udp_var = tk.BooleanVar(value=config["local"]["udp"])
115
124
  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
- )
125
+ udp_frame = ttk.Frame(frame)
126
+ udp_frame.grid(row=row, column=1, sticky="w", pady=4)
127
+ ttk.Checkbutton(udp_frame, variable=udp_var).pack(side="left")
119
128
  row += 1
120
129
 
121
130
  proxy_private_var = tk.BooleanVar(value=config["local"].get("proxy_private", False))
122
131
  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
- )
132
+ private_frame = ttk.Frame(frame)
133
+ private_frame.grid(row=row, column=1, sticky="w", pady=4)
134
+ ttk.Checkbutton(private_frame, variable=proxy_private_var).pack(side="left")
135
+ ttk.Label(private_frame, text="(192.168.x / 172.16.x / 10.x)", foreground="gray").pack(side="left")
126
136
  row += 1
127
137
 
128
138
  ttk.Separator(frame, orient="horizontal").grid(
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -29,6 +29,21 @@ def is_private_ip(host):
29
29
 
30
30
 
31
31
  RELAY_IDLE_TIMEOUT = 300
32
+ UDP_IDLE_TIMEOUT = 120
33
+
34
+
35
+ async def write_udp_frame(writer, data):
36
+ frame = struct.pack("!H", len(data)) + data
37
+ writer.write(frame)
38
+ await writer.drain()
39
+
40
+
41
+ async def read_udp_frame(reader):
42
+ length_data = await reader.readexactly(2)
43
+ length = struct.unpack("!H", length_data)[0]
44
+ if length == 0 or length > 65535:
45
+ raise Exception("invalid UDP frame length")
46
+ return await reader.readexactly(length)
32
47
 
33
48
 
34
49
  async def relay(reader, writer):
@@ -57,6 +72,7 @@ async def relay(reader, writer):
57
72
 
58
73
  CONNECT_TIMEOUT = 10
59
74
  HANDSHAKE_TIMEOUT = 10
75
+ LOCAL_HANDSHAKE_TIMEOUT = 30
60
76
 
61
77
 
62
78
  async def connect_upstream_socks5(server_config, dest_addr, dest_port, ssl_ctx=None):
@@ -129,7 +145,138 @@ async def connect_upstream_socks5(server_config, dest_addr, dest_port, ssl_ctx=N
129
145
  elif reply[3] == 0x04:
130
146
  await reader.readexactly(16 + 2)
131
147
 
132
- await asyncio.wait_for(_handshake(), timeout=HANDSHAKE_TIMEOUT)
148
+ try:
149
+ await asyncio.wait_for(_handshake(), timeout=HANDSHAKE_TIMEOUT)
150
+ except Exception:
151
+ writer.close()
152
+ try:
153
+ await writer.wait_closed()
154
+ except OSError:
155
+ pass
156
+ raise
157
+
158
+ return reader, writer
159
+
160
+
161
+ async def connect_upstream_http(server_config, dest_addr, dest_port, ssl_ctx=None):
162
+ host = server_config["address"]
163
+ port = server_config["port"]
164
+ username = server_config["username"]
165
+ password = server_config["password"]
166
+ use_tls = server_config["tls"]
167
+
168
+ reader, writer = await asyncio.wait_for(
169
+ asyncio.open_connection(
170
+ host, port, ssl=ssl_ctx if use_tls else None,
171
+ server_hostname=host if use_tls else None,
172
+ ),
173
+ timeout=CONNECT_TIMEOUT,
174
+ )
175
+
176
+ async def _handshake():
177
+ target = f"{dest_addr}:{dest_port}"
178
+ lines = [f"CONNECT {target} HTTP/1.1", f"Host: {target}"]
179
+ if username and password:
180
+ import base64
181
+ cred = base64.b64encode(f"{username}:{password}".encode()).decode()
182
+ lines.append(f"Proxy-Authorization: Basic {cred}")
183
+ lines.append("")
184
+ lines.append("")
185
+ writer.write("\r\n".join(lines).encode())
186
+ await writer.drain()
187
+
188
+ status_line = await reader.readline()
189
+ if not status_line:
190
+ raise Exception("HTTP proxy closed connection")
191
+ parts = status_line.decode().split(" ", 2)
192
+ if len(parts) < 2 or not parts[1].startswith("2"):
193
+ raise Exception(f"HTTP proxy CONNECT failed: {status_line.decode().strip()}")
194
+ while True:
195
+ line = await reader.readline()
196
+ if line in (b"\r\n", b"\n", b""):
197
+ break
198
+
199
+ try:
200
+ await asyncio.wait_for(_handshake(), timeout=HANDSHAKE_TIMEOUT)
201
+ except Exception:
202
+ writer.close()
203
+ try:
204
+ await writer.wait_closed()
205
+ except OSError:
206
+ pass
207
+ raise
208
+
209
+ return reader, writer
210
+
211
+
212
+ async def connect_upstream_udp_associate(server_config, ssl_ctx=None):
213
+ host = server_config["address"]
214
+ port = server_config["port"]
215
+ username = server_config["username"]
216
+ password = server_config["password"]
217
+ use_tls = server_config["tls"]
218
+
219
+ reader, writer = await asyncio.wait_for(
220
+ asyncio.open_connection(
221
+ host, port, ssl=ssl_ctx if use_tls else None,
222
+ server_hostname=host if use_tls else None,
223
+ ),
224
+ timeout=CONNECT_TIMEOUT,
225
+ )
226
+
227
+ async def _handshake():
228
+ if username and password:
229
+ writer.write(b"\x05\x01\x02")
230
+ else:
231
+ writer.write(b"\x05\x01\x00")
232
+ await writer.drain()
233
+
234
+ resp = await reader.readexactly(2)
235
+ if resp[0] != 0x05:
236
+ raise Exception("SOCKS5 version mismatch")
237
+
238
+ if resp[1] == 0x02:
239
+ uname = username.encode("utf-8")
240
+ passwd = password.encode("utf-8")
241
+ writer.write(
242
+ b"\x01"
243
+ + struct.pack("B", len(uname))
244
+ + uname
245
+ + struct.pack("B", len(passwd))
246
+ + passwd
247
+ )
248
+ await writer.drain()
249
+ auth_resp = await reader.readexactly(2)
250
+ if auth_resp[1] != 0x00:
251
+ raise Exception("SOCKS5 auth failed")
252
+ elif resp[1] == 0xFF:
253
+ raise Exception("SOCKS5 no acceptable auth method")
254
+
255
+ # CMD=0x03 UDP ASSOCIATE, DST.ADDR=0.0.0.0:0
256
+ writer.write(b"\x05\x03\x00\x01" + b"\x00" * 4 + b"\x00\x00")
257
+ await writer.drain()
258
+
259
+ reply = await reader.readexactly(4)
260
+ if reply[1] != 0x00:
261
+ raise Exception(f"SOCKS5 UDP ASSOCIATE failed: {reply[1]:#x}")
262
+
263
+ if reply[3] == 0x01:
264
+ await reader.readexactly(4 + 2)
265
+ elif reply[3] == 0x03:
266
+ length = (await reader.readexactly(1))[0]
267
+ await reader.readexactly(length + 2)
268
+ elif reply[3] == 0x04:
269
+ await reader.readexactly(16 + 2)
270
+
271
+ try:
272
+ await asyncio.wait_for(_handshake(), timeout=HANDSHAKE_TIMEOUT)
273
+ except Exception:
274
+ writer.close()
275
+ try:
276
+ await writer.wait_closed()
277
+ except OSError:
278
+ pass
279
+ raise
133
280
 
134
281
  return reader, writer
135
282
 
@@ -141,6 +288,24 @@ async def connect_direct(dest_addr, dest_port):
141
288
  MAX_CONCURRENT = 256
142
289
 
143
290
 
291
+ class _UdpRelayProtocol(asyncio.DatagramProtocol):
292
+ def __init__(self, tcp_writer, loop):
293
+ self._tcp_writer = tcp_writer
294
+ self._loop = loop
295
+ self.client_addr = None
296
+ self.transport = None
297
+
298
+ def connection_made(self, transport):
299
+ self.transport = transport
300
+
301
+ def datagram_received(self, data, addr):
302
+ self.client_addr = addr
303
+ if self._tcp_writer.is_closing():
304
+ return
305
+ frame = struct.pack("!H", len(data)) + data
306
+ self._tcp_writer.write(frame)
307
+
308
+
144
309
  class ProxyCore:
145
310
  def __init__(self):
146
311
  self._loop = None
@@ -150,6 +315,7 @@ class ProxyCore:
150
315
  self._running = False
151
316
  self._server_config = None
152
317
  self._proxy_private = False
318
+ self._udp_enabled = True
153
319
  self._socks_port = 1080
154
320
  self._http_port = 1087
155
321
  self._ssl_ctx = None
@@ -175,6 +341,7 @@ class ProxyCore:
175
341
  self._socks_port = local["socks_port"]
176
342
  self._http_port = local["http_port"]
177
343
  self._proxy_private = local.get("proxy_private", False)
344
+ self._udp_enabled = local.get("udp", True)
178
345
  self._build_ssl_context()
179
346
 
180
347
  self._loop = asyncio.new_event_loop()
@@ -202,15 +369,25 @@ class ProxyCore:
202
369
  self._loop.stop()
203
370
  self._loop.call_soon_threadsafe(_shutdown)
204
371
  if self._thread:
205
- self._thread.join(timeout=2)
206
- if self._loop:
207
- self._loop.close()
372
+ self._thread.join(timeout=5)
373
+ if not self._thread.is_alive() and self._loop:
374
+ self._loop.close()
375
+ elif self._loop:
376
+ logger.warning("proxy thread did not exit in time, skipping loop.close()")
208
377
  self._loop = None
209
378
  self._thread = None
210
379
 
211
380
  def is_running(self):
212
381
  return self._running and self._thread is not None and self._thread.is_alive()
213
382
 
383
+ @property
384
+ def socks_port(self):
385
+ return self._socks_port
386
+
387
+ @property
388
+ def http_port(self):
389
+ return self._http_port
390
+
214
391
  async def _measure_latency(self):
215
392
  import time
216
393
  start = time.monotonic()
@@ -280,12 +457,26 @@ class ProxyCore:
280
457
 
281
458
  async def _start_servers(self):
282
459
  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
- )
460
+ max_attempts = 100
461
+ for attempt in range(max_attempts):
462
+ try:
463
+ self._socks_server = await asyncio.start_server(
464
+ self._handle_socks, "127.0.0.1", self._socks_port
465
+ )
466
+ self._http_server = await asyncio.start_server(
467
+ self._handle_http, "127.0.0.1", self._http_port
468
+ )
469
+ return
470
+ except OSError as e:
471
+ if e.errno == 48 and attempt < max_attempts - 1:
472
+ if self._socks_server:
473
+ self._socks_server.close()
474
+ await self._socks_server.wait_closed()
475
+ self._socks_server = None
476
+ self._socks_port += 1
477
+ self._http_port += 1
478
+ else:
479
+ raise
289
480
 
290
481
  async def _stop_servers(self):
291
482
  if self._socks_server:
@@ -303,6 +494,11 @@ class ProxyCore:
303
494
  async def _connect_target(self, dest_addr, dest_port):
304
495
  if self._should_direct(dest_addr):
305
496
  return await connect_direct(dest_addr, dest_port)
497
+ protocol = self._server_config.get("protocol", "socks5")
498
+ if protocol == "http":
499
+ return await connect_upstream_http(
500
+ self._server_config, dest_addr, dest_port, ssl_ctx=self._ssl_ctx
501
+ )
306
502
  return await connect_upstream_socks5(
307
503
  self._server_config, dest_addr, dest_port, ssl_ctx=self._ssl_ctx
308
504
  )
@@ -313,19 +509,23 @@ class ProxyCore:
313
509
 
314
510
  async def _do_handle_socks(self, client_reader, client_writer):
315
511
  try:
316
- header = await client_reader.readexactly(2)
512
+ header = await asyncio.wait_for(client_reader.readexactly(2), timeout=LOCAL_HANDSHAKE_TIMEOUT)
317
513
  ver, nmethods = header
318
514
  if ver != 0x05:
319
515
  client_writer.close()
320
516
  return
321
- await client_reader.readexactly(nmethods)
517
+ await asyncio.wait_for(client_reader.readexactly(nmethods), timeout=LOCAL_HANDSHAKE_TIMEOUT)
322
518
 
323
519
  client_writer.write(b"\x05\x00")
324
520
  await client_writer.drain()
325
521
 
326
- req = await client_reader.readexactly(4)
522
+ req = await asyncio.wait_for(client_reader.readexactly(4), timeout=LOCAL_HANDSHAKE_TIMEOUT)
327
523
  ver, cmd, _, atyp = req
328
524
 
525
+ if cmd == 0x03:
526
+ await self._handle_udp_associate(client_reader, client_writer, atyp)
527
+ return
528
+
329
529
  if cmd != 0x01:
330
530
  client_writer.write(
331
531
  b"\x05\x07\x00\x01" + b"\x00" * 4 + b"\x00\x00"
@@ -335,13 +535,13 @@ class ProxyCore:
335
535
  return
336
536
 
337
537
  if atyp == 0x01:
338
- raw = await client_reader.readexactly(4)
538
+ raw = await asyncio.wait_for(client_reader.readexactly(4), timeout=LOCAL_HANDSHAKE_TIMEOUT)
339
539
  dest_addr = str(ipaddress.IPv4Address(raw))
340
540
  elif atyp == 0x03:
341
- length = (await client_reader.readexactly(1))[0]
342
- dest_addr = (await client_reader.readexactly(length)).decode("utf-8")
541
+ length = (await asyncio.wait_for(client_reader.readexactly(1), timeout=LOCAL_HANDSHAKE_TIMEOUT))[0]
542
+ dest_addr = (await asyncio.wait_for(client_reader.readexactly(length), timeout=LOCAL_HANDSHAKE_TIMEOUT)).decode("utf-8")
343
543
  elif atyp == 0x04:
344
- raw = await client_reader.readexactly(16)
544
+ raw = await asyncio.wait_for(client_reader.readexactly(16), timeout=LOCAL_HANDSHAKE_TIMEOUT)
345
545
  dest_addr = str(ipaddress.IPv6Address(raw))
346
546
  else:
347
547
  client_writer.write(
@@ -351,7 +551,7 @@ class ProxyCore:
351
551
  client_writer.close()
352
552
  return
353
553
 
354
- port_data = await client_reader.readexactly(2)
554
+ port_data = await asyncio.wait_for(client_reader.readexactly(2), timeout=LOCAL_HANDSHAKE_TIMEOUT)
355
555
  dest_port = struct.unpack("!H", port_data)[0]
356
556
 
357
557
  remote_reader, remote_writer = await self._connect_target(
@@ -376,13 +576,97 @@ class ProxyCore:
376
576
  except OSError:
377
577
  pass
378
578
 
579
+ async def _handle_udp_associate(self, client_reader, client_writer, atyp):
580
+ # 消费掉请求中剩余的地址和端口字段
581
+ try:
582
+ if atyp == 0x01:
583
+ await asyncio.wait_for(client_reader.readexactly(4 + 2), timeout=LOCAL_HANDSHAKE_TIMEOUT)
584
+ elif atyp == 0x03:
585
+ length = (await asyncio.wait_for(client_reader.readexactly(1), timeout=LOCAL_HANDSHAKE_TIMEOUT))[0]
586
+ await asyncio.wait_for(client_reader.readexactly(length + 2), timeout=LOCAL_HANDSHAKE_TIMEOUT)
587
+ elif atyp == 0x04:
588
+ await asyncio.wait_for(client_reader.readexactly(16 + 2), timeout=LOCAL_HANDSHAKE_TIMEOUT)
589
+ except Exception:
590
+ client_writer.close()
591
+ return
592
+
593
+ protocol = self._server_config.get("protocol", "socks5")
594
+ udp_enabled = getattr(self, "_udp_enabled", True)
595
+ if protocol != "socks5" or not udp_enabled:
596
+ client_writer.write(b"\x05\x07\x00\x01" + b"\x00" * 4 + b"\x00\x00")
597
+ await client_writer.drain()
598
+ client_writer.close()
599
+ return
600
+
601
+ try:
602
+ remote_reader, remote_writer = await connect_upstream_udp_associate(
603
+ self._server_config, ssl_ctx=self._ssl_ctx
604
+ )
605
+ except Exception:
606
+ client_writer.write(b"\x05\x05\x00\x01" + b"\x00" * 4 + b"\x00\x00")
607
+ await client_writer.drain()
608
+ client_writer.close()
609
+ return
610
+
611
+ loop = asyncio.get_event_loop()
612
+ transport, udp_relay = await loop.create_datagram_endpoint(
613
+ lambda: _UdpRelayProtocol(remote_writer, loop),
614
+ local_addr=("127.0.0.1", 0),
615
+ )
616
+ relay_addr = transport.get_extra_info("sockname")
617
+ relay_port = relay_addr[1]
618
+
619
+ # 回复客户端 UDP relay 地址
620
+ reply = b"\x05\x00\x00\x01\x7f\x00\x00\x01" + struct.pack("!H", relay_port)
621
+ client_writer.write(reply)
622
+ await client_writer.drain()
623
+
624
+ async def _tcp_to_udp():
625
+ try:
626
+ while True:
627
+ frame_data = await asyncio.wait_for(
628
+ read_udp_frame(remote_reader), timeout=UDP_IDLE_TIMEOUT
629
+ )
630
+ if udp_relay.client_addr:
631
+ transport.sendto(frame_data, udp_relay.client_addr)
632
+ except (asyncio.TimeoutError, asyncio.IncompleteReadError,
633
+ ConnectionResetError, BrokenPipeError, OSError):
634
+ pass
635
+
636
+ async def _wait_control_close():
637
+ try:
638
+ await client_reader.read(1)
639
+ except (ConnectionResetError, BrokenPipeError, OSError):
640
+ pass
641
+
642
+ tasks = [
643
+ asyncio.ensure_future(_tcp_to_udp()),
644
+ asyncio.ensure_future(_wait_control_close()),
645
+ ]
646
+ try:
647
+ await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
648
+ for t in tasks:
649
+ t.cancel()
650
+ finally:
651
+ transport.close()
652
+ remote_writer.close()
653
+ try:
654
+ await remote_writer.wait_closed()
655
+ except OSError:
656
+ pass
657
+ try:
658
+ client_writer.close()
659
+ await client_writer.wait_closed()
660
+ except OSError:
661
+ pass
662
+
379
663
  async def _handle_http(self, client_reader, client_writer):
380
664
  async with self._semaphore:
381
665
  await self._do_handle_http(client_reader, client_writer)
382
666
 
383
667
  async def _do_handle_http(self, client_reader, client_writer):
384
668
  try:
385
- raw_line = await client_reader.readline()
669
+ raw_line = await asyncio.wait_for(client_reader.readline(), timeout=LOCAL_HANDSHAKE_TIMEOUT)
386
670
  if not raw_line:
387
671
  client_writer.close()
388
672
  return
@@ -404,7 +688,7 @@ class ProxyCore:
404
688
  port = 443
405
689
 
406
690
  while True:
407
- header_line = await client_reader.readline()
691
+ header_line = await asyncio.wait_for(client_reader.readline(), timeout=LOCAL_HANDSHAKE_TIMEOUT)
408
692
  if header_line in (b"\r\n", b"\n", b""):
409
693
  break
410
694
 
@@ -442,7 +726,7 @@ class ProxyCore:
442
726
 
443
727
  headers = []
444
728
  while True:
445
- header_line = await client_reader.readline()
729
+ header_line = await asyncio.wait_for(client_reader.readline(), timeout=LOCAL_HANDSHAKE_TIMEOUT)
446
730
  if header_line in (b"\r\n", b"\n", b""):
447
731
  break
448
732
  headers.append(header_line)