tunnelvt 0.1.0__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.
tunnelvt-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Vintech IDs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: tunnelvt
3
+ Version: 0.1.0
4
+ Summary: Simple tunneling — expose local apps behind Cloudflare. No login.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/vintechids/tunnelvt-py
7
+ Project-URL: Repository, https://github.com/vintechids/tunnelvt-py
8
+ Keywords: tunnel,ngrok,localhost,expose,webhook,proxy,cloudflare
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Internet :: Proxy Servers
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: websocket-client>=1.7
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Requires-Dist: pytest-cov; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # tunnelvt — Python
29
+
30
+ Simple tunneling client in Python. No login. Expose local apps at `domain/<device>/<app>`.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install tunnelvt
36
+ ```
37
+
38
+ Requires Python ≥ 3.9.
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ tunnelvt -s https://tunnel.example.com -a myapp -p 3000
44
+ ```
45
+
46
+ Output:
47
+ ```
48
+ [tunnelvt] connected — a1b2c3d4e5f6g7h8/myapp -> localhost:3000
49
+ ```
50
+
51
+ Your app is now at:
52
+
53
+ ```
54
+ https://tunnel.example.com/a1b2c3d4e5f6g7h8/myapp/
55
+ ```
56
+
57
+ ### Custom device ID
58
+
59
+ ```bash
60
+ tunnelvt -s https://tunnel.example.com -d my-laptop -a api -p 8080
61
+ # → https://tunnel.example.com/my-laptop/api/
62
+ ```
63
+
64
+ ### Library usage
65
+
66
+ ```python
67
+ from tunnelvt import TunnelVT
68
+
69
+ client = TunnelVT(
70
+ server_url="https://tunnel.example.com",
71
+ app="myapp",
72
+ port=3000,
73
+ )
74
+ client.connect() # blocks
75
+ ```
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,52 @@
1
+ # tunnelvt — Python
2
+
3
+ Simple tunneling client in Python. No login. Expose local apps at `domain/<device>/<app>`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install tunnelvt
9
+ ```
10
+
11
+ Requires Python ≥ 3.9.
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ tunnelvt -s https://tunnel.example.com -a myapp -p 3000
17
+ ```
18
+
19
+ Output:
20
+ ```
21
+ [tunnelvt] connected — a1b2c3d4e5f6g7h8/myapp -> localhost:3000
22
+ ```
23
+
24
+ Your app is now at:
25
+
26
+ ```
27
+ https://tunnel.example.com/a1b2c3d4e5f6g7h8/myapp/
28
+ ```
29
+
30
+ ### Custom device ID
31
+
32
+ ```bash
33
+ tunnelvt -s https://tunnel.example.com -d my-laptop -a api -p 8080
34
+ # → https://tunnel.example.com/my-laptop/api/
35
+ ```
36
+
37
+ ### Library usage
38
+
39
+ ```python
40
+ from tunnelvt import TunnelVT
41
+
42
+ client = TunnelVT(
43
+ server_url="https://tunnel.example.com",
44
+ app="myapp",
45
+ port=3000,
46
+ )
47
+ client.connect() # blocks
48
+ ```
49
+
50
+ ## License
51
+
52
+ MIT
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tunnelvt"
7
+ version = "0.1.0"
8
+ description = "Simple tunneling — expose local apps behind Cloudflare. No login."
9
+ license = {text = "MIT"}
10
+ readme = "README.md"
11
+ requires-python = ">=3.9"
12
+ keywords = ["tunnel", "ngrok", "localhost", "expose", "webhook", "proxy", "cloudflare"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Internet :: Proxy Servers",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ ]
25
+ dependencies = [
26
+ "websocket-client>=1.7",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = ["pytest>=8", "pytest-cov"]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/vintechids/tunnelvt-py"
34
+ Repository = "https://github.com/vintechids/tunnelvt-py"
35
+
36
+ [project.scripts]
37
+ tunnelvt = "tunnelvt.cli:main"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ """tunnelvt — Simple tunneling. No login.
2
+
3
+ Expose local apps at ``domain/<device>/<app>`` through a tunnelvt server.
4
+ """
5
+
6
+ from .client import TunnelVT
7
+
8
+ __version__ = "0.1.0"
9
+ __all__ = ["TunnelVT"]
@@ -0,0 +1,39 @@
1
+ """CLI entry point for ``tunnelvt``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import sys
8
+
9
+ from .client import TunnelVT
10
+
11
+
12
+ def main(argv: list[str] | None = None) -> None:
13
+ p = argparse.ArgumentParser(
14
+ prog="tunnelvt",
15
+ description="Expose a local port through gotunnel. No login needed.",
16
+ )
17
+ p.add_argument("-a", "--app", required=True, help="app name for this tunnel")
18
+ p.add_argument("-p", "--port", type=int, required=True, help="local port to expose")
19
+ p.add_argument("-v", "--verbose", action="store_true", help="enable debug logging")
20
+ args = p.parse_args(argv)
21
+
22
+ logging.basicConfig(
23
+ level=logging.DEBUG if args.verbose else logging.INFO,
24
+ format="[tunnelvt] %(levelname)s %(message)s",
25
+ )
26
+
27
+ client = TunnelVT(app=args.app, port=args.port)
28
+ try:
29
+ client.connect()
30
+ except KeyboardInterrupt:
31
+ print("", file=sys.stderr)
32
+ sys.exit(0)
33
+ except Exception as exc:
34
+ logging.error("%s", exc)
35
+ sys.exit(1)
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
@@ -0,0 +1,190 @@
1
+ """WebSocket tunnel client — username+password → JWT, auto-refresh.
2
+
3
+ First run prompts for username+password. Gets JWT from /_tunnel/auth.
4
+ Saves to ~/.tunnelvt.json. JWT expires 7 days — auto-refreshes with saved creds.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import getpass
11
+ import json
12
+ import logging
13
+ import sys
14
+ import threading
15
+ from http.client import HTTPConnection, HTTPResponse
16
+ from pathlib import Path
17
+ from typing import Any
18
+ from urllib.parse import urlparse, urlunparse
19
+ from urllib.request import Request, urlopen
20
+
21
+ import websocket
22
+
23
+ logger = logging.getLogger("tunnelvt")
24
+
25
+ DEFAULT_SERVER = "https://gotunnel.vinstechid.com"
26
+ VERSION = "1.0.0"
27
+ BUILD_HASH = "dev"
28
+
29
+ HOP_BY_HOP = {
30
+ "connection", "keep-alive", "proxy-authenticate",
31
+ "proxy-authorization", "te", "trailers",
32
+ "transfer-encoding", "upgrade",
33
+ }
34
+ MAX_BODY = 10 * 1024 * 1024
35
+
36
+
37
+ def _identity_file() -> Path:
38
+ return Path.home() / ".tunnelvt.json"
39
+
40
+
41
+ def _build_ws_url(server_url: str) -> str:
42
+ p = urlparse(server_url)
43
+ scheme = "wss" if p.scheme in ("https", "wss") else "ws"
44
+ return urlunparse((scheme, p.netloc, "/_tunnel/connect", "", "", ""))
45
+
46
+
47
+ def _fetch_jwt(server_url: str, username: str, password: str) -> str:
48
+ data = json.dumps({"username": username, "password": password}).encode()
49
+ req = Request(server_url + "/_tunnel/auth", data=data,
50
+ headers={"Content-Type": "application/json"})
51
+ with urlopen(req, timeout=10) as resp:
52
+ if resp.status != 200:
53
+ raise RuntimeError(resp.read().decode())
54
+ return json.loads(resp.read())["jwt"]
55
+
56
+
57
+ class TunnelVT:
58
+ """Connect to gotunnel and expose a local port."""
59
+
60
+ def __init__(self, app: str = "", port: int = 0) -> None:
61
+ self.app = app
62
+ self.port = port
63
+ self._username = ""
64
+ self._password = ""
65
+ self._jwt = ""
66
+ self._ws: websocket.WebSocket | None = None
67
+
68
+ def connect(self) -> None:
69
+ while True:
70
+ self._ensure_jwt()
71
+ try:
72
+ self._connect_ws()
73
+ return
74
+ except Exception as e:
75
+ if "invalid or expired" in str(e):
76
+ self._jwt = ""
77
+ continue
78
+ raise
79
+
80
+ def _connect_ws(self) -> None:
81
+ ws_url = _build_ws_url(DEFAULT_SERVER)
82
+ self._ws = websocket.create_connection(ws_url, timeout=30)
83
+ try:
84
+ self._register()
85
+ self._read_loop()
86
+ finally:
87
+ self._ws.close()
88
+
89
+ def _ensure_jwt(self) -> None:
90
+ idf = _identity_file()
91
+ if idf.exists():
92
+ try:
93
+ data = json.loads(idf.read_text())
94
+ if data.get("username"):
95
+ self._username = data["username"]
96
+ self._password = data.get("password", "")
97
+ self._jwt = data.get("jwt", "")
98
+ except Exception:
99
+ pass
100
+
101
+ if not self._username:
102
+ self._username = input("Username: ").strip()
103
+ if not self._username:
104
+ raise RuntimeError("username required")
105
+ self._password = getpass.getpass("Password: ").strip()
106
+ if not self._password:
107
+ raise RuntimeError("password required")
108
+
109
+ if not self._jwt:
110
+ try:
111
+ self._jwt = _fetch_jwt(DEFAULT_SERVER, self._username, self._password)
112
+ except Exception:
113
+ idf.unlink(missing_ok=True)
114
+ self._username = ""
115
+ self._password = ""
116
+ raise
117
+
118
+ idf.write_text(json.dumps({
119
+ "username": self._username,
120
+ "password": self._password,
121
+ "jwt": self._jwt,
122
+ }))
123
+
124
+ def _register(self) -> None:
125
+ msg = {
126
+ "type": "register",
127
+ "jwt": self._jwt,
128
+ "app": self.app,
129
+ "port": self.port,
130
+ "version": VERSION,
131
+ "vhash": BUILD_HASH,
132
+ }
133
+ self._send(msg)
134
+ ack = self._recv()
135
+ if ack is None:
136
+ raise RuntimeError("connection closed")
137
+ if ack.get("type") == "error":
138
+ raise RuntimeError(ack.get("error", "server rejected"))
139
+ print(f"https://gotunnel.vinstechid.com/{self._username}/{self.app}/")
140
+ logger.info("connected — %s/%s -> localhost:%d", self._username, self.app, self.port)
141
+
142
+ def _read_loop(self) -> None:
143
+ while True:
144
+ msg = self._recv()
145
+ if msg is None:
146
+ break
147
+ if msg.get("type") == "request":
148
+ t = threading.Thread(target=self._handle_request, args=(msg,), daemon=True)
149
+ t.start()
150
+
151
+ def _handle_request(self, msg: dict[str, Any]) -> None:
152
+ self._send(self._do_local(msg))
153
+
154
+ def _do_local(self, msg: dict[str, Any]) -> dict[str, Any]:
155
+ req_id = msg.get("id", "")
156
+ def err(e: str) -> dict[str, Any]:
157
+ return {"type": "error", "id": req_id, "error": e}
158
+ try:
159
+ body = base64.b64decode(msg.get("body", ""))
160
+ except Exception:
161
+ return err("invalid body encoding")
162
+ conn = HTTPConnection("localhost", self.port, timeout=25)
163
+ try:
164
+ headers = {k: v for k, v in msg.get("headers", {}).items()
165
+ if k.lower() not in HOP_BY_HOP}
166
+ conn.request(method=msg.get("method", "GET"), url=msg.get("path", "/"),
167
+ body=body if body else None, headers=headers)
168
+ resp: HTTPResponse = conn.getresponse()
169
+ resp_body = resp.read(MAX_BODY)
170
+ return {
171
+ "type": "response", "id": req_id, "status": resp.status,
172
+ "headers": dict(resp.getheaders()),
173
+ "body": base64.b64encode(resp_body).decode(),
174
+ }
175
+ except Exception as exc:
176
+ return err(f"local request failed: {exc}")
177
+ finally:
178
+ conn.close()
179
+
180
+ def _send(self, msg: dict[str, Any]) -> None:
181
+ if self._ws:
182
+ self._ws.send(json.dumps(msg))
183
+
184
+ def _recv(self) -> dict[str, Any] | None:
185
+ if self._ws is None:
186
+ return None
187
+ try:
188
+ return json.loads(self._ws.recv())
189
+ except websocket.WebSocketConnectionClosedException:
190
+ return None
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: tunnelvt
3
+ Version: 0.1.0
4
+ Summary: Simple tunneling — expose local apps behind Cloudflare. No login.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/vintechids/tunnelvt-py
7
+ Project-URL: Repository, https://github.com/vintechids/tunnelvt-py
8
+ Keywords: tunnel,ngrok,localhost,expose,webhook,proxy,cloudflare
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Internet :: Proxy Servers
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: websocket-client>=1.7
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Requires-Dist: pytest-cov; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # tunnelvt — Python
29
+
30
+ Simple tunneling client in Python. No login. Expose local apps at `domain/<device>/<app>`.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install tunnelvt
36
+ ```
37
+
38
+ Requires Python ≥ 3.9.
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ tunnelvt -s https://tunnel.example.com -a myapp -p 3000
44
+ ```
45
+
46
+ Output:
47
+ ```
48
+ [tunnelvt] connected — a1b2c3d4e5f6g7h8/myapp -> localhost:3000
49
+ ```
50
+
51
+ Your app is now at:
52
+
53
+ ```
54
+ https://tunnel.example.com/a1b2c3d4e5f6g7h8/myapp/
55
+ ```
56
+
57
+ ### Custom device ID
58
+
59
+ ```bash
60
+ tunnelvt -s https://tunnel.example.com -d my-laptop -a api -p 8080
61
+ # → https://tunnel.example.com/my-laptop/api/
62
+ ```
63
+
64
+ ### Library usage
65
+
66
+ ```python
67
+ from tunnelvt import TunnelVT
68
+
69
+ client = TunnelVT(
70
+ server_url="https://tunnel.example.com",
71
+ app="myapp",
72
+ port=3000,
73
+ )
74
+ client.connect() # blocks
75
+ ```
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/tunnelvt/__init__.py
5
+ src/tunnelvt/cli.py
6
+ src/tunnelvt/client.py
7
+ src/tunnelvt.egg-info/PKG-INFO
8
+ src/tunnelvt.egg-info/SOURCES.txt
9
+ src/tunnelvt.egg-info/dependency_links.txt
10
+ src/tunnelvt.egg-info/entry_points.txt
11
+ src/tunnelvt.egg-info/requires.txt
12
+ src/tunnelvt.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tunnelvt = tunnelvt.cli:main
@@ -0,0 +1,5 @@
1
+ websocket-client>=1.7
2
+
3
+ [dev]
4
+ pytest>=8
5
+ pytest-cov
@@ -0,0 +1 @@
1
+ tunnelvt