linksocks 1.7.1__cp311-cp311-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/__init__.py +9 -0
- linksocks/_base.py +230 -0
- linksocks/_cli.py +291 -0
- linksocks/_client.py +291 -0
- linksocks/_logging_config.py +182 -0
- linksocks/_server.py +355 -0
- linksocks/_utils.py +144 -0
- linksocks-1.7.1.dist-info/METADATA +479 -0
- linksocks-1.7.1.dist-info/RECORD +21 -0
- linksocks-1.7.1.dist-info/WHEEL +5 -0
- linksocks-1.7.1.dist-info/entry_points.txt +2 -0
- linksocks-1.7.1.dist-info/top_level.txt +2 -0
- linksockslib/__init__.py +0 -0
- linksockslib/_linksockslib.cp311-win_amd64.h +1119 -0
- linksockslib/_linksockslib.cp311-win_amd64.pyd +0 -0
- linksockslib/build.py +773 -0
- linksockslib/go.py +4067 -0
- linksockslib/linksocks.py +5922 -0
- linksockslib/linksockslib.c +14530 -0
- linksockslib/linksockslib.go +8374 -0
- linksockslib/linksockslib_go.h +1119 -0
linksocks/_server.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket SOCKS5 proxy server implementation.
|
|
3
|
+
|
|
4
|
+
This module provides the Server class for running a SOCKS5 proxy server
|
|
5
|
+
that communicates with clients over WebSocket connections.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any, 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
|
+
ReverseTokenResult,
|
|
23
|
+
DurationLike,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Server(_SnakePassthrough):
|
|
28
|
+
"""WebSocket SOCKS5 proxy server.
|
|
29
|
+
|
|
30
|
+
The Server class manages WebSocket connections from clients and provides
|
|
31
|
+
SOCKS5 proxy functionality. It supports both forward and reverse proxy modes.
|
|
32
|
+
|
|
33
|
+
In forward mode, clients connect to the server and the server makes outbound
|
|
34
|
+
connections on behalf of the clients.
|
|
35
|
+
|
|
36
|
+
In reverse mode, the server runs a local SOCKS5 server and forwards connections
|
|
37
|
+
through WebSocket to clients, which then connect to targets directly.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
*,
|
|
43
|
+
logger: Optional[logging.Logger] = None,
|
|
44
|
+
ws_host: Optional[str] = None,
|
|
45
|
+
ws_port: Optional[int] = None,
|
|
46
|
+
socks_host: Optional[str] = None,
|
|
47
|
+
port_pool: Optional[Any] = None,
|
|
48
|
+
socks_wait_client: Optional[bool] = None,
|
|
49
|
+
buffer_size: Optional[int] = None,
|
|
50
|
+
api_key: Optional[str] = None,
|
|
51
|
+
channel_timeout: Optional[DurationLike] = None,
|
|
52
|
+
connect_timeout: Optional[DurationLike] = None,
|
|
53
|
+
fast_open: Optional[bool] = None,
|
|
54
|
+
upstream_proxy: Optional[str] = None,
|
|
55
|
+
upstream_username: Optional[str] = None,
|
|
56
|
+
upstream_password: Optional[str] = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Initialize the WebSocket SOCKS5 proxy server.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
logger: Python logger instance for this server
|
|
62
|
+
ws_host: WebSocket server listen address
|
|
63
|
+
ws_port: WebSocket server listen port
|
|
64
|
+
socks_host: SOCKS5 server listen address (for reverse mode)
|
|
65
|
+
port_pool: Pool of ports for SOCKS5 servers
|
|
66
|
+
socks_wait_client: Whether to wait for client connections before starting SOCKS5
|
|
67
|
+
buffer_size: Buffer size for data transfer
|
|
68
|
+
api_key: API key for HTTP management interface
|
|
69
|
+
channel_timeout: Timeout for WebSocket channels
|
|
70
|
+
connect_timeout: Timeout for outbound connections
|
|
71
|
+
fast_open: Assume connection success and allow data transfer immediately
|
|
72
|
+
upstream_proxy: Upstream proxy address for chaining
|
|
73
|
+
upstream_username: Username for upstream proxy authentication
|
|
74
|
+
upstream_password: Password for upstream proxy authentication
|
|
75
|
+
"""
|
|
76
|
+
opt = linksocks.DefaultServerOption()
|
|
77
|
+
if logger is None:
|
|
78
|
+
logger = _logger
|
|
79
|
+
# Use buffer-based logger system
|
|
80
|
+
self._managed_logger = BufferZerologLogger(logger, f"server_{id(self)}")
|
|
81
|
+
opt.WithLogger(self._managed_logger.go_logger)
|
|
82
|
+
if ws_host is not None:
|
|
83
|
+
opt.WithWSHost(ws_host)
|
|
84
|
+
if ws_port is not None:
|
|
85
|
+
opt.WithWSPort(ws_port)
|
|
86
|
+
if socks_host is not None:
|
|
87
|
+
opt.WithSocksHost(socks_host)
|
|
88
|
+
if port_pool is not None:
|
|
89
|
+
opt.WithPortPool(port_pool)
|
|
90
|
+
if socks_wait_client is not None:
|
|
91
|
+
opt.WithSocksWaitClient(bool(socks_wait_client))
|
|
92
|
+
if buffer_size is not None:
|
|
93
|
+
opt.WithBufferSize(int(buffer_size))
|
|
94
|
+
if api_key is not None:
|
|
95
|
+
opt.WithAPI(api_key)
|
|
96
|
+
if channel_timeout is not None:
|
|
97
|
+
opt.WithChannelTimeout(_to_duration(channel_timeout))
|
|
98
|
+
if connect_timeout is not None:
|
|
99
|
+
opt.WithConnectTimeout(_to_duration(connect_timeout))
|
|
100
|
+
if fast_open is not None:
|
|
101
|
+
opt.WithFastOpen(bool(fast_open))
|
|
102
|
+
if upstream_proxy is not None:
|
|
103
|
+
opt.WithUpstreamProxy(upstream_proxy)
|
|
104
|
+
if upstream_username or upstream_password:
|
|
105
|
+
opt.WithUpstreamAuth(upstream_username or "", upstream_password or "")
|
|
106
|
+
|
|
107
|
+
self._raw = linksocks.NewLinkSocksServer(opt)
|
|
108
|
+
self._ctx = None
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def log(self) -> logging.Logger:
|
|
112
|
+
"""Access the Python logger for this server instance."""
|
|
113
|
+
return self._managed_logger.py_logger
|
|
114
|
+
|
|
115
|
+
def add_forward_token(self, token: Optional[str] = None) -> str:
|
|
116
|
+
"""Add a forward proxy token.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
token: Token string, auto-generated if not provided
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
The token string (generated or provided)
|
|
123
|
+
"""
|
|
124
|
+
return self._raw.AddForwardToken(token or "")
|
|
125
|
+
|
|
126
|
+
async def async_add_forward_token(self, token: Optional[str] = None) -> str:
|
|
127
|
+
"""Add a forward proxy token asynchronously.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
token: Token string, auto-generated if not provided
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
The token string (generated or provided)
|
|
134
|
+
"""
|
|
135
|
+
return await asyncio.to_thread(self._raw.AddForwardToken, token or "")
|
|
136
|
+
|
|
137
|
+
def add_reverse_token(
|
|
138
|
+
self,
|
|
139
|
+
*,
|
|
140
|
+
token: Optional[str] = None,
|
|
141
|
+
port: Optional[int] = None,
|
|
142
|
+
username: Optional[str] = None,
|
|
143
|
+
password: Optional[str] = None,
|
|
144
|
+
allow_manage_connector: Optional[bool] = None,
|
|
145
|
+
) -> ReverseTokenResult:
|
|
146
|
+
"""Add a reverse proxy token.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
token: Token string, auto-generated if not provided
|
|
150
|
+
port: SOCKS5 server port, auto-assigned if not provided
|
|
151
|
+
username: SOCKS5 authentication username
|
|
152
|
+
password: SOCKS5 authentication password
|
|
153
|
+
allow_manage_connector: Whether to allow clients to manage connector tokens
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Result containing the token and assigned port
|
|
157
|
+
"""
|
|
158
|
+
opts = linksocks.DefaultReverseTokenOptions()
|
|
159
|
+
if token:
|
|
160
|
+
opts.Token = token
|
|
161
|
+
if port is not None:
|
|
162
|
+
opts.Port = int(port)
|
|
163
|
+
if username is not None:
|
|
164
|
+
opts.Username = username
|
|
165
|
+
if password is not None:
|
|
166
|
+
opts.Password = password
|
|
167
|
+
if allow_manage_connector is not None:
|
|
168
|
+
opts.AllowManageConnector = bool(allow_manage_connector)
|
|
169
|
+
result = self._raw.AddReverseToken(opts)
|
|
170
|
+
return ReverseTokenResult(token=result.Token, port=result.Port)
|
|
171
|
+
|
|
172
|
+
async def async_add_reverse_token(
|
|
173
|
+
self,
|
|
174
|
+
*,
|
|
175
|
+
token: Optional[str] = None,
|
|
176
|
+
port: Optional[int] = None,
|
|
177
|
+
username: Optional[str] = None,
|
|
178
|
+
password: Optional[str] = None,
|
|
179
|
+
allow_manage_connector: Optional[bool] = None,
|
|
180
|
+
) -> ReverseTokenResult:
|
|
181
|
+
"""Add a reverse proxy token asynchronously.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
token: Token string, auto-generated if not provided
|
|
185
|
+
port: SOCKS5 server port, auto-assigned if not provided
|
|
186
|
+
username: SOCKS5 authentication username
|
|
187
|
+
password: SOCKS5 authentication password
|
|
188
|
+
allow_manage_connector: Whether to allow clients to manage connector tokens
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Result containing the token and assigned port
|
|
192
|
+
"""
|
|
193
|
+
opts = linksocks.DefaultReverseTokenOptions()
|
|
194
|
+
if token:
|
|
195
|
+
opts.Token = token
|
|
196
|
+
if port is not None:
|
|
197
|
+
opts.Port = int(port)
|
|
198
|
+
if username is not None:
|
|
199
|
+
opts.Username = username
|
|
200
|
+
if password is not None:
|
|
201
|
+
opts.Password = password
|
|
202
|
+
if allow_manage_connector is not None:
|
|
203
|
+
opts.AllowManageConnector = bool(allow_manage_connector)
|
|
204
|
+
result = await asyncio.to_thread(self._raw.AddReverseToken, opts)
|
|
205
|
+
return ReverseTokenResult(token=result.Token, port=result.Port)
|
|
206
|
+
|
|
207
|
+
def add_connector_token(self, connector_token: Optional[str], reverse_token: str) -> str:
|
|
208
|
+
"""Add a connector token for reverse proxy.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
connector_token: Connector token string, auto-generated if not provided
|
|
212
|
+
reverse_token: Associated reverse proxy token
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
The connector token string (generated or provided)
|
|
216
|
+
"""
|
|
217
|
+
return self._raw.AddConnectorToken(connector_token or "", reverse_token)
|
|
218
|
+
|
|
219
|
+
async def async_add_connector_token(self, connector_token: Optional[str], reverse_token: str) -> str:
|
|
220
|
+
"""Add a connector token for reverse proxy asynchronously.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
connector_token: Connector token string, auto-generated if not provided
|
|
224
|
+
reverse_token: Associated reverse proxy token
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
The connector token string (generated or provided)
|
|
228
|
+
"""
|
|
229
|
+
return await asyncio.to_thread(self._raw.AddConnectorToken, connector_token or "", reverse_token)
|
|
230
|
+
|
|
231
|
+
def remove_token(self, token: str) -> bool:
|
|
232
|
+
"""Remove a token from the server.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
token: Token to remove
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
True if token was removed, False if not found
|
|
239
|
+
"""
|
|
240
|
+
return self._raw.RemoveToken(token)
|
|
241
|
+
|
|
242
|
+
async def async_remove_token(self, token: str) -> bool:
|
|
243
|
+
"""Remove a token from the server asynchronously.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
token: Token to remove
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
True if token was removed, False if not found
|
|
250
|
+
"""
|
|
251
|
+
return await asyncio.to_thread(self._raw.RemoveToken, token)
|
|
252
|
+
|
|
253
|
+
def wait_ready(self, timeout: Optional[DurationLike] = None) -> None:
|
|
254
|
+
"""Wait for the server to be ready.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
timeout: Maximum time to wait, no timeout if None
|
|
258
|
+
"""
|
|
259
|
+
if not self._ctx:
|
|
260
|
+
self._ctx = linksocks.NewContextWithCancel()
|
|
261
|
+
timeout = _to_duration(timeout) if timeout is not None else 0
|
|
262
|
+
self._raw.WaitReady(ctx=self._ctx.Context(), timeout=timeout)
|
|
263
|
+
|
|
264
|
+
async def async_wait_ready(self, timeout: Optional[DurationLike] = None) -> None:
|
|
265
|
+
"""Wait for the server to be ready asynchronously.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
timeout: Maximum time to wait, no timeout if None
|
|
269
|
+
"""
|
|
270
|
+
if not self._ctx:
|
|
271
|
+
self._ctx = linksocks.NewContextWithCancel()
|
|
272
|
+
timeout = _to_duration(timeout) if timeout is not None else 0
|
|
273
|
+
try:
|
|
274
|
+
return await asyncio.to_thread(self._raw.WaitReady, ctx=self._ctx.Context(), timeout=timeout)
|
|
275
|
+
except asyncio.CancelledError:
|
|
276
|
+
# Ensure the underlying Go server stops when startup wait is cancelled
|
|
277
|
+
try:
|
|
278
|
+
try:
|
|
279
|
+
self._ctx.Cancel()
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
await asyncio.shield(asyncio.to_thread(self._raw.Close))
|
|
283
|
+
if hasattr(self, '_managed_logger') and self._managed_logger:
|
|
284
|
+
try:
|
|
285
|
+
self._managed_logger.cleanup()
|
|
286
|
+
except Exception:
|
|
287
|
+
pass
|
|
288
|
+
finally:
|
|
289
|
+
raise
|
|
290
|
+
|
|
291
|
+
def close(self) -> None:
|
|
292
|
+
"""Close the server and clean up resources."""
|
|
293
|
+
# Close server
|
|
294
|
+
if hasattr(self, '_raw') and self._raw:
|
|
295
|
+
self._raw.Close()
|
|
296
|
+
# Clean up managed logger
|
|
297
|
+
if hasattr(self, '_managed_logger') and self._managed_logger:
|
|
298
|
+
try:
|
|
299
|
+
self._managed_logger.cleanup()
|
|
300
|
+
except:
|
|
301
|
+
# Ignore cleanup errors
|
|
302
|
+
pass
|
|
303
|
+
# Close context
|
|
304
|
+
if hasattr(self, '_ctx') and self._ctx:
|
|
305
|
+
try:
|
|
306
|
+
self._ctx.Cancel()
|
|
307
|
+
except Exception:
|
|
308
|
+
# Ignore errors during context close
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
async def async_close(self) -> None:
|
|
312
|
+
"""Close the server and clean up resources asynchronously."""
|
|
313
|
+
# Close server
|
|
314
|
+
if hasattr(self, '_raw') and self._raw:
|
|
315
|
+
await asyncio.to_thread(self._raw.Close)
|
|
316
|
+
# Clean up managed logger
|
|
317
|
+
if hasattr(self, '_managed_logger') and self._managed_logger:
|
|
318
|
+
try:
|
|
319
|
+
self._managed_logger.cleanup()
|
|
320
|
+
except:
|
|
321
|
+
# Ignore cleanup errors
|
|
322
|
+
pass
|
|
323
|
+
# Close context
|
|
324
|
+
if hasattr(self, '_ctx') and self._ctx:
|
|
325
|
+
try:
|
|
326
|
+
self._ctx.Cancel()
|
|
327
|
+
except Exception:
|
|
328
|
+
# Ignore errors during context close
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
def __enter__(self) -> "Server":
|
|
332
|
+
"""Context manager entry."""
|
|
333
|
+
self.wait_ready()
|
|
334
|
+
return self
|
|
335
|
+
|
|
336
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
337
|
+
"""Context manager exit."""
|
|
338
|
+
self.close()
|
|
339
|
+
|
|
340
|
+
async def __aenter__(self) -> "Server":
|
|
341
|
+
"""Async context manager entry."""
|
|
342
|
+
await self.async_wait_ready()
|
|
343
|
+
return self
|
|
344
|
+
|
|
345
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
346
|
+
"""Async context manager exit."""
|
|
347
|
+
await self.async_close()
|
|
348
|
+
|
|
349
|
+
def __del__(self):
|
|
350
|
+
"""Destructor - clean up resources."""
|
|
351
|
+
try:
|
|
352
|
+
self.close()
|
|
353
|
+
except Exception:
|
|
354
|
+
# Ignore errors during cleanup
|
|
355
|
+
pass
|
linksocks/_utils.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for linksocks CLI.
|
|
3
|
+
|
|
4
|
+
This module provides common utility functions used across the linksocks
|
|
5
|
+
command-line interface, including SOCKS proxy URL parsing and environment
|
|
6
|
+
variable handling.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import urllib.parse
|
|
11
|
+
from typing import Optional, Tuple
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_socks_proxy(proxy_url: str) -> Tuple[str, Optional[str], Optional[str]]:
|
|
15
|
+
"""
|
|
16
|
+
Parse a SOCKS5 proxy URL and extract connection details.
|
|
17
|
+
|
|
18
|
+
This function parses SOCKS5 proxy URLs in the standard format and extracts
|
|
19
|
+
the host address, username, and password for authentication. It supports
|
|
20
|
+
URLs with optional authentication credentials and port numbers.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
proxy_url: URL string in format socks5://[user:pass@]host[:port]
|
|
24
|
+
Examples:
|
|
25
|
+
- "socks5://127.0.0.1:9870"
|
|
26
|
+
- "socks5://user:pass@proxy.example.com:9870"
|
|
27
|
+
- "socks5://proxy.example.com" (uses default port 9870)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A tuple containing:
|
|
31
|
+
- address (str): Host and port in "host:port" format
|
|
32
|
+
- username (str | None): Username for authentication, if provided
|
|
33
|
+
- password (str | None): Password for authentication, if provided
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: If the URL is invalid, malformed, or uses an unsupported scheme
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
>>> parse_socks_proxy("socks5://127.0.0.1:9870")
|
|
40
|
+
("127.0.0.1:9870", None, None)
|
|
41
|
+
|
|
42
|
+
>>> parse_socks_proxy("socks5://user:pass@proxy.example.com:9870")
|
|
43
|
+
("proxy.example.com:9870", "user", "pass")
|
|
44
|
+
|
|
45
|
+
>>> parse_socks_proxy("socks5://proxy.example.com")
|
|
46
|
+
("proxy.example.com:9870", None, None)
|
|
47
|
+
"""
|
|
48
|
+
if not proxy_url:
|
|
49
|
+
return "", None, None
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
parsed_url = urllib.parse.urlparse(proxy_url)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
raise ValueError(f"Invalid proxy URL: {e}") from e
|
|
55
|
+
|
|
56
|
+
# Validate the URL scheme
|
|
57
|
+
if parsed_url.scheme != "socks5":
|
|
58
|
+
raise ValueError(f"Unsupported proxy scheme: {parsed_url.scheme}. Only 'socks5' is supported.")
|
|
59
|
+
|
|
60
|
+
# Extract authentication credentials if present
|
|
61
|
+
username = None
|
|
62
|
+
password = None
|
|
63
|
+
if parsed_url.username:
|
|
64
|
+
username = urllib.parse.unquote(parsed_url.username)
|
|
65
|
+
if parsed_url.password:
|
|
66
|
+
password = urllib.parse.unquote(parsed_url.password)
|
|
67
|
+
|
|
68
|
+
# Build the address with default port if not specified
|
|
69
|
+
host = parsed_url.hostname
|
|
70
|
+
if not host:
|
|
71
|
+
raise ValueError("Proxy URL must specify a hostname")
|
|
72
|
+
|
|
73
|
+
port = parsed_url.port or 9870 # Default SOCKS5 port
|
|
74
|
+
address = f"{host}:{port}"
|
|
75
|
+
|
|
76
|
+
return address, username, password
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_env_or_flag(flag_value: Optional[str], env_var: str) -> Optional[str]:
|
|
80
|
+
"""
|
|
81
|
+
Get a configuration value from command-line flag or environment variable.
|
|
82
|
+
|
|
83
|
+
This utility function implements the common pattern of checking for a value
|
|
84
|
+
in a command-line flag first, and falling back to an environment variable
|
|
85
|
+
if the flag is not provided. This allows users to configure the application
|
|
86
|
+
either through command-line arguments or environment variables.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
flag_value: The value provided via command-line flag (may be None)
|
|
90
|
+
env_var: The name of the environment variable to check as fallback
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The configuration value from the flag if provided, otherwise from
|
|
94
|
+
the environment variable, or None if neither is set
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
>>> os.environ['LINKSOCKS_TOKEN'] = 'env-token'
|
|
98
|
+
>>> get_env_or_flag(None, 'LINKSOCKS_TOKEN')
|
|
99
|
+
'env-token'
|
|
100
|
+
|
|
101
|
+
>>> get_env_or_flag('flag-token', 'LINKSOCKS_TOKEN')
|
|
102
|
+
'flag-token'
|
|
103
|
+
|
|
104
|
+
>>> get_env_or_flag(None, 'NONEXISTENT_VAR')
|
|
105
|
+
None
|
|
106
|
+
|
|
107
|
+
Note:
|
|
108
|
+
Command-line flags always take precedence over environment variables.
|
|
109
|
+
This allows users to override environment settings on a per-invocation basis.
|
|
110
|
+
"""
|
|
111
|
+
if flag_value:
|
|
112
|
+
return flag_value
|
|
113
|
+
return os.getenv(env_var)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def validate_required_token(token: Optional[str], env_var: str = "LINKSOCKS_TOKEN") -> str:
|
|
117
|
+
"""
|
|
118
|
+
Validate that a required authentication token is provided.
|
|
119
|
+
|
|
120
|
+
This function checks that an authentication token is available either from
|
|
121
|
+
a command-line flag or environment variable, and raises a descriptive error
|
|
122
|
+
if neither is provided.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
token: The token value from command-line flag (may be None)
|
|
126
|
+
env_var: The environment variable name to check (default: "LINKSOCKS_TOKEN")
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The validated token string
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
ValueError: If no token is provided via flag or environment variable
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> validate_required_token("my-token")
|
|
136
|
+
"my-token"
|
|
137
|
+
|
|
138
|
+
>>> validate_required_token(None) # Will check LINKSOCKS_TOKEN env var
|
|
139
|
+
ValueError: Token is required. Provide via --token or LINKSOCKS_TOKEN environment variable.
|
|
140
|
+
"""
|
|
141
|
+
actual_token = get_env_or_flag(token, env_var)
|
|
142
|
+
if not actual_token:
|
|
143
|
+
raise ValueError(f"Token is required. Provide via --token or {env_var} environment variable.")
|
|
144
|
+
return actual_token
|