wormhole-proxy 3.2.0__tar.gz → 3.2.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: wormhole-proxy
3
- Version: 3.2.0
3
+ Version: 3.2.2
4
4
  Summary: Asynchronous I/O HTTP and HTTPS Proxy on Python >= 3.11
5
5
  License: MIT
6
6
  Keywords: wormhole,asynchronous,web,proxy
@@ -1,7 +1,7 @@
1
1
  # Main project metadata (PEP 621 standard)
2
2
  [project]
3
3
  name = "wormhole-proxy"
4
- version = "3.2.0"
4
+ version = "3.2.2"
5
5
  description = "Asynchronous I/O HTTP and HTTPS Proxy on Python >= 3.11"
6
6
  readme = "README.md"
7
7
  authors = [
@@ -28,7 +28,7 @@ def _secure_create_file(path: Path) -> bool:
28
28
  if sys.platform == "win32":
29
29
  # On Windows, we rely on default user permissions and warn if the filesystem is not NTFS.
30
30
  try:
31
- import win32api
31
+ import win32api # noqa
32
32
 
33
33
  fs_type = win32api.GetVolumeInformation(
34
34
  str(path.resolve().drive) + "\\"
@@ -1,4 +1,4 @@
1
- from .logger import logger
1
+ from .logger import logger, format_log_message as flm
2
2
  from .safeguards import has_public_ipv6, is_ad_domain, is_private_ip
3
3
  from .tools import get_content_length, get_host_and_port
4
4
  import asyncio
@@ -19,6 +19,7 @@ async def relay_stream(
19
19
  writer: asyncio.StreamWriter,
20
20
  ident: dict[str, str],
21
21
  return_first_line: bool = False,
22
+ verbose: int = 0,
22
23
  ) -> bytes | None:
23
24
  """
24
25
  Relays data between a reader and a writer stream until EOF.
@@ -46,13 +47,13 @@ async def relay_stream(
46
47
  writer.write(data)
47
48
  await writer.drain()
48
49
  except (ConnectionResetError, BrokenPipeError) as e:
49
- logger.debug(
50
- f"[{ident['id']}][{ident['client']}]: Relay network error: {e}"
51
- )
50
+ logger.debug(flm(f"Relay network error: {e}", ident, verbose))
52
51
  except Exception as e:
53
- logger.exception(
54
- f"[{ident['id']}][{ident['client']}]: Unexpected relay error: {e}",
55
- )
52
+ msg = flm(f"Unexpected relay error: {e}", ident, verbose)
53
+ if verbose > 2: # Show full traceback only for -vv
54
+ logger.exception(msg)
55
+ else:
56
+ logger.error(msg)
56
57
  finally:
57
58
  if not writer.is_closing():
58
59
  writer.close()
@@ -61,7 +62,7 @@ async def relay_stream(
61
62
 
62
63
 
63
64
  async def _resolve_and_validate_host(
64
- host: str, allow_private: bool
65
+ host: str, ident: dict[str, str], allow_private: bool, verbose: int = 0
65
66
  ) -> list[str]:
66
67
  """
67
68
  Resolves a hostname to a list of IPs, validates them, and caches the list.
@@ -72,14 +73,18 @@ async def _resolve_and_validate_host(
72
73
  """
73
74
  # Ad-block check
74
75
  if is_ad_domain(host):
75
- raise PermissionError(f"Blocked ad domain: {host}")
76
+ raise PermissionError(f"Blocked ad domain")
76
77
 
77
78
  # Check cache first
78
79
  if host in DNS_CACHE:
79
80
  ip_list, timestamp = DNS_CACHE[host]
80
81
  if time.time() - timestamp < DNS_CACHE_TTL:
81
82
  logger.debug(
82
- f"DNS cache hit for '{host}'. ({len(DNS_CACHE)} hosts cached)"
83
+ flm(
84
+ f"DNS cache hit for '{host}'. ({len(DNS_CACHE)} hosts cached)",
85
+ ident,
86
+ verbose,
87
+ )
83
88
  )
84
89
  return ip_list
85
90
 
@@ -109,7 +114,11 @@ async def _resolve_and_validate_host(
109
114
  final_ip_list = []
110
115
  if has_public_ipv6() and valid_ipv6s:
111
116
  logger.debug(
112
- f"Host has public IPv6. Prioritizing {len(valid_ipv6s)} IPv6 addresses."
117
+ flm(
118
+ f"Host has public IPv6. Prioritizing {len(valid_ipv6s)} IPv6 addresses.",
119
+ ident,
120
+ verbose,
121
+ )
113
122
  )
114
123
  random.shuffle(valid_ipv6s)
115
124
  final_ip_list.extend(valid_ipv6s)
@@ -131,19 +140,30 @@ async def _resolve_and_validate_host(
131
140
  # Update cache
132
141
  DNS_CACHE[host] = (final_ip_list, time.time())
133
142
  logger.debug(
134
- f"DNS cache miss for '{host}'. Resolved to {final_ip_list}. Caching. ({len(DNS_CACHE)} hosts cached)"
143
+ flm(
144
+ (
145
+ f"DNS cache miss for '{host}'. "
146
+ f"Resolved to {final_ip_list}. Caching. "
147
+ f"({len(DNS_CACHE)} hosts cached)"
148
+ ),
149
+ ident,
150
+ verbose,
151
+ )
135
152
  )
136
153
  return final_ip_list
137
154
 
138
155
 
139
156
  async def _create_connection_with_retries(
140
- ip_list: list[str], port: int, ident: dict[str, str]
157
+ ip_list: list[str],
158
+ port: int,
159
+ ident: dict[str, str],
160
+ max_attempts: int = 3,
161
+ timeout: int = 5,
162
+ verbose: int = 0,
141
163
  ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
142
164
  """
143
165
  Tries to connect to a list of IPs with a fast timeout and retry mechanism.
144
166
  """
145
- max_attempts = 3
146
- timeout = 5 # seconds
147
167
  last_error = None
148
168
 
149
169
  # Create a list of connection targets to try, ensuring we don't exceed max_attempts
@@ -154,7 +174,11 @@ async def _create_connection_with_retries(
154
174
  for i, ip in enumerate(targets_to_try):
155
175
  attempt = i + 1
156
176
  logger.debug(
157
- f"Connection attempt {attempt}/{max_attempts} to {ip}:{port}"
177
+ flm(
178
+ f"Connection attempt {attempt}/{max_attempts} to {ip}:{port}",
179
+ ident,
180
+ verbose,
181
+ )
158
182
  )
159
183
  try:
160
184
  # Use a short timeout for each connection attempt
@@ -162,13 +186,21 @@ async def _create_connection_with_retries(
162
186
  asyncio.open_connection(ip, port), timeout=timeout
163
187
  )
164
188
  logger.debug(
165
- f"Successfully connected to {ip}:{port} on attempt {attempt}"
189
+ flm(
190
+ f"Successfully connected to {ip}:{port} on attempt {attempt}",
191
+ ident,
192
+ verbose,
193
+ )
166
194
  )
167
195
  return reader, writer
168
196
  except (OSError, asyncio.TimeoutError) as e:
169
197
  last_error = e
170
198
  logger.warning(
171
- f"Connection to {ip}:{port} failed on attempt {attempt}: {e}"
199
+ flm(
200
+ f"Connection to {ip}:{port} failed on attempt {attempt}: {e}",
201
+ ident,
202
+ verbose,
203
+ )
172
204
  )
173
205
 
174
206
  raise OSError(
@@ -186,6 +218,7 @@ async def process_https_tunnel(
186
218
  uri: str,
187
219
  ident: dict[str, str],
188
220
  allow_private: bool,
221
+ verbose: int = 0,
189
222
  ) -> None:
190
223
  """Establishes an HTTPS tunnel and relays data between client and server."""
191
224
  host, port = get_host_and_port(uri)
@@ -194,11 +227,13 @@ async def process_https_tunnel(
194
227
 
195
228
  try:
196
229
  # Resolve and validate the host to get a list of potential IPs.
197
- ip_list = await _resolve_and_validate_host(host, allow_private)
230
+ ip_list = await _resolve_and_validate_host(
231
+ host, ident, allow_private, verbose
232
+ )
198
233
 
199
234
  # Attempt to connect to one of the IPs with retry logic.
200
235
  server_reader, server_writer = await _create_connection_with_retries(
201
- ip_list, port, ident
236
+ ip_list, port, ident, verbose=verbose
202
237
  )
203
238
 
204
239
  # Signal the client that the tunnel is established.
@@ -207,21 +242,30 @@ async def process_https_tunnel(
207
242
 
208
243
  # Use a TaskGroup for structured concurrency to relay data in both directions.
209
244
  async with asyncio.TaskGroup() as tg:
210
- tg.create_task(relay_stream(client_reader, server_writer, ident))
211
- tg.create_task(relay_stream(server_reader, client_writer, ident))
245
+ tg.create_task(
246
+ relay_stream(
247
+ client_reader, server_writer, ident, verbose=verbose
248
+ )
249
+ )
250
+ tg.create_task(
251
+ relay_stream(
252
+ server_reader, client_writer, ident, verbose=verbose
253
+ )
254
+ )
212
255
 
213
- logger.info(f"[{ident['id']}][{ident['client']}]: {method} 200 {uri}")
256
+ logger.info(flm(f"{method} 200 {uri}", ident, verbose))
214
257
 
215
258
  except PermissionError as e:
216
- logger.warning(
217
- f"[{ident['id']}][{ident['client']}]: {method} 403 {uri} ({e})"
218
- )
259
+ logger.warning(flm(f"{method} 403 {uri} ({e})", ident, verbose))
219
260
  client_writer.write(b"HTTP/1.1 403 Forbidden\r\n\r\n")
220
261
  await client_writer.drain()
221
262
  except Exception as e:
222
- logger.exception(
223
- f"[{ident['id']}][{ident['client']}]: {method} 502 {uri} ({e})"
224
- )
263
+ msg = flm(f"{method} 502 {uri} ({e})", ident, verbose)
264
+ if verbose > 2: # Show full traceback only for -vv
265
+ logger.exception(msg)
266
+ else:
267
+ logger.error(msg)
268
+
225
269
  finally:
226
270
  # Ensure server streams are closed if they were opened.
227
271
  if server_writer and not server_writer.is_closing():
@@ -238,6 +282,8 @@ async def _send_http_request(
238
282
  headers: list[str],
239
283
  payload: bytes,
240
284
  ident: dict[str, str],
285
+ max_attempts: int = 3,
286
+ verbose: int = 0,
241
287
  ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
242
288
  """Helper function to connect and send an HTTP request."""
243
289
  request_line = f"{method} {path or '/'} {version}".encode()
@@ -245,7 +291,7 @@ async def _send_http_request(
245
291
 
246
292
  # Attempt to connect to one of the IPs with retry logic.
247
293
  server_reader, server_writer = await _create_connection_with_retries(
248
- ip_list, port, ident
294
+ ip_list, port, ident, max_attempts, verbose=verbose
249
295
  )
250
296
 
251
297
  server_writer.write(request_line + b"\r\n" + headers_bytes + b"\r\n\r\n")
@@ -265,6 +311,8 @@ async def process_http_request(
265
311
  payload: bytes,
266
312
  ident: dict[str, str],
267
313
  allow_private: bool,
314
+ max_attempts: int = 3,
315
+ verbose: int = 0,
268
316
  ) -> None:
269
317
  """Processes a standard HTTP request by forwarding it to the target server."""
270
318
  server_reader = None
@@ -300,12 +348,18 @@ async def process_http_request(
300
348
  return
301
349
 
302
350
  # Resolve and validate the host to get a list of potential IPs.
303
- ip_list = await _resolve_and_validate_host(host, allow_private)
351
+ ip_list = await _resolve_and_validate_host(
352
+ host, ident, allow_private, verbose
353
+ )
304
354
 
305
355
  # --- Attempt to upgrade to HTTP/1.1 if needed ---
306
356
  if version == "HTTP/1.0":
307
357
  logger.debug(
308
- f"[{ident['id']}][{ident['client']}]: Attempting to upgrade HTTP/1.0 request for {host_header} to HTTP/1.1"
358
+ flm(
359
+ f"Attempting to upgrade HTTP/1.0 request for {host_header} to HTTP/1.1",
360
+ ident,
361
+ verbose,
362
+ )
309
363
  )
310
364
 
311
365
  # Prepare headers for HTTP/1.1
@@ -332,10 +386,16 @@ async def process_http_request(
332
386
  headers_v1_1,
333
387
  payload,
334
388
  ident,
389
+ max_attempts,
390
+ verbose,
335
391
  )
336
392
  except Exception as e:
337
393
  logger.warning(
338
- f"[{ident['id']}][{ident['client']}]: HTTP/1.1 upgrade failed ({e}). Falling back to HTTP/1.0."
394
+ flm(
395
+ f"HTTP/1.1 upgrade failed ({e}). Falling back to HTTP/1.0.",
396
+ ident,
397
+ verbose,
398
+ )
339
399
  )
340
400
  if server_writer and not server_writer.is_closing():
341
401
  server_writer.close()
@@ -361,6 +421,8 @@ async def process_http_request(
361
421
  original_headers,
362
422
  payload,
363
423
  ident,
424
+ max_attempts,
425
+ verbose,
364
426
  )
365
427
  else:
366
428
  # Original request was already HTTP/1.1 or newer
@@ -385,11 +447,17 @@ async def process_http_request(
385
447
  final_headers,
386
448
  payload,
387
449
  ident,
450
+ max_attempts,
451
+ verbose,
388
452
  )
389
453
 
390
454
  # Relay the server's response back to the client.
391
455
  response_status_line = await relay_stream(
392
- server_reader, client_writer, ident, return_first_line=True
456
+ server_reader,
457
+ client_writer,
458
+ ident,
459
+ return_first_line=True,
460
+ verbose=verbose,
393
461
  )
394
462
 
395
463
  # Log the outcome.
@@ -398,26 +466,24 @@ async def process_http_request(
398
466
  if response_status_line
399
467
  else 502
400
468
  )
401
- logger.info(
402
- f"[{ident['id']}][{ident['client']}]: {method} {response_code} {uri}"
403
- )
469
+ logger.info(flm(f"{method} {response_code} {uri}", ident, verbose))
404
470
 
405
471
  except PermissionError as e:
406
- logger.warning(
407
- f"[{ident['id']}][{ident['client']}]: {method} 403 {uri} ({e})"
408
- )
472
+ logger.warning(flm(f"{method} 403 {uri} ({e})", ident, verbose))
409
473
  client_writer.write(b"HTTP/1.1 403 Forbidden\r\n\r\n")
410
474
  await client_writer.drain()
411
475
  except Exception as e:
412
- logger.exception(
413
- f"[{ident['id']}][{ident['client']}]: {method} 502 {uri} ({e})"
414
- )
476
+ msg = flm(f"{method} 502 {uri} ({e})", ident, verbose)
477
+ if verbose > 2: # Show full traceback only for -vv
478
+ logger.exception(msg)
479
+ else:
480
+ logger.error(msg)
415
481
  if not client_writer.is_closing():
416
482
  try:
417
483
  client_writer.write(b"HTTP/1.1 502 Bad Gateway\r\n\r\n")
418
484
  await client_writer.drain()
419
485
  except ConnectionError:
420
- pass
486
+ pass # Ignore if client is already closed
421
487
  finally:
422
488
  if server_writer and not server_writer.is_closing():
423
489
  server_writer.close()
@@ -425,7 +491,7 @@ async def process_http_request(
425
491
 
426
492
 
427
493
  async def parse_request(
428
- client_reader: asyncio.StreamReader, max_retry: int, ident: dict[str, str]
494
+ client_reader: asyncio.StreamReader, ident: dict[str, str], verbose: int = 0
429
495
  ) -> tuple[str, list[str], bytes] | tuple[None, None, None]:
430
496
  """
431
497
  Parses the initial request from the client.
@@ -443,7 +509,7 @@ async def parse_request(
443
509
  )
444
510
  except (asyncio.IncompleteReadError, asyncio.TimeoutError) as e:
445
511
  logger.debug(
446
- f"[{ident['id']}][{ident['client']}]: Failed to read initial request: {e}"
512
+ flm(f"Failed to read initial request: {e}", ident, verbose)
447
513
  )
448
514
  return None, None, None
449
515
 
@@ -459,9 +525,7 @@ async def parse_request(
459
525
  try:
460
526
  payload = await client_reader.readexactly(content_length)
461
527
  except asyncio.IncompleteReadError:
462
- logger.debug(
463
- f"[{ident['id']}][{ident['client']}]: Incomplete payload read."
464
- )
528
+ logger.debug(flm(f"Incomplete payload read.", ident, verbose))
465
529
  return None, None, None
466
530
 
467
531
  return request_line, headers, payload
@@ -0,0 +1,136 @@
1
+ from loguru import logger
2
+ import asyncio
3
+ import logging.handlers
4
+ import os
5
+ import sys
6
+
7
+
8
+ class LogThrottler:
9
+ """A class to throttle and summarize repeated log messages."""
10
+
11
+ def __init__(self, logger, level: str, delay: float = 5.0):
12
+ """Initializes the log throttler with a level name and delay."""
13
+ self.logger = logger
14
+ self.level = level.upper() # Store the level name, e.g., "ERROR"
15
+ self.delay = delay
16
+ self.last_message: str | None = None
17
+ self.repeat_count: int = 0
18
+ self.timer: asyncio.TimerHandle | None = None
19
+
20
+ def _flush_summary(self, **kwargs):
21
+ """Prints the summary of how many times the last message was repeated."""
22
+ if self.repeat_count > 2:
23
+ self.logger.opt(depth=2).log(
24
+ self.level,
25
+ f"{self.last_message} (and {self.repeat_count -1} more in the last {self.delay} seconds.)",
26
+ **kwargs,
27
+ )
28
+ elif self.repeat_count == 2:
29
+ # If the message was repeated only once, we log it directly.
30
+ self.logger.opt(depth=2).log(
31
+ self.level, self.last_message, **kwargs
32
+ )
33
+
34
+ # Reset the state
35
+ self.timer = None
36
+ self.last_message = None
37
+ self.repeat_count = 0
38
+
39
+ def process(self, message: str, **kwargs):
40
+ """Processes a log message, either logging it or incrementing a repeat counter."""
41
+ if self.last_message and message != self.last_message:
42
+ if self.timer:
43
+ self.timer.cancel()
44
+ self._flush_summary()
45
+
46
+ if message == self.last_message:
47
+ self.repeat_count += 1
48
+ else:
49
+ # It's a new message. Log it immediately, but look 1 frame up the stack.
50
+ # This ensures the log record shows the original caller (e.g., wormhole.handler).
51
+ self.logger.opt(depth=1).log(self.level, message, **kwargs)
52
+ self.last_message = message
53
+ self.repeat_count = 1
54
+
55
+ if self.timer:
56
+ self.timer.cancel()
57
+
58
+ loop = asyncio.get_running_loop()
59
+ self.timer = loop.call_later(self.delay, self._flush_summary)
60
+
61
+
62
+ # In loguru, the logger is imported and ready to be configured.
63
+ # We just need to ensure other modules import this configured instance.
64
+ def setup_logger(
65
+ syslog_host: str | None = None, syslog_port: int = 514, verbose: int = 0
66
+ ) -> None:
67
+ """
68
+ Configures the global loguru logger instance. This should only be called once.
69
+ """
70
+ # Remove the default handler to have full control over sinks.
71
+ logger.remove()
72
+
73
+ # Set logging level based on verbosity.
74
+ if verbose >= 2:
75
+ level = "DEBUG"
76
+ elif verbose >= 1:
77
+ level = "DEBUG"
78
+ else:
79
+ level = "INFO"
80
+
81
+ # --- Console Sink ---
82
+ # Loguru automatically adds contextual data. The format is simpler.
83
+ console_format = (
84
+ "<green>{time:MMM D HH:mm:ss}</green> "
85
+ "<cyan>{name}</cyan>[<cyan>{process}</cyan>]: "
86
+ "<level>{message}</level>"
87
+ )
88
+ logger.add(sys.stderr, level=level, format=console_format)
89
+
90
+ # --- Syslog Sink ---
91
+ if syslog_host and syslog_host != "DISABLED":
92
+ # Create a standard library syslog handler instance.
93
+ # Loguru can sink to handler objects directly.
94
+ if syslog_host.startswith("/") and os.path.exists(syslog_host):
95
+ handler = logging.handlers.SysLogHandler(address=syslog_host)
96
+ syslog_format = "{time:MMM D HH:mm:ss} {name}[{process}]: {message}"
97
+ else:
98
+ handler = logging.handlers.SysLogHandler(
99
+ address=(syslog_host, syslog_port)
100
+ )
101
+ # For network syslog, the hostname is typically added by the syslog server,
102
+ # but we can include it if needed.
103
+ syslog_format = "{time:MMM D HH:mm:ss} {extra[hostname]} {name}[{process}]: {message}"
104
+ # Add hostname to all log records.
105
+ logger.configure(extra={"hostname": os.uname().nodename})
106
+
107
+ logger.add(handler, level="INFO", format=syslog_format)
108
+
109
+ # Suppress overly verbose asyncio logger messages unless in high verbosity.
110
+ logging.getLogger("asyncio").setLevel(
111
+ logging.DEBUG if verbose >= 2 else logging.CRITICAL
112
+ )
113
+ if verbose < 2:
114
+ logger.info = LogThrottler(logger, "info").process
115
+ logger.warning = LogThrottler(logger, "warning").process
116
+ logger.error = LogThrottler(logger, "error").process
117
+
118
+
119
+ def format_log_message(
120
+ message: str, ident: dict[str, str], verbose: int
121
+ ) -> str:
122
+ """
123
+ Formats a log message with the given identifier.
124
+
125
+ Args:
126
+ message: The log message to format.
127
+ ident: A dictionary containing identifiers like 'id' and 'client'.
128
+ verbose: The verbosity level of the logger.
129
+
130
+ Returns:
131
+ A formatted log message string.
132
+ """
133
+ if verbose > 1:
134
+ return f"[{ident['id']}][{ident['client']}]: {message}"
135
+ else:
136
+ return f"[{ident['client']}]: {message}"
@@ -9,7 +9,7 @@ if sys.version_info < (3, 11):
9
9
 
10
10
  from .ad_blocker import update_database
11
11
  from .auth_manager import add_user, modify_user, delete_user
12
- from .logger import logger, setup_logger
12
+ from .logger import logger, setup_logger, format_log_message as flm
13
13
  from .safeguards import load_ad_block_db, load_allowlist
14
14
  from .server import start_wormhole_server
15
15
  from .version import VERSION
@@ -32,22 +32,42 @@ except ImportError:
32
32
  async def main_async(args) -> None:
33
33
  """The main asynchronous function to run the server."""
34
34
  if uvloop:
35
- logger.info(f"Using high-performance event loop: {uvloop.__name__}")
35
+ logger.info(
36
+ flm(
37
+ f"Using high-performance event loop: {uvloop.__name__}",
38
+ ident={"id": "000000", "client": args.host},
39
+ verbose=args.verbose,
40
+ )
41
+ )
36
42
  else:
37
- logger.info("Using standard asyncio event loop.")
43
+ logger.info(
44
+ flm(
45
+ "Using standard asyncio event loop.",
46
+ ident={"id": "000000", "client": args.host},
47
+ verbose=args.verbose,
48
+ )
49
+ )
38
50
 
39
51
  if args.allowlist:
40
- num_allowed = load_allowlist(args.allowlist)
52
+ num_allowed = load_allowlist(args.allowlist, args.host)
41
53
  if num_allowed > 0:
42
54
  logger.info(
43
- f"Loaded custom allowlist. Total allowlist size: {num_allowed} domains."
55
+ flm(
56
+ f"Loaded custom allowlist. Total allowlist size: {num_allowed} domains.",
57
+ ident={"id": "000000", "client": args.host},
58
+ verbose=args.verbose,
59
+ )
44
60
  )
45
61
 
46
62
  if args.ad_block_db:
47
- num_blocked = await load_ad_block_db(args.ad_block_db)
63
+ num_blocked = await load_ad_block_db(args.ad_block_db, args.host)
48
64
  if num_blocked > 0:
49
65
  logger.info(
50
- f"Ad-blocker enabled with {num_blocked} domains from database."
66
+ flm(
67
+ f"Ad-blocker enabled with {num_blocked} domains from database.",
68
+ ident={"id": "000000", "client": args.host},
69
+ verbose=args.verbose,
70
+ )
51
71
  )
52
72
 
53
73
  shutdown_event = asyncio.Event()
@@ -65,16 +85,34 @@ async def main_async(args) -> None:
65
85
  args.allow_private,
66
86
  )
67
87
 
68
- logger.info("Server startup complete. Waiting for connections...")
88
+ logger.info(
89
+ flm(
90
+ "Server startup complete. Waiting for connections...",
91
+ ident={"id": "000000", "client": args.host},
92
+ verbose=args.verbose,
93
+ )
94
+ )
69
95
 
70
96
  # Wait for the shutdown signal.
71
97
  await shutdown_event.wait()
72
98
 
73
99
  # Gracefully shut down the server.
74
- logger.info("Shutdown signal received, closing server...")
100
+ logger.info(
101
+ flm(
102
+ f"Shutdown signal received, closing server...",
103
+ ident={"id": "000000", "client": args.host},
104
+ verbose=args.verbose,
105
+ )
106
+ )
75
107
  server.close()
76
108
  await server.wait_closed()
77
- logger.info("Server has been shut down gracefully.")
109
+ logger.info(
110
+ flm(
111
+ f"Server has been shut down gracefully.",
112
+ ident={"id": "000000", "client": args.host},
113
+ verbose=args.verbose,
114
+ )
115
+ )
78
116
 
79
117
 
80
118
  def main() -> int:
@@ -83,13 +83,14 @@ def is_private_ip(ip_str: str) -> bool:
83
83
  return True
84
84
 
85
85
 
86
- async def load_ad_block_db(path: str) -> int:
86
+ async def load_ad_block_db(path: str, host: str) -> int:
87
87
  """
88
88
  Loads a list of domains to block from a SQLite database into a global set
89
89
  for fast in-memory access.
90
90
 
91
91
  Args:
92
92
  path: The path to the SQLite database file.
93
+ host: The host IP of the server, used for logging.
93
94
 
94
95
  Returns:
95
96
  The number of unique domains loaded into the blocklist.
@@ -103,7 +104,7 @@ async def load_ad_block_db(path: str) -> int:
103
104
  AD_BLOCK_SET.add(row[0])
104
105
  except Exception as e:
105
106
  logger.error(
106
- f"Could not load ad-block database from '{path}': {e}",
107
+ f"[000000][{host}]: Could not load ad-block database from '{path}': {e}",
107
108
  )
108
109
 
109
110
  if AD_BLOCK_SET:
@@ -112,13 +113,13 @@ async def load_ad_block_db(path: str) -> int:
112
113
  content_size = sum(sys.getsizeof(s) for s in AD_BLOCK_SET)
113
114
  total_size_mb = (set_size + content_size) / (1024 * 1024)
114
115
  logger.debug(
115
- f"Ad-block set memory usage: ~{total_size_mb:.2f} MB for {len(AD_BLOCK_SET)} domains"
116
+ f"[000000][{host}]: Ad-block set memory usage: ~{total_size_mb:.2f} MB for {len(AD_BLOCK_SET)} domains"
116
117
  )
117
118
 
118
119
  return len(AD_BLOCK_SET)
119
120
 
120
121
 
121
- def load_allowlist(path: str) -> int:
122
+ def load_allowlist(path: str, host: str) -> int:
122
123
  """
123
124
  Loads domains from a user-provided file and adds them to the global allowlist set.
124
125
  """
@@ -128,7 +129,7 @@ def load_allowlist(path: str) -> int:
128
129
  if line.strip() and not line.startswith("#"):
129
130
  ALLOW_LIST_SET.add(line.strip().lower())
130
131
  except FileNotFoundError:
131
- logger.error(f"Allowlist file not found at '{path}'")
132
+ logger.error(f"[000000][{host}]: Allowlist file not found at '{path}'")
132
133
  return len(ALLOW_LIST_SET)
133
134
 
134
135
 
@@ -1,6 +1,6 @@
1
1
  from .authentication import get_ident, verify_credentials
2
2
  from .handler import process_http_request, process_https_tunnel, parse_request
3
- from .logger import logger
3
+ from .logger import logger, format_log_message as flm
4
4
  from time import time
5
5
  import asyncio
6
6
  import functools
@@ -45,22 +45,22 @@ async def handle_connection(
45
45
  CURRENT_TASKS += 1
46
46
  if verbose > 0:
47
47
  logger.debug(
48
- f"[{ident['id']}][{ident['client']}]: {CURRENT_TASKS}/{MAX_TASKS} Tasks active"
48
+ flm(f"{CURRENT_TASKS}/{MAX_TASKS} Tasks active", ident, verbose)
49
49
  )
50
50
  else:
51
- logger.debug(f"[{ident['id']}][{ident['client']}]: Connection started.")
51
+ logger.debug(flm("Connection started.", ident, verbose))
52
52
 
53
53
  try:
54
54
  # Parse the initial request from the client.
55
55
  request_line, headers, payload = await parse_request(
56
- client_reader, MAX_RETRY, ident
56
+ client_reader, ident, verbose
57
57
  )
58
58
  # If parse_request fails, it returns (None, None, None). We check all three
59
59
  # to explicitly narrow the types for mypy, which can't infer that if one
60
60
  # is None, they all are.
61
61
  if not request_line or headers is None or payload is None:
62
62
  logger.debug(
63
- f"[{ident['id']}][{ident['client']}]: Empty request, closing connection."
63
+ flm("Empty request, closing connection.", ident, verbose)
64
64
  )
65
65
  return
66
66
 
@@ -69,7 +69,11 @@ async def handle_connection(
69
69
  method, uri, version = request_line.split(" ", 2)
70
70
  except ValueError:
71
71
  logger.debug(
72
- f"[{ident['id']}][{ident['client']}]: Malformed request line '{request_line}', closing."
72
+ flm(
73
+ f"Malformed request line '{request_line}', closing.",
74
+ ident,
75
+ verbose,
76
+ )
73
77
  )
74
78
  return
75
79
 
@@ -86,14 +90,24 @@ async def handle_connection(
86
90
  )
87
91
  if user_ident is None:
88
92
  logger.info(
89
- f"[{ident['id']}][{ident['client']}]: {method} 407 {uri} (Authentication Failed)"
93
+ flm(
94
+ f"{method} 407 {uri} (Authentication Failed)",
95
+ ident,
96
+ verbose,
97
+ )
90
98
  )
91
99
  return
92
100
  ident = user_ident # Update ident with authenticated user info.
93
101
  # --- Request Dispatching ---
94
102
  if method.upper() == "CONNECT":
95
103
  await process_https_tunnel(
96
- client_reader, client_writer, method, uri, ident, allow_private
104
+ client_reader,
105
+ client_writer,
106
+ method,
107
+ uri,
108
+ ident,
109
+ allow_private,
110
+ verbose,
97
111
  )
98
112
  else:
99
113
  # The check above ensures `headers` is `list[str]` and `payload` is `bytes`.
@@ -106,11 +120,12 @@ async def handle_connection(
106
120
  payload,
107
121
  ident,
108
122
  allow_private,
123
+ verbose,
109
124
  )
110
125
 
111
126
  except Exception as e:
112
127
  logger.error(
113
- f"[{ident['id']}][{ident['client']}]: Unhandled error in connection handler: {e}",
128
+ flm(f"Unhandled error in connection handler: {e}", ident, verbose),
114
129
  exc_info=True,
115
130
  )
116
131
  finally:
@@ -120,7 +135,7 @@ async def handle_connection(
120
135
  await client_writer.wait_closed()
121
136
  duration = time() - start_time
122
137
  logger.debug(
123
- f"[{ident['id']}][{ident['client']}]: Connection closed ({duration:.5f} seconds)."
138
+ flm(f"Connection closed ({duration:.5f} seconds).", ident, verbose)
124
139
  )
125
140
 
126
141
 
@@ -154,13 +169,21 @@ async def start_wormhole_server(
154
169
  for s in server.sockets:
155
170
  addr = s.getsockname()
156
171
  logger.info(
157
- f"[000000][{host}]: Wormhole proxy bound and listening at {addr[0]}:{addr[1]}"
172
+ flm(
173
+ f"Wormhole proxy bound and listening at {addr[0]}:{addr[1]}",
174
+ ident={"id": "000000", "client": host},
175
+ verbose=verbose,
176
+ )
158
177
  )
159
178
 
160
179
  return server
161
180
 
162
181
  except OSError as e:
163
182
  logger.critical(
164
- f"[000000][{host}]: Failed to bind server at {host}:{port}: {e}"
183
+ flm(
184
+ f"Failed to bind server at {host}:{port}: {e}",
185
+ ident={"id": "000000", "client": host},
186
+ verbose=verbose,
187
+ )
165
188
  )
166
189
  raise
@@ -1,58 +0,0 @@
1
- from loguru import logger
2
- import logging.handlers
3
- import os
4
- import sys
5
-
6
- # In loguru, the logger is imported and ready to be configured.
7
- # We just need to ensure other modules import this configured instance.
8
-
9
-
10
- def setup_logger(
11
- syslog_host: str | None = None, syslog_port: int = 514, verbose: int = 0
12
- ) -> None:
13
- """
14
- Configures the global loguru logger instance. This should only be called once.
15
- """
16
- # Remove the default handler to have full control over sinks.
17
- logger.remove()
18
-
19
- # Set logging level based on verbosity.
20
- if verbose >= 2:
21
- level = "DEBUG"
22
- elif verbose >= 1:
23
- level = "DEBUG"
24
- else:
25
- level = "INFO"
26
-
27
- # --- Console Sink ---
28
- # Loguru automatically adds contextual data. The format is simpler.
29
- console_format = (
30
- "<green>{time:MMM D HH:mm:ss}</green> "
31
- "<cyan>{name}</cyan>[<cyan>{process}</cyan>]: "
32
- "<level>{message}</level>"
33
- )
34
- logger.add(sys.stderr, level=level, format=console_format)
35
-
36
- # --- Syslog Sink ---
37
- if syslog_host and syslog_host != "DISABLED":
38
- # Create a standard library syslog handler instance.
39
- # Loguru can sink to handler objects directly.
40
- if syslog_host.startswith("/") and os.path.exists(syslog_host):
41
- handler = logging.handlers.SysLogHandler(address=syslog_host)
42
- syslog_format = "{time:MMM D HH:mm:ss} {name}[{process}]: {message}"
43
- else:
44
- handler = logging.handlers.SysLogHandler(
45
- address=(syslog_host, syslog_port)
46
- )
47
- # For network syslog, the hostname is typically added by the syslog server,
48
- # but we can include it if needed.
49
- syslog_format = "{time:MMM D HH:mm:ss} {extra[hostname]} {name}[{process}]: {message}"
50
- # Add hostname to all log records.
51
- logger.configure(extra={"hostname": os.uname().nodename})
52
-
53
- logger.add(handler, level="INFO", format=syslog_format)
54
-
55
- # Suppress overly verbose asyncio logger messages unless in high verbosity.
56
- logging.getLogger("asyncio").setLevel(
57
- logging.DEBUG if verbose >= 2 else logging.CRITICAL
58
- )
File without changes
File without changes