tunnel-client 1.0.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.
- tunnel_client-1.0.0/MANIFEST.in +3 -0
- tunnel_client-1.0.0/PKG-INFO +17 -0
- tunnel_client-1.0.0/README.md +113 -0
- tunnel_client-1.0.0/client/__init__.py +2 -0
- tunnel_client-1.0.0/client/client.py +208 -0
- tunnel_client-1.0.0/client/client_config.yaml.example +16 -0
- tunnel_client-1.0.0/client/control_bus.py +19 -0
- tunnel_client-1.0.0/client/dashboard.py +430 -0
- tunnel_client-1.0.0/client/database.py +72 -0
- tunnel_client-1.0.0/client/models.py +72 -0
- tunnel_client-1.0.0/client/static/index.html +1391 -0
- tunnel_client-1.0.0/client/static/js/api.js +99 -0
- tunnel_client-1.0.0/client/static/js/codegen.js +28 -0
- tunnel_client-1.0.0/client/static/js/main.js +88 -0
- tunnel_client-1.0.0/client/static/js/ui.js +582 -0
- tunnel_client-1.0.0/client/static/js/utils.js +79 -0
- tunnel_client-1.0.0/client/tunnel_client.py +497 -0
- tunnel_client-1.0.0/client/utils.py +45 -0
- tunnel_client-1.0.0/setup.cfg +4 -0
- tunnel_client-1.0.0/setup.py +27 -0
- tunnel_client-1.0.0/shared/__init__.py +6 -0
- tunnel_client-1.0.0/shared/protocol.py +104 -0
- tunnel_client-1.0.0/tunnel_client.egg-info/PKG-INFO +17 -0
- tunnel_client-1.0.0/tunnel_client.egg-info/SOURCES.txt +26 -0
- tunnel_client-1.0.0/tunnel_client.egg-info/dependency_links.txt +1 -0
- tunnel_client-1.0.0/tunnel_client.egg-info/entry_points.txt +2 -0
- tunnel_client-1.0.0/tunnel_client.egg-info/requires.txt +9 -0
- tunnel_client-1.0.0/tunnel_client.egg-info/top_level.txt +2 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tunnel-client
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Tunnel client for tunnel.tunneloon.online
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: click>=8.0.0
|
|
7
|
+
Requires-Dist: rich>=13.0.0
|
|
8
|
+
Requires-Dist: websockets>=12.0
|
|
9
|
+
Requires-Dist: httpx>=0.25.0
|
|
10
|
+
Requires-Dist: fastapi>=0.104.0
|
|
11
|
+
Requires-Dist: uvicorn>=0.24.0
|
|
12
|
+
Requires-Dist: sqlalchemy>=2.0.0
|
|
13
|
+
Requires-Dist: aiosqlite>=0.19.0
|
|
14
|
+
Requires-Dist: pyyaml>=6.0
|
|
15
|
+
Dynamic: requires-dist
|
|
16
|
+
Dynamic: requires-python
|
|
17
|
+
Dynamic: summary
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Tunnel System
|
|
2
|
+
|
|
3
|
+
## Клиент: установка и запуск
|
|
4
|
+
|
|
5
|
+
### Вариант 1. Установка из PyPI (рекомендуется)
|
|
6
|
+
|
|
7
|
+
После каждого изменения в `client/` пакет автоматически публикуется в PyPI.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pipx install tunnel-client
|
|
11
|
+
# или
|
|
12
|
+
pip install tunnel-client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Вариант 2. Установка из GitLab Package Registry
|
|
16
|
+
|
|
17
|
+
После каждого изменения в `client/` пакет также публикуется в GitLab Package Registry.
|
|
18
|
+
|
|
19
|
+
**Для публичного проекта:**
|
|
20
|
+
```bash
|
|
21
|
+
pipx install tunnel-client \
|
|
22
|
+
--index-url https://gitlab.com/api/v4/projects/PROJECT_ID/packages/pypi/simple \
|
|
23
|
+
--extra-index-url https://pypi.org/simple
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Для приватного проекта:**
|
|
27
|
+
1. Создайте Personal Access Token с правами `read_api` и `read_repository`
|
|
28
|
+
2. Установите:
|
|
29
|
+
```bash
|
|
30
|
+
pipx install tunnel-client \
|
|
31
|
+
--index-url https://__token__:YOUR_TOKEN@gitlab.com/api/v4/projects/PROJECT_ID/packages/pypi/simple \
|
|
32
|
+
--extra-index-url https://pypi.org/simple
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Вариант 3. Установка из артефакта (wheel)
|
|
36
|
+
1. После пуша в `main/master` с изменениями в `client/` скачайте wheel из артефактов job `build_client`
|
|
37
|
+
2. Установите:
|
|
38
|
+
```bash
|
|
39
|
+
pipx install dist/tunnel_client-*.whl
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Вариант 4. Установка из исходников
|
|
43
|
+
```bash
|
|
44
|
+
pip install .
|
|
45
|
+
# или в editable-режиме для разработки
|
|
46
|
+
pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Запуск клиента
|
|
50
|
+
```bash
|
|
51
|
+
tunnel-client --port 3000
|
|
52
|
+
```
|
|
53
|
+
Сервер по умолчанию: `wss://tunnel.tunneloon.online` (можно переопределить через `--server`)
|
|
54
|
+
|
|
55
|
+
## Сервер: деплой в Docker
|
|
56
|
+
|
|
57
|
+
### Автоматический деплой
|
|
58
|
+
|
|
59
|
+
При каждом изменении в `server_/` или `shared/`:
|
|
60
|
+
1. Автоматически собирается Docker образ
|
|
61
|
+
2. Образ пушится в GitLab Container Registry
|
|
62
|
+
3. Автоматически деплоится на сервер (перезапускается контейнер)
|
|
63
|
+
|
|
64
|
+
**Требуется настройка:**
|
|
65
|
+
- GitLab CI/CD Variables: `SSH_PRIVATE_KEY`, `SSH_USER`, `SERVER_HOST`
|
|
66
|
+
- На сервере должны быть созданы директории: `/opt/tunnel-server/{config.yaml,data,certs}`
|
|
67
|
+
|
|
68
|
+
### Ручной запуск контейнера
|
|
69
|
+
```bash
|
|
70
|
+
docker run -d \
|
|
71
|
+
--name tunnel-server \
|
|
72
|
+
--restart unless-stopped \
|
|
73
|
+
--network host \
|
|
74
|
+
-v /opt/tunnel-server/config.yaml:/app/config.yaml:ro \
|
|
75
|
+
-v /opt/tunnel-server/certs:/app/certs:ro \
|
|
76
|
+
-v /opt/tunnel-server/data:/app/data \
|
|
77
|
+
-e SERVER_CONFIG=/app/config.yaml \
|
|
78
|
+
registry.gitlab.com/YOUR_PROJECT/server:latest
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## CI/CD (GitLab)
|
|
82
|
+
|
|
83
|
+
### Автоматические процессы:
|
|
84
|
+
|
|
85
|
+
**При изменении `server_/` или `shared/`:**
|
|
86
|
+
- `build_server_image` — сборка Docker образа
|
|
87
|
+
- `deploy_server_docker` — автоматический деплой на сервер
|
|
88
|
+
|
|
89
|
+
**При изменении `client/`, `shared/`, `setup.py` или `MANIFEST.in`:**
|
|
90
|
+
- `build_client` — сборка wheel + sdist (артефакты)
|
|
91
|
+
- `publish_client_gitlab` — автоматическая публикация в GitLab Package Registry
|
|
92
|
+
- `publish_client_pypi` — автоматическая публикация в PyPI
|
|
93
|
+
|
|
94
|
+
### Переменные для деплоя сервера
|
|
95
|
+
Задайте в GitLab → Settings → CI/CD → Variables:
|
|
96
|
+
- `SSH_PRIVATE_KEY` — приватный ключ для SSH
|
|
97
|
+
- `SSH_USER` — пользователь на сервере
|
|
98
|
+
- `SERVER_HOST` — адрес сервера
|
|
99
|
+
- (опц.) `SERVER_CONFIG_REMOTE`, `SERVER_CERTS_REMOTE`, `SERVER_DATA_REMOTE`, `SERVER_CONTAINER_NAME`, `SERVER_PORT_HTTP`, `SSH_PORT`
|
|
100
|
+
|
|
101
|
+
### Переменные для публикации в PyPI
|
|
102
|
+
Задайте в GitLab → Settings → CI/CD → Variables:
|
|
103
|
+
- `PYPI_USERNAME` — должно быть `__token__` (буквально так)
|
|
104
|
+
- `PYPI_PASSWORD` — ваш PyPI API token (создайте на https://pypi.org/manage/account/token/)
|
|
105
|
+
|
|
106
|
+
## Разработка
|
|
107
|
+
```bash
|
|
108
|
+
make install-dev # инструменты разработки (ruff, black, mypy, build)
|
|
109
|
+
make pre-commit-install
|
|
110
|
+
make lint # ruff
|
|
111
|
+
make fmt # black
|
|
112
|
+
make build-client # сборка пакета клиента
|
|
113
|
+
```
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import uvicorn
|
|
10
|
+
import yaml
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from sqlalchemy import select
|
|
13
|
+
|
|
14
|
+
from .database import init_db, get_session
|
|
15
|
+
from .models import Settings
|
|
16
|
+
from .tunnel_client import TunnelClient
|
|
17
|
+
from .dashboard import app as dashboard_app
|
|
18
|
+
from .utils import normalize_server_url, generate_random_subdomain
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
# Default server URL - hardcoded
|
|
24
|
+
DEFAULT_SERVER_URL = "wss://tunnel.tunneloon.online"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_config(path: Optional[str]) -> dict:
|
|
28
|
+
cfg_path = path or str(Path(__file__).with_name("client_config.yaml"))
|
|
29
|
+
if Path(cfg_path).exists():
|
|
30
|
+
with open(cfg_path, "r") as f:
|
|
31
|
+
return yaml.safe_load(f)
|
|
32
|
+
return {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def get_or_create_settings() -> tuple[Optional[str], Optional[int], str]:
|
|
36
|
+
"""Get server_url, local_port, and subdomain from Settings, or generate defaults."""
|
|
37
|
+
async for session in get_session():
|
|
38
|
+
res = await session.execute(select(Settings).order_by(Settings.id.asc()))
|
|
39
|
+
settings = res.scalars().first()
|
|
40
|
+
|
|
41
|
+
server_url = None
|
|
42
|
+
local_port = None
|
|
43
|
+
subdomain = None
|
|
44
|
+
|
|
45
|
+
if settings:
|
|
46
|
+
server_url = settings.server_url
|
|
47
|
+
local_port = settings.local_port
|
|
48
|
+
subdomain = settings.selected_subdomain
|
|
49
|
+
|
|
50
|
+
# If missing, generate random subdomain
|
|
51
|
+
if not subdomain:
|
|
52
|
+
subdomain = generate_random_subdomain()
|
|
53
|
+
if settings:
|
|
54
|
+
settings.selected_subdomain = subdomain
|
|
55
|
+
await session.commit()
|
|
56
|
+
else:
|
|
57
|
+
new_settings = Settings(selected_subdomain=subdomain)
|
|
58
|
+
session.add(new_settings)
|
|
59
|
+
await session.commit()
|
|
60
|
+
|
|
61
|
+
return server_url, local_port, subdomain
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@click.group(invoke_without_command=True)
|
|
65
|
+
@click.pass_context
|
|
66
|
+
@click.option("--server", "server", "-s", type=str, required=False, hidden=True, help="Server URL override (advanced, not recommended)")
|
|
67
|
+
@click.option("--port", "port", "-p", type=int, required=False, help="Local port to expose")
|
|
68
|
+
@click.option("--config", "config_path", type=str, required=False, help="Path to client_config.yaml (advanced)")
|
|
69
|
+
def cli(ctx: click.Context, server: Optional[str], port: Optional[int], config_path: Optional[str]):
|
|
70
|
+
"""Tunnel client for tunnel.tunneloon.online"""
|
|
71
|
+
# If no subcommand was invoked, run start by default
|
|
72
|
+
if ctx.invoked_subcommand is None:
|
|
73
|
+
ctx.invoke(start, server=server, port=port, config_path=config_path)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@cli.command()
|
|
77
|
+
@click.option("--server", "server", "-s", type=str, required=False, hidden=True, help="Server URL override (advanced, not recommended)")
|
|
78
|
+
@click.option("--port", "port", "-p", type=int, required=False, help="Local port to expose")
|
|
79
|
+
@click.option("--config", "config_path", type=str, required=False, help="Path to client_config.yaml (advanced)")
|
|
80
|
+
def start(server: Optional[str], port: Optional[int], config_path: Optional[str]):
|
|
81
|
+
"""
|
|
82
|
+
Start tunnel client and local dashboard.
|
|
83
|
+
|
|
84
|
+
Simplest usage:
|
|
85
|
+
tunnel-client --port 3000
|
|
86
|
+
|
|
87
|
+
Server is pre-configured to tunnel.tunneloon.online.
|
|
88
|
+
If port not specified, uses settings from dashboard.
|
|
89
|
+
"""
|
|
90
|
+
cfg = load_config(config_path)
|
|
91
|
+
|
|
92
|
+
# Load settings from DB
|
|
93
|
+
async def load_settings():
|
|
94
|
+
await init_db(cfg.get("database", {}).get("path"))
|
|
95
|
+
return await get_or_create_settings()
|
|
96
|
+
|
|
97
|
+
db_server_url, db_local_port, db_subdomain = asyncio.run(load_settings())
|
|
98
|
+
|
|
99
|
+
# Server URL: CLI override -> DB -> default (hardcoded)
|
|
100
|
+
if server:
|
|
101
|
+
# Only allow override for advanced users
|
|
102
|
+
server_url = normalize_server_url(server)
|
|
103
|
+
console.print(f"[yellow]Warning:[/yellow] Using custom server URL: {server_url}")
|
|
104
|
+
else:
|
|
105
|
+
# Use default hardcoded server
|
|
106
|
+
server_url = DEFAULT_SERVER_URL
|
|
107
|
+
|
|
108
|
+
if port is None:
|
|
109
|
+
port = db_local_port or cfg.get("client", {}).get("local_port")
|
|
110
|
+
|
|
111
|
+
if not port:
|
|
112
|
+
console.print("[red]Error:[/red] Local port is required. Use --port or configure in dashboard.")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# Use subdomain from DB (might be auto-generated)
|
|
116
|
+
subdomain = db_subdomain or generate_random_subdomain()
|
|
117
|
+
|
|
118
|
+
dashboard_enabled = cfg.get("dashboard", {}).get("enabled", True)
|
|
119
|
+
dashboard_port = int(cfg.get("dashboard", {}).get("port", 8080))
|
|
120
|
+
|
|
121
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
122
|
+
|
|
123
|
+
# Save settings to DB for future use (server_url is not saved, it's hardcoded)
|
|
124
|
+
async def save_settings():
|
|
125
|
+
async for session in get_session():
|
|
126
|
+
res = await session.execute(select(Settings).order_by(Settings.id.asc()))
|
|
127
|
+
settings = res.scalars().first()
|
|
128
|
+
if not settings:
|
|
129
|
+
settings = Settings()
|
|
130
|
+
session.add(settings)
|
|
131
|
+
|
|
132
|
+
# Don't save server_url - it's hardcoded
|
|
133
|
+
settings.local_port = port
|
|
134
|
+
settings.selected_subdomain = subdomain
|
|
135
|
+
await session.commit()
|
|
136
|
+
|
|
137
|
+
asyncio.run(save_settings())
|
|
138
|
+
|
|
139
|
+
console.print(f"[green]🚀 Starting tunnel...[/green]")
|
|
140
|
+
console.print(f"[blue]Server:[/blue] {server_url}")
|
|
141
|
+
console.print(f"[blue]WebSocket:[/blue] {server_url}/tunnel/connect")
|
|
142
|
+
console.print(f"[blue]Subdomain:[/blue] {subdomain}")
|
|
143
|
+
console.print(f"[blue]Public URL:[/blue] https://{subdomain}.tunnel.tunneloon.online")
|
|
144
|
+
console.print(f"[blue]Local:[/blue] localhost:{port}")
|
|
145
|
+
console.print(f"[yellow]Dashboard:[/yellow] http://127.0.0.1:{dashboard_port}")
|
|
146
|
+
console.print(f"[dim]Managing tunnel via dashboard at http://127.0.0.1:{dashboard_port}[/dim]")
|
|
147
|
+
|
|
148
|
+
async def runner():
|
|
149
|
+
client = TunnelClient()
|
|
150
|
+
tasks: list[asyncio.Task] = []
|
|
151
|
+
tunnel_task = asyncio.create_task(client.connect(server_url, subdomain, port))
|
|
152
|
+
tasks.append(tunnel_task)
|
|
153
|
+
server_obj = None
|
|
154
|
+
if dashboard_enabled:
|
|
155
|
+
dashboard_app.state.local_port = port
|
|
156
|
+
dashboard_app.state.tunnel_client = client # Store reference for status checks
|
|
157
|
+
dashboard_app.state.connection_status = "connecting" # Initial status
|
|
158
|
+
config = uvicorn.Config(dashboard_app, host="127.0.0.1", port=dashboard_port, log_level="info")
|
|
159
|
+
server_obj = uvicorn.Server(config)
|
|
160
|
+
server_task = asyncio.create_task(server_obj.serve())
|
|
161
|
+
tasks.append(server_task)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
|
165
|
+
except asyncio.CancelledError:
|
|
166
|
+
pass
|
|
167
|
+
finally:
|
|
168
|
+
await client.stop()
|
|
169
|
+
if server_obj is not None:
|
|
170
|
+
server_obj.should_exit = True
|
|
171
|
+
for t in tasks:
|
|
172
|
+
t.cancel()
|
|
173
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
asyncio.run(runner())
|
|
177
|
+
except KeyboardInterrupt:
|
|
178
|
+
console.print("[red]Shutting down...[/red]")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@cli.command()
|
|
182
|
+
@click.option("--server", "server", "-S", type=str, required=False, default=None, hidden=True, help="Server URL override (advanced)")
|
|
183
|
+
def list(server: Optional[str]):
|
|
184
|
+
"""List registered domains via server API."""
|
|
185
|
+
import httpx
|
|
186
|
+
base_url = "https://tunnel.tunneloon.online"
|
|
187
|
+
if server:
|
|
188
|
+
base = server.replace("ws://", "http://").replace("wss://", "https://")
|
|
189
|
+
base = base.replace("http://", "https://")
|
|
190
|
+
else:
|
|
191
|
+
base = base_url
|
|
192
|
+
try:
|
|
193
|
+
resp = httpx.get(f"{base}/api/domains", timeout=10)
|
|
194
|
+
resp.raise_for_status()
|
|
195
|
+
data = resp.json()
|
|
196
|
+
domains = data.get("domains", [])
|
|
197
|
+
if not domains:
|
|
198
|
+
console.print("[yellow]No domains found[/yellow]")
|
|
199
|
+
else:
|
|
200
|
+
console.print("[green]Domains:[/green] " + ", ".join(domains))
|
|
201
|
+
except Exception as e:
|
|
202
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
cli()
|
|
207
|
+
|
|
208
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
|
|
6
|
+
_queue: asyncio.Queue[Dict[str, Any]] | None = None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_queue() -> asyncio.Queue[Dict[str, Any]]:
|
|
10
|
+
global _queue
|
|
11
|
+
if _queue is None:
|
|
12
|
+
_queue = asyncio.Queue()
|
|
13
|
+
return _queue
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def enqueue(cmd: Dict[str, Any]) -> None:
|
|
17
|
+
await get_queue().put(cmd)
|
|
18
|
+
|
|
19
|
+
|