linksocks 3.0.12__cp39-cp39-win_amd64.whl

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.

Potentially problematic release.


This version of linksocks might be problematic. Click here for more details.

linksocks/_client.py ADDED
@@ -0,0 +1,291 @@
1
+ """
2
+ WebSocket SOCKS5 proxy client implementation.
3
+
4
+ This module provides the Client class for connecting to a SOCKS5 proxy server
5
+ over WebSocket and establishing proxy functionality.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import logging
12
+ from typing import Optional
13
+
14
+ # Underlying Go bindings module (generated)
15
+ from linksockslib import linksocks # type: ignore
16
+
17
+ from ._base import (
18
+ _SnakePassthrough,
19
+ _to_duration,
20
+ _logger,
21
+ BufferZerologLogger,
22
+ DurationLike,
23
+ )
24
+
25
+
26
+ class Client(_SnakePassthrough):
27
+ """WebSocket SOCKS5 proxy client.
28
+
29
+ The Client class connects to a WebSocket server and establishes SOCKS5 proxy
30
+ functionality. It supports both forward and reverse proxy modes.
31
+
32
+ In forward mode, the client runs a local SOCKS5 server and forwards connections
33
+ through WebSocket to the server, which then connects to targets.
34
+
35
+ In reverse mode, the client connects to targets directly and forwards data
36
+ back through WebSocket to a SOCKS5 server running on the server side.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ token: str,
42
+ *,
43
+ logger: Optional[logging.Logger] = None,
44
+ ws_url: Optional[str] = None,
45
+ reverse: Optional[bool] = None,
46
+ socks_host: Optional[str] = None,
47
+ socks_port: Optional[int] = None,
48
+ socks_username: Optional[str] = None,
49
+ socks_password: Optional[str] = None,
50
+ socks_wait_server: Optional[bool] = None,
51
+ reconnect: Optional[bool] = None,
52
+ reconnect_delay: Optional[DurationLike] = None,
53
+ buffer_size: Optional[int] = None,
54
+ channel_timeout: Optional[DurationLike] = None,
55
+ connect_timeout: Optional[DurationLike] = None,
56
+ threads: Optional[int] = None,
57
+ fast_open: Optional[bool] = None,
58
+ upstream_proxy: Optional[str] = None,
59
+ upstream_username: Optional[str] = None,
60
+ upstream_password: Optional[str] = None,
61
+ no_env_proxy: Optional[bool] = None,
62
+ ) -> None:
63
+ """Initialize the WebSocket SOCKS5 proxy client.
64
+
65
+ Args:
66
+ token: Authentication token for WebSocket connection
67
+ logger: Python logger instance for this client
68
+ ws_url: WebSocket server URL to connect to
69
+ reverse: Whether to use reverse proxy mode
70
+ socks_host: SOCKS5 server listen address (for forward mode)
71
+ socks_port: SOCKS5 server listen port (for forward mode)
72
+ socks_username: SOCKS5 authentication username
73
+ socks_password: SOCKS5 authentication password
74
+ socks_wait_server: Whether to wait for server connection before starting SOCKS5
75
+ reconnect: Whether to automatically reconnect on disconnection
76
+ reconnect_delay: Delay between reconnection attempts
77
+ buffer_size: Buffer size for data transfer
78
+ channel_timeout: Timeout for WebSocket channels
79
+ connect_timeout: Timeout for outbound connections
80
+ threads: Number of threads for concurrent processing
81
+ fast_open: Assume connection success and allow data transfer immediately
82
+ upstream_proxy: Upstream proxy address for chaining
83
+ upstream_username: Username for upstream proxy authentication
84
+ upstream_password: Password for upstream proxy authentication
85
+ no_env_proxy: Whether to ignore proxy environment variables
86
+ """
87
+ opt = linksocks.DefaultClientOption()
88
+ if logger is None:
89
+ logger = _logger
90
+ # Use buffer-based logger system
91
+ self._managed_logger = BufferZerologLogger(logger, f"client_{id(self)}")
92
+ opt.WithLogger(self._managed_logger.go_logger)
93
+ if ws_url is not None:
94
+ opt.WithWSURL(ws_url)
95
+ if reverse is not None:
96
+ opt.WithReverse(bool(reverse))
97
+ if socks_host is not None:
98
+ opt.WithSocksHost(socks_host)
99
+ if socks_port is not None:
100
+ opt.WithSocksPort(int(socks_port))
101
+ if socks_username is not None:
102
+ opt.WithSocksUsername(socks_username)
103
+ if socks_password is not None:
104
+ opt.WithSocksPassword(socks_password)
105
+ if socks_wait_server is not None:
106
+ opt.WithSocksWaitServer(bool(socks_wait_server))
107
+ if reconnect is not None:
108
+ opt.WithReconnect(bool(reconnect))
109
+ if reconnect_delay is not None:
110
+ opt.WithReconnectDelay(_to_duration(reconnect_delay))
111
+ if buffer_size is not None:
112
+ opt.WithBufferSize(int(buffer_size))
113
+ if channel_timeout is not None:
114
+ opt.WithChannelTimeout(_to_duration(channel_timeout))
115
+ if connect_timeout is not None:
116
+ opt.WithConnectTimeout(_to_duration(connect_timeout))
117
+ if threads is not None:
118
+ opt.WithThreads(int(threads))
119
+ if fast_open is not None:
120
+ opt.WithFastOpen(bool(fast_open))
121
+ if upstream_proxy is not None:
122
+ opt.WithUpstreamProxy(upstream_proxy)
123
+ if upstream_username or upstream_password:
124
+ opt.WithUpstreamAuth(upstream_username or "", upstream_password or "")
125
+ if no_env_proxy is not None:
126
+ opt.WithNoEnvProxy(bool(no_env_proxy))
127
+
128
+ self._raw = linksocks.NewLinkSocksClient(token, opt)
129
+ self._ctx = None
130
+
131
+ @property
132
+ def log(self) -> logging.Logger:
133
+ """Access the Python logger for this client instance."""
134
+ return self._managed_logger.py_logger
135
+
136
+ def wait_ready(self, timeout: Optional[DurationLike] = None) -> None:
137
+ """Wait for the client to be ready.
138
+
139
+ Args:
140
+ timeout: Maximum time to wait, no timeout if None
141
+ """
142
+ if not self._ctx:
143
+ self._ctx = linksocks.NewContextWithCancel()
144
+ timeout = _to_duration(timeout) if timeout is not None else 0
145
+ return self._raw.WaitReady(ctx=self._ctx.Context(), timeout=timeout)
146
+
147
+ async def async_wait_ready(self, timeout: Optional[DurationLike] = None) -> None:
148
+ """Wait for the client to be ready asynchronously.
149
+
150
+ Args:
151
+ timeout: Maximum time to wait, no timeout if None
152
+ """
153
+ if not self._ctx:
154
+ self._ctx = linksocks.NewContextWithCancel()
155
+ timeout = _to_duration(timeout) if timeout is not None else 0
156
+ try:
157
+ return await asyncio.to_thread(self._raw.WaitReady, ctx=self._ctx.Context(), timeout=timeout)
158
+ except asyncio.CancelledError:
159
+ # Ensure the underlying Go client stops retrying/logging when the
160
+ # awaiting task is cancelled (e.g. Ctrl+C). We cancel the context
161
+ # we passed into Go and close the client, then re-raise.
162
+ try:
163
+ try:
164
+ self._ctx.Cancel()
165
+ except Exception:
166
+ pass
167
+ # Shield cleanup from further cancellation so it can complete
168
+ await asyncio.shield(asyncio.to_thread(self._raw.Close))
169
+ # Best-effort logger cleanup
170
+ if hasattr(self, '_managed_logger') and self._managed_logger:
171
+ try:
172
+ self._managed_logger.cleanup()
173
+ except Exception:
174
+ pass
175
+ finally:
176
+ raise
177
+
178
+ def add_connector(self, connector_token: Optional[str]) -> str:
179
+ """Add a connector token for reverse proxy.
180
+
181
+ Args:
182
+ connector_token: Connector token string, auto-generated if not provided
183
+
184
+ Returns:
185
+ The connector token string (generated or provided)
186
+ """
187
+ return self._raw.AddConnector(connector_token or "")
188
+
189
+ async def async_add_connector(self, connector_token: Optional[str]) -> str:
190
+ """Add a connector token for reverse proxy asynchronously.
191
+
192
+ Args:
193
+ connector_token: Connector token string, auto-generated if not provided
194
+
195
+ Returns:
196
+ The connector token string (generated or provided)
197
+ """
198
+ return await asyncio.to_thread(self._raw.AddConnector, connector_token or "")
199
+
200
+ @property
201
+ def is_connected(self) -> bool:
202
+ """Check if the client is connected to the server.
203
+
204
+ Returns:
205
+ True if connected, False otherwise
206
+ """
207
+ try:
208
+ return bool(self._raw.IsConnected)
209
+ except Exception:
210
+ # If not exposed as field, fall back to False
211
+ return False
212
+
213
+ @property
214
+ def socks_port(self) -> Optional[int]:
215
+ """Get the SOCKS5 server port (for forward mode).
216
+
217
+ Returns:
218
+ The port number if available, None otherwise
219
+ """
220
+ try:
221
+ # Exposed field in bindings
222
+ port = getattr(self._raw, "SocksPort", None)
223
+ return int(port) if port is not None else None
224
+ except Exception:
225
+ return None
226
+
227
+ def close(self) -> None:
228
+ """Close the client and clean up resources."""
229
+ # Close client
230
+ if hasattr(self, '_raw') and self._raw:
231
+ self._raw.Close()
232
+ # Clean up managed logger
233
+ if hasattr(self, '_managed_logger') and self._managed_logger:
234
+ try:
235
+ self._managed_logger.cleanup()
236
+ except:
237
+ # Ignore cleanup errors
238
+ pass
239
+ # Close context
240
+ if hasattr(self, '_ctx') and self._ctx:
241
+ try:
242
+ self._ctx.Cancel()
243
+ except Exception:
244
+ # Ignore errors during context close
245
+ pass
246
+
247
+ async def async_close(self) -> None:
248
+ """Close the client and clean up resources asynchronously."""
249
+ # Close client
250
+ if hasattr(self, '_raw') and self._raw:
251
+ await asyncio.to_thread(self._raw.Close)
252
+ # Clean up managed logger
253
+ if hasattr(self, '_managed_logger') and self._managed_logger:
254
+ try:
255
+ self._managed_logger.cleanup()
256
+ except:
257
+ # Ignore cleanup errors
258
+ pass
259
+ # Close context
260
+ if hasattr(self, '_ctx') and self._ctx:
261
+ try:
262
+ self._ctx.Cancel()
263
+ except Exception:
264
+ # Ignore errors during context close
265
+ pass
266
+
267
+ # Context manager support
268
+ def __enter__(self) -> "Client":
269
+ """Context manager entry."""
270
+ return self
271
+
272
+ def __exit__(self, exc_type, exc, tb) -> None:
273
+ """Context manager exit."""
274
+ self.close()
275
+
276
+ async def __aenter__(self) -> "Client":
277
+ """Async context manager entry."""
278
+ await self.async_wait_ready()
279
+ return self
280
+
281
+ async def __aexit__(self, exc_type, exc, tb) -> None:
282
+ """Async context manager exit."""
283
+ await self.async_close()
284
+
285
+ def __del__(self):
286
+ """Destructor - clean up resources."""
287
+ try:
288
+ self.close()
289
+ except Exception:
290
+ # Ignore errors during cleanup
291
+ pass
@@ -0,0 +1,182 @@
1
+ """
2
+ Logging configuration module for linksocks CLI.
3
+
4
+ This module provides custom logging handlers and configuration utilities
5
+ for the linksocks command-line interface, including Rich-based formatting
6
+ and loguru integration.
7
+ """
8
+
9
+ import inspect
10
+ import logging
11
+ import asyncio
12
+ from typing import Any
13
+
14
+ from loguru import logger
15
+ from rich.text import Text
16
+ from rich.console import Console
17
+ from rich.logging import RichHandler
18
+
19
+ # Global console instance for consistent output
20
+ console = Console(stderr=True)
21
+
22
+
23
+ class CarriageReturnRichHandler(RichHandler):
24
+ """
25
+ A custom RichHandler that outputs a carriage return (\\r) before emitting log messages.
26
+
27
+ This handler prevents Ctrl+C interruptions from disrupting the output by ensuring
28
+ the cursor is positioned at the beginning of the line before writing log messages.
29
+ This is particularly useful in CLI applications where clean output formatting
30
+ is important even during interruptions.
31
+
32
+ Attributes:
33
+ console: The Rich Console instance used for output formatting
34
+ """
35
+
36
+ def emit(self, record: logging.LogRecord) -> None:
37
+ """
38
+ Emit a log record with carriage return prefix.
39
+
40
+ This method overrides the parent emit method to first output a carriage
41
+ return character, moving the cursor to the beginning of the line before
42
+ writing the actual log message. This prevents Ctrl+C characters from
43
+ appearing in the middle of log output.
44
+
45
+ Args:
46
+ record: The LogRecord instance containing the log message and metadata
47
+
48
+ Note:
49
+ The carriage return is only written if the console is a terminal.
50
+ Errors during emission are handled by the parent class's handleError method.
51
+ """
52
+ try:
53
+ # Write carriage return to move cursor to line start
54
+ if self.console.is_terminal:
55
+ self.console.file.write('\r')
56
+ self.console.file.flush()
57
+
58
+ # Call the parent emit method to handle the actual log output
59
+ super().emit(record)
60
+
61
+ except Exception:
62
+ # Let the parent class handle any errors during log emission
63
+ self.handleError(record)
64
+
65
+
66
+ class InterceptHandler(logging.Handler):
67
+ """
68
+ A logging handler that intercepts standard Python logging messages and redirects them to loguru.
69
+
70
+ This handler bridges the gap between Python's standard logging module and loguru,
71
+ allowing all log messages to be processed through loguru's enhanced formatting
72
+ and filtering capabilities while maintaining compatibility with existing code
73
+ that uses standard logging.
74
+
75
+ Attributes:
76
+ level: The minimum log level this handler will process
77
+ """
78
+
79
+ def __init__(self, level: int = 0) -> None:
80
+ """
81
+ Initialize the InterceptHandler.
82
+
83
+ Args:
84
+ level: The minimum logging level to handle (default: 0 for all levels)
85
+ """
86
+ super().__init__(level)
87
+
88
+ def emit(self, record: logging.LogRecord) -> None:
89
+ """
90
+ Intercept a logging record and redirect it to loguru.
91
+
92
+ This method converts standard Python logging records to loguru format,
93
+ preserving the original log level, message content, and exception information.
94
+ It also attempts to maintain the correct call stack depth for accurate
95
+ source location reporting.
96
+
97
+ Args:
98
+ record: The LogRecord instance from Python's standard logging
99
+ """
100
+ try:
101
+ # Try to map the logging level name to loguru's level system
102
+ level = logger.level(record.levelname).name
103
+ except ValueError:
104
+ # If the level name is not recognized, use the numeric level
105
+ level = record.levelno
106
+
107
+ # Find the correct frame in the call stack to report accurate source location
108
+ frame = inspect.currentframe()
109
+ depth = 0
110
+ while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__):
111
+ frame = frame.f_back
112
+ depth += 1
113
+
114
+ # Extract and clean the log message text
115
+ text = record.getMessage()
116
+ # Convert ANSI escape sequences to plain text for consistent formatting
117
+ text = Text.from_ansi(text).plain
118
+
119
+ # Forward the message to loguru with preserved context
120
+ logger.opt(depth=depth, exception=record.exc_info).log(level, text)
121
+
122
+
123
+ def apply_logging_adapter(level: int = logging.INFO) -> None:
124
+ """
125
+ Configure Python's standard logging to use the InterceptHandler.
126
+
127
+ This function replaces the standard logging configuration with our custom
128
+ InterceptHandler, ensuring all logging messages are processed through loguru.
129
+
130
+ Args:
131
+ level: The minimum logging level to capture (default: logging.INFO)
132
+
133
+ Note:
134
+ This function uses force=True to override any existing logging configuration.
135
+ """
136
+ logging.basicConfig(handlers=[InterceptHandler()], level=level, force=True)
137
+
138
+
139
+ def init_logging(level: int = logging.INFO, **kwargs: Any) -> None:
140
+ """
141
+ Initialize the complete logging configuration for the CLI application.
142
+
143
+ This function sets up a comprehensive logging system that:
144
+ - Intercepts standard Python logging and routes it through loguru
145
+ - Configures Rich-based formatting for enhanced terminal output
146
+ - Supports custom log levels including trace-level debugging
147
+ - Provides consistent formatting with timestamps
148
+
149
+ Args:
150
+ level: The minimum logging level to display (default: logging.INFO)
151
+ **kwargs: Additional keyword arguments passed to CarriageReturnRichHandler
152
+
153
+ The logging system supports the following levels:
154
+ - logging.INFO (20): Standard informational messages
155
+ - logging.DEBUG (10): Detailed debugging information
156
+ - 5: Trace-level debugging (most verbose)
157
+
158
+ Example:
159
+ >>> init_logging(level=logging.DEBUG)
160
+ >>> logger.info("Application started")
161
+ >>> logger.debug("Debug information")
162
+ """
163
+ # Set up the logging adapter to intercept standard logging
164
+ apply_logging_adapter(level)
165
+
166
+ # Remove any existing loguru handlers to start fresh
167
+ logger.remove()
168
+
169
+ # Create our custom Rich handler with carriage return support
170
+ handler = CarriageReturnRichHandler(
171
+ console=console,
172
+ markup=True,
173
+ rich_tracebacks=True,
174
+ tracebacks_suppress=[asyncio],
175
+ **kwargs
176
+ )
177
+
178
+ # Set a simple timestamp format for log messages
179
+ handler.setFormatter(logging.Formatter(None, "[%m/%d %H:%M]"))
180
+
181
+ # Add the handler to loguru with minimal formatting (Rich handles the styling)
182
+ logger.add(handler, format="{message}", level=level)