linksocks 3.0.12__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.

Potentially problematic release.


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

Files changed (37) hide show
  1. linksocks-3.0.12/MANIFEST.in +18 -0
  2. linksocks-3.0.12/PKG-INFO +64 -0
  3. linksocks-3.0.12/go.mod +24 -0
  4. linksocks-3.0.12/go.sum +47 -0
  5. linksocks-3.0.12/linksocks/__init__.py +9 -0
  6. linksocks-3.0.12/linksocks/_base.py +230 -0
  7. linksocks-3.0.12/linksocks/_cli.py +291 -0
  8. linksocks-3.0.12/linksocks/_client.py +291 -0
  9. linksocks-3.0.12/linksocks/_logging_config.py +182 -0
  10. linksocks-3.0.12/linksocks/_server.py +355 -0
  11. linksocks-3.0.12/linksocks/_utils.py +144 -0
  12. linksocks-3.0.12/linksocks.egg-info/PKG-INFO +64 -0
  13. linksocks-3.0.12/linksocks.egg-info/SOURCES.txt +35 -0
  14. linksocks-3.0.12/linksocks.egg-info/dependency_links.txt +1 -0
  15. linksocks-3.0.12/linksocks.egg-info/entry_points.txt +2 -0
  16. linksocks-3.0.12/linksocks.egg-info/not-zip-safe +1 -0
  17. linksocks-3.0.12/linksocks.egg-info/requires.txt +16 -0
  18. linksocks-3.0.12/linksocks.egg-info/top_level.txt +2 -0
  19. linksocks-3.0.12/linksocks_go/_python.go +358 -0
  20. linksocks-3.0.12/linksocks_go/api.go +242 -0
  21. linksocks-3.0.12/linksocks_go/batchlog.go +57 -0
  22. linksocks-3.0.12/linksocks_go/cli.go +433 -0
  23. linksocks-3.0.12/linksocks_go/client.go +1145 -0
  24. linksocks-3.0.12/linksocks_go/conn.go +137 -0
  25. linksocks-3.0.12/linksocks_go/doc.go +33 -0
  26. linksocks-3.0.12/linksocks_go/forwarder.go +376 -0
  27. linksocks-3.0.12/linksocks_go/message.go +733 -0
  28. linksocks-3.0.12/linksocks_go/portpool.go +84 -0
  29. linksocks-3.0.12/linksocks_go/relay.go +1590 -0
  30. linksocks-3.0.12/linksocks_go/server.go +1558 -0
  31. linksocks-3.0.12/linksocks_go/socket.go +102 -0
  32. linksocks-3.0.12/linksocks_go/version.go +8 -0
  33. linksocks-3.0.12/setup.cfg +4 -0
  34. linksocks-3.0.12/setup.py +912 -0
  35. linksocks-3.0.12/tests/test_crash.py +1686 -0
  36. linksocks-3.0.12/tests/test_lib.py +1002 -0
  37. linksocks-3.0.12/tests/test_wrapper.py +456 -0
@@ -0,0 +1,18 @@
1
+ # Include Go source files in linksocks_go directory
2
+ recursive-include linksocks_go *.go
3
+ include go.mod
4
+ include go.sum
5
+
6
+ # Include documentation
7
+ include README.md
8
+
9
+ # Exclude unnecessary files
10
+ global-exclude __pycache__
11
+ global-exclude *.py[co]
12
+ global-exclude .git*
13
+ global-exclude *.so
14
+ global-exclude *.dylib
15
+ global-exclude *.dll
16
+
17
+ # Exclude existing bindings directory to force rebuild
18
+ prune linksockslib
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: linksocks
3
+ Version: 3.0.12
4
+ Summary: Python bindings for LinkSocks - SOCKS proxy over WebSocket
5
+ Home-page: https://github.com/linksocks/linksocks
6
+ Author: jackzzs
7
+ Author-email: jackzzs@outlook.com
8
+ License: MIT
9
+ Project-URL: Bug Reports, https://github.com/linksocks/linksocks/issues
10
+ Project-URL: Source, https://github.com/linksocks/linksocks
11
+ Project-URL: Documentation, https://github.com/linksocks/linksocks#readme
12
+ Project-URL: Changelog, https://github.com/linksocks/linksocks/releases
13
+ Keywords: socks proxy websocket network tunneling firewall bypass load-balancing go bindings
14
+ Platform: any
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: System Administrators
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Operating System :: MacOS
20
+ Classifier: Operating System :: Microsoft :: Windows
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3.9
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Programming Language :: Go
28
+ Classifier: Topic :: Internet :: Proxy Servers
29
+ Classifier: Topic :: Internet :: WWW/HTTP
30
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
31
+ Classifier: Topic :: System :: Networking
32
+ Requires-Python: >=3.9
33
+ Description-Content-Type: text/markdown
34
+ Requires-Dist: setuptools>=40.0
35
+ Requires-Dist: click>=8.0
36
+ Requires-Dist: loguru
37
+ Requires-Dist: rich
38
+ Provides-Extra: dev
39
+ Requires-Dist: pytest>=6.0; extra == "dev"
40
+ Requires-Dist: pytest-cov>=2.10; extra == "dev"
41
+ Requires-Dist: pytest-mock>=3.0; extra == "dev"
42
+ Requires-Dist: pytest-xdist; extra == "dev"
43
+ Requires-Dist: black>=21.0; extra == "dev"
44
+ Requires-Dist: flake8>=3.8; extra == "dev"
45
+ Requires-Dist: mypy>=0.800; extra == "dev"
46
+ Requires-Dist: httpx[socks]; extra == "dev"
47
+ Requires-Dist: requests; extra == "dev"
48
+ Requires-Dist: pysocks; extra == "dev"
49
+ Dynamic: author
50
+ Dynamic: author-email
51
+ Dynamic: classifier
52
+ Dynamic: description
53
+ Dynamic: description-content-type
54
+ Dynamic: home-page
55
+ Dynamic: keywords
56
+ Dynamic: license
57
+ Dynamic: platform
58
+ Dynamic: project-url
59
+ Dynamic: provides-extra
60
+ Dynamic: requires-dist
61
+ Dynamic: requires-python
62
+ Dynamic: summary
63
+
64
+ Python bindings for LinkSocks - a SOCKS proxy implementation over WebSocket protocol.
@@ -0,0 +1,24 @@
1
+ module github.com/linksocks/linksocks
2
+
3
+ go 1.21
4
+
5
+ require (
6
+ github.com/google/uuid v1.6.0
7
+ github.com/gorilla/websocket v1.5.3
8
+ github.com/rs/zerolog v1.33.0
9
+ github.com/spf13/cobra v1.8.1
10
+ github.com/stretchr/testify v1.10.0
11
+ )
12
+
13
+ require (
14
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
15
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
16
+ github.com/kr/pretty v0.3.1 // indirect
17
+ github.com/mattn/go-colorable v0.1.13 // indirect
18
+ github.com/mattn/go-isatty v0.0.19 // indirect
19
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
20
+ github.com/spf13/pflag v1.0.5 // indirect
21
+ golang.org/x/sys v0.30.0 // indirect
22
+ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
23
+ gopkg.in/yaml.v3 v3.0.1 // indirect
24
+ )
@@ -0,0 +1,47 @@
1
+ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
2
+ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
3
+ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
4
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
5
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6
+ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
7
+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
8
+ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
9
+ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
10
+ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
11
+ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
12
+ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
13
+ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
14
+ github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
15
+ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
16
+ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
17
+ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
18
+ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
19
+ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
20
+ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
21
+ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
22
+ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
23
+ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
24
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
25
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
26
+ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
27
+ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
28
+ github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
29
+ github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
30
+ github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
31
+ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
32
+ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
33
+ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
34
+ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
35
+ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
36
+ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
37
+ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
38
+ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
40
+ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
41
+ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
42
+ golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
43
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
44
+ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
45
+ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
46
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
47
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -0,0 +1,9 @@
1
+ """linksocks: SOCKS5 over WebSocket proxy library."""
2
+
3
+ __version__ = "3.0.12"
4
+
5
+ from ._server import Server
6
+ from ._client import Client
7
+ from ._base import ReverseTokenResult, set_log_level
8
+
9
+ __all__ = ["Server", "Client", "ReverseTokenResult", "set_log_level"]
@@ -0,0 +1,230 @@
1
+ """
2
+ Base classes and utilities for linksocks.
3
+
4
+ This module contains shared functionality used by Server and Client classes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ import asyncio
12
+ from dataclasses import dataclass
13
+ import threading
14
+ import time
15
+ from datetime import timedelta
16
+ from typing import Any, Callable, Dict, Iterable, Optional, Tuple, Union, List
17
+
18
+ # Underlying Go bindings module (generated)
19
+ from linksockslib import linksocks # type: ignore
20
+
21
+ _logger = logging.getLogger(__name__)
22
+
23
+ # Type aliases
24
+ DurationLike = Union[int, float, timedelta, str]
25
+
26
+
27
+ def _snake_to_camel(name: str) -> str:
28
+ """Convert snake_case to CamelCase."""
29
+ parts = name.split("_")
30
+ return "".join(p.capitalize() for p in parts if p)
31
+
32
+
33
+ def _camel_to_snake(name: str) -> str:
34
+ """Convert CamelCase to snake_case."""
35
+ out: List[str] = []
36
+ for ch in name:
37
+ if ch.isupper() and out:
38
+ out.append("_")
39
+ out.append(ch.lower())
40
+ return "".join(out)
41
+
42
+
43
+ def _to_duration(value: Optional[DurationLike]) -> Any:
44
+ """Convert seconds/str/timedelta to Go time.Duration via bindings.
45
+
46
+ - None -> 0
47
+ - int/float -> seconds (supports fractions)
48
+ - timedelta -> total seconds
49
+ - str -> parsed by Go (e.g., "1.5s", "300ms")
50
+ """
51
+ if value is None:
52
+ return 0
53
+ if isinstance(value, timedelta):
54
+ seconds = value.total_seconds()
55
+ return seconds * linksocks.Second()
56
+ if isinstance(value, (int, float)):
57
+ return value * linksocks.Second()
58
+ if isinstance(value, str):
59
+ try:
60
+ return linksocks.ParseDuration(value)
61
+ except Exception as exc:
62
+ raise ValueError(f"Invalid duration string: {value}") from exc
63
+ raise TypeError(f"Unsupported duration type: {type(value)!r}")
64
+
65
+
66
+ # Shared Go->Python log dispatcher
67
+ _def_level_map = {
68
+ "trace": logging.DEBUG,
69
+ "debug": logging.DEBUG,
70
+ "info": logging.INFO,
71
+ "warn": logging.WARNING,
72
+ "warning": logging.WARNING,
73
+ "error": logging.ERROR,
74
+ "fatal": logging.CRITICAL,
75
+ "panic": logging.CRITICAL,
76
+ }
77
+
78
+
79
+ def _emit_go_log(py_logger: logging.Logger, line: str) -> None:
80
+ """Process a Go log line and emit it to the Python logger."""
81
+ try:
82
+ obj = json.loads(line)
83
+ except Exception:
84
+ py_logger.info(line)
85
+ return
86
+ level = str(obj.get("level", "")).lower()
87
+ message = obj.get("message") or obj.get("msg") or ""
88
+ extras: Dict[str, Any] = {}
89
+ for k, v in obj.items():
90
+ if k in ("level", "time", "message", "msg"):
91
+ continue
92
+ extras[k] = v
93
+ py_logger.log(_def_level_map.get(level, logging.INFO), message, extra={"go": extras})
94
+
95
+
96
+ # Global registry for logger instances
97
+ _logger_registry: Dict[str, logging.Logger] = {}
98
+
99
+ # Event-driven log monitoring system
100
+ _log_listeners: List[Callable[[List], None]] = []
101
+ _listener_thread: Optional[threading.Thread] = None
102
+ _listener_active: bool = False
103
+
104
+
105
+ def _start_log_listener() -> None:
106
+ """Start background thread to drain Go log buffer and forward to Python loggers."""
107
+ global _listener_thread, _listener_active
108
+ if _listener_active and _listener_thread and _listener_thread.is_alive():
109
+ return
110
+ _listener_active = True
111
+
112
+ def _run() -> None:
113
+ # Drain loop: wait for entries with timeout to allow graceful shutdown
114
+ while _listener_active:
115
+ try:
116
+ entries = linksocks.WaitForLogEntries(2000) # wait up to 2s
117
+ except Exception:
118
+ # Backoff on unexpected errors to avoid busy loop
119
+ time.sleep(0.2)
120
+ continue
121
+
122
+ if not entries:
123
+ continue
124
+
125
+ # Iterate returned entries; handle both attr and dict styles
126
+ for entry in entries:
127
+ try:
128
+ logger_id = getattr(entry, "LoggerID", None)
129
+ if logger_id is None and isinstance(entry, dict):
130
+ logger_id = entry.get("LoggerID")
131
+
132
+ message = getattr(entry, "Message", None)
133
+ if message is None and isinstance(entry, dict):
134
+ message = entry.get("Message")
135
+
136
+ if not message:
137
+ continue
138
+
139
+ py_logger = _logger_registry.get(str(logger_id)) or _logger
140
+ _emit_go_log(py_logger, str(message))
141
+ except Exception:
142
+ # Never let logging path crash the listener
143
+ continue
144
+
145
+ _listener_thread = threading.Thread(target=_run, name="linksocks-go-log-listener", daemon=True)
146
+ _listener_thread.start()
147
+
148
+
149
+ def _stop_log_listener() -> None:
150
+ """Stop the background log listener thread."""
151
+ global _listener_active
152
+ _listener_active = False
153
+ try:
154
+ # Unblock WaitForLogEntries callers
155
+ linksocks.CancelLogWaiters()
156
+ except Exception:
157
+ pass
158
+
159
+
160
+ class BufferZerologLogger:
161
+ """Buffer-based logger system for Go bindings."""
162
+
163
+ def __init__(self, py_logger: logging.Logger, logger_id: str):
164
+ self.py_logger = py_logger
165
+ self.logger_id = logger_id
166
+ # Ensure background listener is running
167
+ _start_log_listener()
168
+
169
+ # Prefer Go logger with explicit ID so we can map entries back
170
+ try:
171
+ # Newer binding that tags entries with our provided ID
172
+ self.go_logger = linksocks.NewLoggerWithID(self.logger_id)
173
+ except Exception:
174
+ # Fallback to older API; if present, still try callback path
175
+ try:
176
+ def log_callback(line: str) -> None:
177
+ _emit_go_log(py_logger, line)
178
+
179
+ self.go_logger = linksocks.NewLogger(log_callback)
180
+ except Exception:
181
+ # As a last resort, create a default Go logger
182
+ self.go_logger = linksocks.NewLoggerWithID(self.logger_id) # may still raise; surface to caller
183
+ _logger_registry[logger_id] = py_logger
184
+
185
+ def cleanup(self):
186
+ """Clean up logger resources."""
187
+ if self.logger_id in _logger_registry:
188
+ del _logger_registry[self.logger_id]
189
+
190
+
191
+ @dataclass
192
+ class ReverseTokenResult:
193
+ """Result of adding a reverse token."""
194
+ token: str
195
+ port: int
196
+
197
+
198
+ class _SnakePassthrough:
199
+ """Mixin to map snake_case attribute access to underlying CamelCase.
200
+
201
+ Only used when an explicit Pythonic method/attribute is not defined.
202
+ """
203
+
204
+ def __getattr__(self, name: str) -> Any:
205
+ raw = super().__getattribute__("_raw") # type: ignore[attr-defined]
206
+ camel = _snake_to_camel(name)
207
+ try:
208
+ return getattr(raw, camel)
209
+ except AttributeError:
210
+ raise
211
+
212
+ def __dir__(self) -> List[str]:
213
+ # Expose snake_case versions of underlying CamelCase for IDEs
214
+ names = set(super().__dir__())
215
+ try:
216
+ raw = super().__getattribute__("_raw") # type: ignore[attr-defined]
217
+ for attr in dir(raw):
218
+ if not attr or attr.startswith("_"):
219
+ continue
220
+ names.add(_camel_to_snake(attr))
221
+ except Exception:
222
+ pass
223
+ return sorted(names)
224
+
225
+
226
+ def set_log_level(level: Union[int, str]) -> None:
227
+ """Set the global log level for linksocks."""
228
+ if isinstance(level, str):
229
+ level = getattr(logging, level.upper())
230
+ _logger.setLevel(level)
@@ -0,0 +1,291 @@
1
+ """
2
+ Command-line interface for linksocks.
3
+
4
+ This module provides all CLI commands for the linksocks SOCKS5 over WebSocket proxy tool.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ import platform
10
+ from typing import Optional
11
+
12
+ import click
13
+
14
+ from ._logging_config import init_logging
15
+ from ._utils import get_env_or_flag, parse_socks_proxy, validate_required_token
16
+
17
+
18
+ @click.group()
19
+ def cli():
20
+ """SOCKS5 over WebSocket proxy tool."""
21
+ pass
22
+
23
+
24
+ @click.command()
25
+ def version():
26
+ """Print version and platform information."""
27
+ try:
28
+ from linksocks import __version__
29
+ version_str = __version__
30
+ except ImportError:
31
+ version_str = "unknown"
32
+
33
+ platform_str = platform.platform()
34
+ click.echo(f"linksocks version {version_str} {platform_str}")
35
+
36
+
37
+ @click.command()
38
+ @click.option("--token", "-t", help="Authentication token (env: LINKSOCKS_TOKEN)")
39
+ @click.option("--url", "-u", default="ws://localhost:8765", help="WebSocket server address")
40
+ @click.option("--reverse", "-r", is_flag=True, default=False, help="Use reverse SOCKS5 proxy")
41
+ @click.option("--connector-token", "-c", default=None, help="Connector token for reverse proxy (env: LINKSOCKS_CONNECTOR_TOKEN)")
42
+ @click.option("--socks-host", "-s", default="127.0.0.1", help="SOCKS5 server listen address")
43
+ @click.option("--socks-port", "-p", default=9870, help="SOCKS5 server listen port")
44
+ @click.option("--socks-username", "-n", help="SOCKS5 authentication username")
45
+ @click.option("--socks-password", "-w", help="SOCKS5 authentication password (env: LINKSOCKS_SOCKS_PASSWORD)")
46
+ @click.option("--socks-no-wait", "-i", is_flag=True, default=False, help="Start SOCKS server immediately")
47
+ @click.option("--no-reconnect", "-R", is_flag=True, default=False, help="Stop when server disconnects")
48
+ @click.option("--debug", "-d", count=True, help="Debug logging (-d for debug, -dd for trace)")
49
+ @click.option("--threads", "-T", default=1, help="Number of threads for data transfer")
50
+ @click.option("--upstream-proxy", "-x", help="Upstream SOCKS5 proxy (socks5://[user:pass@]host[:port])")
51
+ @click.option("--fast-open", "-f", is_flag=True, default=False, help="Assume connection success and allow data transfer immediately")
52
+ @click.option("--no-env-proxy", "-E", is_flag=True, default=False, help="Ignore proxy env vars for WebSocket connection")
53
+ def client(
54
+ token: Optional[str],
55
+ url: str,
56
+ reverse: bool,
57
+ connector_token: Optional[str],
58
+ socks_host: str,
59
+ socks_port: int,
60
+ socks_username: Optional[str],
61
+ socks_password: Optional[str],
62
+ socks_no_wait: bool,
63
+ no_reconnect: bool,
64
+ debug: int,
65
+ threads: int,
66
+ upstream_proxy: Optional[str],
67
+ fast_open: bool,
68
+ no_env_proxy: bool,
69
+ ):
70
+ """Start SOCKS5 over WebSocket proxy client."""
71
+ from ._client import Client
72
+
73
+ async def main():
74
+ # Get values from flags or environment
75
+ actual_token = get_env_or_flag(token, "LINKSOCKS_TOKEN")
76
+ actual_connector_token = get_env_or_flag(connector_token, "LINKSOCKS_CONNECTOR_TOKEN")
77
+ actual_socks_password = get_env_or_flag(socks_password, "LINKSOCKS_SOCKS_PASSWORD")
78
+
79
+ # Validate required token
80
+ try:
81
+ validated_token = validate_required_token(actual_token)
82
+ except ValueError as e:
83
+ raise click.ClickException(str(e)) from e
84
+
85
+ # Setup logging
86
+ if debug == 0:
87
+ log_level = logging.INFO
88
+ elif debug == 1:
89
+ log_level = logging.DEBUG
90
+ else:
91
+ log_level = 5 # TRACE level
92
+
93
+ init_logging(level=log_level)
94
+
95
+ # Parse upstream proxy
96
+ if upstream_proxy:
97
+ try:
98
+ upstream_host, upstream_username, upstream_password = parse_socks_proxy(upstream_proxy)
99
+ except ValueError as e:
100
+ raise click.ClickException(f"Invalid upstream proxy: {e}") from e
101
+ else:
102
+ upstream_host = upstream_username = upstream_password = None
103
+
104
+ # Create client
105
+ client = Client(
106
+ ws_url=url,
107
+ token=validated_token,
108
+ reverse=reverse,
109
+ socks_host=socks_host,
110
+ socks_port=socks_port,
111
+ socks_username=socks_username,
112
+ socks_password=actual_socks_password,
113
+ socks_wait_server=not socks_no_wait,
114
+ reconnect=not no_reconnect,
115
+ upstream_proxy=upstream_host,
116
+ upstream_username=upstream_username,
117
+ upstream_password=upstream_password,
118
+ threads=threads,
119
+ fast_open=fast_open,
120
+ no_env_proxy=no_env_proxy,
121
+ )
122
+
123
+ # Add connector for reverse mode
124
+ if actual_connector_token and reverse:
125
+ await client.async_add_connector(actual_connector_token)
126
+ await asyncio.sleep(0.2)
127
+
128
+ # Run client
129
+ async with client:
130
+ await asyncio.Future()
131
+
132
+ asyncio.run(main())
133
+
134
+
135
+ @click.command()
136
+ @click.option("--ws-host", "-H", default="0.0.0.0", help="WebSocket server listen address")
137
+ @click.option("--ws-port", "-P", default=8765, help="WebSocket server listen port")
138
+ @click.option("--token", "-t", default=None, help="Auth token, auto-generated if not provided (env: LINKSOCKS_TOKEN)")
139
+ @click.option("--connector-token", "-c", default=None, help="Connector token for reverse proxy (env: LINKSOCKS_CONNECTOR_TOKEN)")
140
+ @click.option("--connector-autonomy", "-a", is_flag=True, default=False, help="Allow clients to manage connector tokens")
141
+ @click.option("--buffer-size", "-b", default=4096, help="Buffer size for data transfer")
142
+ @click.option("--reverse", "-r", is_flag=True, default=False, help="Use reverse SOCKS5 proxy")
143
+ @click.option("--socks-host", "-s", default="127.0.0.1", help="SOCKS5 server listen address for reverse proxy")
144
+ @click.option("--socks-port", "-p", default=9870, help="SOCKS5 server listen port for reverse proxy")
145
+ @click.option("--socks-username", "-n", help="SOCKS5 username for authentication")
146
+ @click.option("--socks-password", "-w", help="SOCKS5 password for authentication (env: LINKSOCKS_SOCKS_PASSWORD)")
147
+ @click.option("--socks-nowait", "-i", is_flag=True, default=False, help="Start SOCKS server immediately")
148
+ @click.option("--debug", "-d", count=True, help="Debug logging (-d for debug, -dd for trace)")
149
+ @click.option("--api-key", "-k", help="Enable HTTP API with specified key")
150
+ @click.option("--upstream-proxy", "-x", help="Upstream SOCKS5 proxy (socks5://[user:pass@]host[:port])")
151
+ @click.option("--fast-open", "-f", is_flag=True, default=False, help="Assume connection success and allow data transfer immediately")
152
+ def server(
153
+ ws_host: str,
154
+ ws_port: int,
155
+ token: Optional[str],
156
+ connector_token: Optional[str],
157
+ connector_autonomy: bool,
158
+ buffer_size: int,
159
+ reverse: bool,
160
+ socks_host: str,
161
+ socks_port: int,
162
+ socks_username: Optional[str],
163
+ socks_password: Optional[str],
164
+ socks_nowait: bool,
165
+ debug: int,
166
+ api_key: Optional[str],
167
+ upstream_proxy: Optional[str],
168
+ fast_open: bool,
169
+ ):
170
+ """Start SOCKS5 over WebSocket proxy server."""
171
+ from ._server import Server
172
+
173
+ async def main():
174
+ # Get values from flags or environment
175
+ actual_token = get_env_or_flag(token, "LINKSOCKS_TOKEN")
176
+ actual_connector_token = get_env_or_flag(connector_token, "LINKSOCKS_CONNECTOR_TOKEN")
177
+ actual_socks_password = get_env_or_flag(socks_password, "LINKSOCKS_SOCKS_PASSWORD")
178
+
179
+ # Setup logging
180
+ if debug == 0:
181
+ log_level = logging.INFO
182
+ elif debug == 1:
183
+ log_level = logging.DEBUG
184
+ else:
185
+ log_level = 5 # TRACE level
186
+
187
+ init_logging(level=log_level)
188
+
189
+ # Parse upstream proxy
190
+ if upstream_proxy:
191
+ try:
192
+ upstream_host, upstream_username, upstream_password = parse_socks_proxy(upstream_proxy)
193
+ except ValueError as e:
194
+ raise click.ClickException(f"Invalid upstream proxy: {e}") from e
195
+ else:
196
+ upstream_host = upstream_username = upstream_password = None
197
+
198
+ # Create server
199
+ server = Server(
200
+ ws_host=ws_host,
201
+ ws_port=ws_port,
202
+ socks_host=socks_host,
203
+ socks_wait_client=not socks_nowait,
204
+ upstream_proxy=upstream_host,
205
+ upstream_username=upstream_username,
206
+ upstream_password=upstream_password,
207
+ buffer_size=buffer_size,
208
+ api_key=api_key,
209
+ fast_open=fast_open,
210
+ )
211
+
212
+ # Configure tokens if no API key
213
+ if not api_key:
214
+ if reverse:
215
+ result = await server.async_add_reverse_token(
216
+ token=actual_token,
217
+ port=socks_port,
218
+ username=socks_username,
219
+ password=actual_socks_password,
220
+ allow_manage_connector=connector_autonomy,
221
+ )
222
+ use_token = result.token
223
+
224
+ if not connector_autonomy:
225
+ use_connector_token = await server.async_add_connector_token(
226
+ actual_connector_token, use_token
227
+ )
228
+ await asyncio.sleep(0.2)
229
+
230
+ server.log.info("Configuration:")
231
+ server.log.info(" Mode: reverse proxy (SOCKS5 on server -> client -> network)")
232
+ server.log.info(f" Token: {use_token}")
233
+ server.log.info(f" SOCKS5 port: {result.port}")
234
+ if socks_username and actual_socks_password:
235
+ server.log.info(f" SOCKS5 username: {socks_username}")
236
+ if not connector_autonomy:
237
+ server.log.info(f" Connector Token: {use_connector_token}")
238
+ if connector_autonomy:
239
+ server.log.info(" Connector autonomy: enabled")
240
+ else:
241
+ use_token = await server.async_add_forward_token(actual_token)
242
+ await asyncio.sleep(0.2)
243
+ server.log.info("Configuration:")
244
+ server.log.info(" Mode: forward proxy (SOCKS5 on client -> server -> network)")
245
+ server.log.info(f" Token: {use_token}")
246
+
247
+ # Run server
248
+ async with server:
249
+ await asyncio.Future()
250
+
251
+ asyncio.run(main())
252
+
253
+
254
+ def create_provider_command():
255
+ """Create provider command as alias for client -r."""
256
+ def provider_wrapper(*args, **kwargs):
257
+ kwargs['reverse'] = True
258
+ return client(*args, **kwargs)
259
+
260
+ return click.Command(
261
+ name="provider",
262
+ callback=provider_wrapper,
263
+ params=client.params.copy(),
264
+ help="Start reverse SOCKS5 proxy client (alias for 'client -r')"
265
+ )
266
+
267
+
268
+ def create_connector_command():
269
+ """Create connector command as alias for client."""
270
+ return click.Command(
271
+ name="connector",
272
+ callback=client,
273
+ params=client.params.copy(),
274
+ help="Start SOCKS5 proxy client (alias for 'client')"
275
+ )
276
+
277
+
278
+ # Create command aliases
279
+ provider_cmd = create_provider_command()
280
+ connector_cmd = create_connector_command()
281
+
282
+ # Register commands
283
+ cli.add_command(client)
284
+ cli.add_command(connector_cmd)
285
+ cli.add_command(provider_cmd)
286
+ cli.add_command(server)
287
+ cli.add_command(version)
288
+
289
+
290
+ if __name__ == "__main__":
291
+ cli()