dreamtunnel 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.
- dreamtunnel-0.1.0/LICENSE +21 -0
- dreamtunnel-0.1.0/PKG-INFO +193 -0
- dreamtunnel-0.1.0/README.md +169 -0
- dreamtunnel-0.1.0/pyproject.toml +39 -0
- dreamtunnel-0.1.0/setup.cfg +4 -0
- dreamtunnel-0.1.0/src/dreamtunnel/__init__.py +40 -0
- dreamtunnel-0.1.0/src/dreamtunnel/__main__.py +6 -0
- dreamtunnel-0.1.0/src/dreamtunnel/binary.py +152 -0
- dreamtunnel-0.1.0/src/dreamtunnel/cli.py +95 -0
- dreamtunnel-0.1.0/src/dreamtunnel/errors.py +32 -0
- dreamtunnel-0.1.0/src/dreamtunnel/tunnel.py +411 -0
- dreamtunnel-0.1.0/src/dreamtunnel.egg-info/PKG-INFO +193 -0
- dreamtunnel-0.1.0/src/dreamtunnel.egg-info/SOURCES.txt +18 -0
- dreamtunnel-0.1.0/src/dreamtunnel.egg-info/dependency_links.txt +1 -0
- dreamtunnel-0.1.0/src/dreamtunnel.egg-info/entry_points.txt +2 -0
- dreamtunnel-0.1.0/src/dreamtunnel.egg-info/requires.txt +3 -0
- dreamtunnel-0.1.0/src/dreamtunnel.egg-info/top_level.txt +1 -0
- dreamtunnel-0.1.0/tests/test_binary.py +52 -0
- dreamtunnel-0.1.0/tests/test_live.py +78 -0
- dreamtunnel-0.1.0/tests/test_tunnel.py +229 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dreamcatchered
|
|
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,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dreamtunnel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for dreamTunnel — instant public HTTPS tunnels
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://tunnel.swag.best
|
|
7
|
+
Project-URL: Repository, https://github.com/dreamcatchered/dreamTunnel
|
|
8
|
+
Keywords: tunnel,ngrok,https,webhook,localhost,self-hosted
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# dreamtunnel (Python SDK)
|
|
26
|
+
|
|
27
|
+
Python-обёртка над CLI `dt`: запускает туннель **из кода**, отдаёт публичный
|
|
28
|
+
URL, присылает события (включая переподключения) и корректно всё останавливает.
|
|
29
|
+
|
|
30
|
+
Токен уже встроен в бинарник — после установки ничего настраивать не нужно.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install "git+https://github.com/dreamcatchered/dreamTunnel.git#subdirectory=sdk/python"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Из локальной копии репозитория — так же просто:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/dreamcatchered/dreamTunnel
|
|
40
|
+
pip install -e dreamTunnel/sdk/python
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Если `dt` ещё нет, SDK скачает его сам при первом запуске.
|
|
44
|
+
|
|
45
|
+
## Быстрый старт
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from dreamtunnel import Tunnel
|
|
49
|
+
|
|
50
|
+
with Tunnel(port=8080) as tun:
|
|
51
|
+
print(tun.url) # https://k7f3m9ab.maiuni.online
|
|
52
|
+
... # туннель живёт, пока мы внутри блока
|
|
53
|
+
# выход из блока — туннель закрыт
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Свой поддомен:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
with Tunnel(port=8080, subdomain="my-api") as tun:
|
|
60
|
+
print(tun.url) # https://my-api.maiuni.online
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Управление вручную
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
tun = Tunnel(port=3000, subdomain="dev")
|
|
67
|
+
tun.start() # ждёт, пока релей подтвердит туннель
|
|
68
|
+
print(tun.url, tun.host, tun.local_url, tun.pid)
|
|
69
|
+
|
|
70
|
+
tun.stop() # SIGTERM, при необходимости SIGKILL
|
|
71
|
+
tun.restart() # перезапуск с тем же поддоменом
|
|
72
|
+
|
|
73
|
+
tun.connect() # синоним start()
|
|
74
|
+
tun.disconnect() # синоним stop()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## События и переподключение
|
|
78
|
+
|
|
79
|
+
`dt` сам держит соединение и переподключается при обрыве. SDK отдаёт это наружу:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
def on_event(event: dict) -> None:
|
|
83
|
+
# event["event"]: ready | reconnected | disconnected | error | request | response
|
|
84
|
+
print(event)
|
|
85
|
+
|
|
86
|
+
tun = Tunnel(port=8080, on_event=on_event)
|
|
87
|
+
tun.start()
|
|
88
|
+
|
|
89
|
+
tun.events # все события, что видели
|
|
90
|
+
tun.connected # True, пока туннель на связи
|
|
91
|
+
tun.stderr # что писал dt в stderr
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Запуск приложения вместе с туннелем
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import threading, time
|
|
98
|
+
from flask import Flask
|
|
99
|
+
from dreamtunnel import Tunnel
|
|
100
|
+
|
|
101
|
+
app = Flask(__name__)
|
|
102
|
+
|
|
103
|
+
@app.route("/")
|
|
104
|
+
def index():
|
|
105
|
+
return "привет"
|
|
106
|
+
|
|
107
|
+
threading.Thread(
|
|
108
|
+
target=lambda: app.run(host="127.0.0.1", port=8080, use_reloader=False),
|
|
109
|
+
daemon=True,
|
|
110
|
+
).start()
|
|
111
|
+
time.sleep(0.5)
|
|
112
|
+
|
|
113
|
+
with Tunnel(port=8080, subdomain="my-api") as tun:
|
|
114
|
+
print(tun.url)
|
|
115
|
+
while True:
|
|
116
|
+
time.sleep(1)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Рабочие примеры — в [`examples/`](examples): `flask_app.py`, `fastapi_app.py`.
|
|
120
|
+
|
|
121
|
+
## CLI
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
dreamtunnel http 8080 # как `dt http`, но через SDK
|
|
125
|
+
dreamtunnel http 8080 my-api --json # машинный вывод (NDJSON)
|
|
126
|
+
dreamtunnel tcp 25565
|
|
127
|
+
python -m dreamtunnel http 8080
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Параметры `Tunnel`
|
|
131
|
+
|
|
132
|
+
| Параметр | По умолчанию | Смысл |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `port` | — | локальный порт |
|
|
135
|
+
| `subdomain` | случайный | желаемое имя: `my-api` → `my-api.maiuni.online` |
|
|
136
|
+
| `mode` | `"http"` | `"http"` или `"tcp"` |
|
|
137
|
+
| `server` | `tunnel.swag.best:444` | адрес control-канала |
|
|
138
|
+
| `token` | встроенный | свой токен, если нужен приватный доступ |
|
|
139
|
+
| `dt_binary` | авто | путь к `dt` |
|
|
140
|
+
| `auto_install` | `True` | скачивать `dt`, если не найден |
|
|
141
|
+
| `ready_timeout` | `30` | сколько ждать подтверждения |
|
|
142
|
+
| `on_event` | `None` | колбэк на каждое событие |
|
|
143
|
+
| `session` | `True` | свой файл настроек и сессия для этого туннеля |
|
|
144
|
+
| `config_path` | — | явный путь к файлу настроек `dt` |
|
|
145
|
+
|
|
146
|
+
## Исключения
|
|
147
|
+
|
|
148
|
+
| Исключение | Когда |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `BinaryNotFound` | `dt` не найден и автоустановка выключена |
|
|
151
|
+
| `TunnelTimeout` | релей не ответил за `ready_timeout` |
|
|
152
|
+
| `TunnelExited` | процесс `dt` умер до подключения (в `.output` — его вывод) |
|
|
153
|
+
| `TunnelError` | релей отказал (плохой токен, занятый поддомен) |
|
|
154
|
+
|
|
155
|
+
## Переменные окружения
|
|
156
|
+
|
|
157
|
+
| Переменная | Смысл |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `DT_BINARY` | путь к `dt` |
|
|
160
|
+
| `DT_TOKEN` | токен доступа |
|
|
161
|
+
| `DT_SERVER` | адрес control-канала |
|
|
162
|
+
| `DT_SUB` | поддомен |
|
|
163
|
+
| `DT_CONFIG` | файл настроек `dt` (SDK задаёт свой на каждый туннель) |
|
|
164
|
+
| `DREAMTUNNEL_BASE_URL` | откуда скачивать бинарник (по умолчанию `https://tunnel.swag.best`) |
|
|
165
|
+
| `DREAMTUNNEL_HOME` | куда класть скачанный бинарник и сессии (по умолчанию `~/.dreamtunnel`) |
|
|
166
|
+
|
|
167
|
+
## Как это устроено
|
|
168
|
+
|
|
169
|
+
SDK не переписывает протокол: он запускает `dt <mode> <port> --json` и
|
|
170
|
+
разбирает поток NDJSON-событий (`ready`, `disconnected`, `error`, …). Если
|
|
171
|
+
попался старый бинарник без `--json`, строки вида
|
|
172
|
+
`Forwarding https://… -> http://…` разбираются регуляркой — поддержка
|
|
173
|
+
обратной совместимости встроена.
|
|
174
|
+
|
|
175
|
+
Каждому туннелю SDK выдаёт **свой** файл настроек `dt` (переменная
|
|
176
|
+
`DT_CONFIG`), поэтому сессии не пересекаются: несколько туннелей с одной
|
|
177
|
+
машины (и даже из одного процесса) работают одновременно и не выбивают друг
|
|
178
|
+
друга. `tun.stop()` сохраняет файл — перезапуск получает тот же адрес,
|
|
179
|
+
`tun.close()` его удаляет.
|
|
180
|
+
|
|
181
|
+
Практический смысл: `dt` хранит session-id в `~/.dreamtunnel/config.env`, и
|
|
182
|
+
если два процесса используют один файл, второй забирает сессию себе, а первый
|
|
183
|
+
теряет туннель.
|
|
184
|
+
|
|
185
|
+
## Тесты
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
pip install -e ".[test]"
|
|
189
|
+
pytest # юнит-тесты на фейковом dt
|
|
190
|
+
DREAMTUNNEL_LIVE=1 pytest tests/test_live.py -v # боевой релей
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Лицензия: MIT.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# dreamtunnel (Python SDK)
|
|
2
|
+
|
|
3
|
+
Python-обёртка над CLI `dt`: запускает туннель **из кода**, отдаёт публичный
|
|
4
|
+
URL, присылает события (включая переподключения) и корректно всё останавливает.
|
|
5
|
+
|
|
6
|
+
Токен уже встроен в бинарник — после установки ничего настраивать не нужно.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install "git+https://github.com/dreamcatchered/dreamTunnel.git#subdirectory=sdk/python"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Из локальной копии репозитория — так же просто:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
git clone https://github.com/dreamcatchered/dreamTunnel
|
|
16
|
+
pip install -e dreamTunnel/sdk/python
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Если `dt` ещё нет, SDK скачает его сам при первом запуске.
|
|
20
|
+
|
|
21
|
+
## Быстрый старт
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from dreamtunnel import Tunnel
|
|
25
|
+
|
|
26
|
+
with Tunnel(port=8080) as tun:
|
|
27
|
+
print(tun.url) # https://k7f3m9ab.maiuni.online
|
|
28
|
+
... # туннель живёт, пока мы внутри блока
|
|
29
|
+
# выход из блока — туннель закрыт
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Свой поддомен:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
with Tunnel(port=8080, subdomain="my-api") as tun:
|
|
36
|
+
print(tun.url) # https://my-api.maiuni.online
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Управление вручную
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
tun = Tunnel(port=3000, subdomain="dev")
|
|
43
|
+
tun.start() # ждёт, пока релей подтвердит туннель
|
|
44
|
+
print(tun.url, tun.host, tun.local_url, tun.pid)
|
|
45
|
+
|
|
46
|
+
tun.stop() # SIGTERM, при необходимости SIGKILL
|
|
47
|
+
tun.restart() # перезапуск с тем же поддоменом
|
|
48
|
+
|
|
49
|
+
tun.connect() # синоним start()
|
|
50
|
+
tun.disconnect() # синоним stop()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## События и переподключение
|
|
54
|
+
|
|
55
|
+
`dt` сам держит соединение и переподключается при обрыве. SDK отдаёт это наружу:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
def on_event(event: dict) -> None:
|
|
59
|
+
# event["event"]: ready | reconnected | disconnected | error | request | response
|
|
60
|
+
print(event)
|
|
61
|
+
|
|
62
|
+
tun = Tunnel(port=8080, on_event=on_event)
|
|
63
|
+
tun.start()
|
|
64
|
+
|
|
65
|
+
tun.events # все события, что видели
|
|
66
|
+
tun.connected # True, пока туннель на связи
|
|
67
|
+
tun.stderr # что писал dt в stderr
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Запуск приложения вместе с туннелем
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import threading, time
|
|
74
|
+
from flask import Flask
|
|
75
|
+
from dreamtunnel import Tunnel
|
|
76
|
+
|
|
77
|
+
app = Flask(__name__)
|
|
78
|
+
|
|
79
|
+
@app.route("/")
|
|
80
|
+
def index():
|
|
81
|
+
return "привет"
|
|
82
|
+
|
|
83
|
+
threading.Thread(
|
|
84
|
+
target=lambda: app.run(host="127.0.0.1", port=8080, use_reloader=False),
|
|
85
|
+
daemon=True,
|
|
86
|
+
).start()
|
|
87
|
+
time.sleep(0.5)
|
|
88
|
+
|
|
89
|
+
with Tunnel(port=8080, subdomain="my-api") as tun:
|
|
90
|
+
print(tun.url)
|
|
91
|
+
while True:
|
|
92
|
+
time.sleep(1)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Рабочие примеры — в [`examples/`](examples): `flask_app.py`, `fastapi_app.py`.
|
|
96
|
+
|
|
97
|
+
## CLI
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
dreamtunnel http 8080 # как `dt http`, но через SDK
|
|
101
|
+
dreamtunnel http 8080 my-api --json # машинный вывод (NDJSON)
|
|
102
|
+
dreamtunnel tcp 25565
|
|
103
|
+
python -m dreamtunnel http 8080
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Параметры `Tunnel`
|
|
107
|
+
|
|
108
|
+
| Параметр | По умолчанию | Смысл |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `port` | — | локальный порт |
|
|
111
|
+
| `subdomain` | случайный | желаемое имя: `my-api` → `my-api.maiuni.online` |
|
|
112
|
+
| `mode` | `"http"` | `"http"` или `"tcp"` |
|
|
113
|
+
| `server` | `tunnel.swag.best:444` | адрес control-канала |
|
|
114
|
+
| `token` | встроенный | свой токен, если нужен приватный доступ |
|
|
115
|
+
| `dt_binary` | авто | путь к `dt` |
|
|
116
|
+
| `auto_install` | `True` | скачивать `dt`, если не найден |
|
|
117
|
+
| `ready_timeout` | `30` | сколько ждать подтверждения |
|
|
118
|
+
| `on_event` | `None` | колбэк на каждое событие |
|
|
119
|
+
| `session` | `True` | свой файл настроек и сессия для этого туннеля |
|
|
120
|
+
| `config_path` | — | явный путь к файлу настроек `dt` |
|
|
121
|
+
|
|
122
|
+
## Исключения
|
|
123
|
+
|
|
124
|
+
| Исключение | Когда |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `BinaryNotFound` | `dt` не найден и автоустановка выключена |
|
|
127
|
+
| `TunnelTimeout` | релей не ответил за `ready_timeout` |
|
|
128
|
+
| `TunnelExited` | процесс `dt` умер до подключения (в `.output` — его вывод) |
|
|
129
|
+
| `TunnelError` | релей отказал (плохой токен, занятый поддомен) |
|
|
130
|
+
|
|
131
|
+
## Переменные окружения
|
|
132
|
+
|
|
133
|
+
| Переменная | Смысл |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `DT_BINARY` | путь к `dt` |
|
|
136
|
+
| `DT_TOKEN` | токен доступа |
|
|
137
|
+
| `DT_SERVER` | адрес control-канала |
|
|
138
|
+
| `DT_SUB` | поддомен |
|
|
139
|
+
| `DT_CONFIG` | файл настроек `dt` (SDK задаёт свой на каждый туннель) |
|
|
140
|
+
| `DREAMTUNNEL_BASE_URL` | откуда скачивать бинарник (по умолчанию `https://tunnel.swag.best`) |
|
|
141
|
+
| `DREAMTUNNEL_HOME` | куда класть скачанный бинарник и сессии (по умолчанию `~/.dreamtunnel`) |
|
|
142
|
+
|
|
143
|
+
## Как это устроено
|
|
144
|
+
|
|
145
|
+
SDK не переписывает протокол: он запускает `dt <mode> <port> --json` и
|
|
146
|
+
разбирает поток NDJSON-событий (`ready`, `disconnected`, `error`, …). Если
|
|
147
|
+
попался старый бинарник без `--json`, строки вида
|
|
148
|
+
`Forwarding https://… -> http://…` разбираются регуляркой — поддержка
|
|
149
|
+
обратной совместимости встроена.
|
|
150
|
+
|
|
151
|
+
Каждому туннелю SDK выдаёт **свой** файл настроек `dt` (переменная
|
|
152
|
+
`DT_CONFIG`), поэтому сессии не пересекаются: несколько туннелей с одной
|
|
153
|
+
машины (и даже из одного процесса) работают одновременно и не выбивают друг
|
|
154
|
+
друга. `tun.stop()` сохраняет файл — перезапуск получает тот же адрес,
|
|
155
|
+
`tun.close()` его удаляет.
|
|
156
|
+
|
|
157
|
+
Практический смысл: `dt` хранит session-id в `~/.dreamtunnel/config.env`, и
|
|
158
|
+
если два процесса используют один файл, второй забирает сессию себе, а первый
|
|
159
|
+
теряет туннель.
|
|
160
|
+
|
|
161
|
+
## Тесты
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pip install -e ".[test]"
|
|
165
|
+
pytest # юнит-тесты на фейковом dt
|
|
166
|
+
DREAMTUNNEL_LIVE=1 pytest tests/test_live.py -v # боевой релей
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Лицензия: MIT.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dreamtunnel"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for dreamTunnel — instant public HTTPS tunnels"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["tunnel", "ngrok", "https", "webhook", "localhost", "self-hosted"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
22
|
+
"Topic :: Software Development :: Libraries",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://tunnel.swag.best"
|
|
27
|
+
Repository = "https://github.com/dreamcatchered/dreamTunnel"
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = ["pytest>=7"]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
dreamtunnel = "dreamtunnel.cli:main"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["src"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Python SDK for dreamTunnel — instant public HTTPS tunnels.
|
|
2
|
+
|
|
3
|
+
Typical use:
|
|
4
|
+
|
|
5
|
+
from dreamtunnel import Tunnel
|
|
6
|
+
|
|
7
|
+
with Tunnel(port=8080) as tun:
|
|
8
|
+
print(tun.url) # https://k7f3m9ab.maiuni.online
|
|
9
|
+
...
|
|
10
|
+
|
|
11
|
+
The SDK supervises the existing ``dt`` CLI process: it starts the tunnel,
|
|
12
|
+
waits until the relay reports it online, exposes the public URL, forwards the
|
|
13
|
+
relay's events (including reconnects) to your code, and shuts the process down
|
|
14
|
+
cleanly on exit.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .binary import ensure_dt, install_dt
|
|
18
|
+
from .errors import (
|
|
19
|
+
BinaryNotFound,
|
|
20
|
+
DreamTunnelError,
|
|
21
|
+
TunnelError,
|
|
22
|
+
TunnelExited,
|
|
23
|
+
TunnelTimeout,
|
|
24
|
+
)
|
|
25
|
+
from .tunnel import DEFAULT_SERVER, Tunnel
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Tunnel",
|
|
31
|
+
"DEFAULT_SERVER",
|
|
32
|
+
"ensure_dt",
|
|
33
|
+
"install_dt",
|
|
34
|
+
"DreamTunnelError",
|
|
35
|
+
"BinaryNotFound",
|
|
36
|
+
"TunnelTimeout",
|
|
37
|
+
"TunnelExited",
|
|
38
|
+
"TunnelError",
|
|
39
|
+
"__version__",
|
|
40
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Locate or install the ``dt`` binary.
|
|
2
|
+
|
|
3
|
+
Lookup order:
|
|
4
|
+
|
|
5
|
+
1. the ``dt_binary`` argument passed to :class:`~dreamtunnel.Tunnel`
|
|
6
|
+
2. the ``DT_BINARY`` environment variable
|
|
7
|
+
3. ``dt`` on ``PATH``
|
|
8
|
+
4. the SDK cache (``~/.dreamtunnel/bin``)
|
|
9
|
+
5. an automatic download from the public release endpoint, unless disabled
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import platform
|
|
16
|
+
import shutil
|
|
17
|
+
import stat
|
|
18
|
+
import sys
|
|
19
|
+
import urllib.error
|
|
20
|
+
import urllib.request
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from .errors import BinaryNotFound
|
|
24
|
+
|
|
25
|
+
DEFAULT_BASE_URL = os.environ.get("DREAMTUNNEL_BASE_URL", "https://tunnel.swag.best")
|
|
26
|
+
|
|
27
|
+
# machine() spellings we accept
|
|
28
|
+
_AMD64 = {"x86_64", "amd64", "x64", "i686", "i386", "x86"}
|
|
29
|
+
_ARM64 = {"aarch64", "arm64"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cache_root() -> Path:
|
|
33
|
+
"""Root directory used by the SDK (``~/.dreamtunnel`` by default)."""
|
|
34
|
+
override = os.environ.get("DREAMTUNNEL_HOME")
|
|
35
|
+
return Path(override).expanduser() if override else Path.home() / ".dreamtunnel"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cache_dir() -> Path:
|
|
39
|
+
"""Directory where downloaded binaries are kept."""
|
|
40
|
+
return cache_root() / "bin"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def platform_binary_name(
|
|
44
|
+
system: str | None = None, machine: str | None = None
|
|
45
|
+
) -> str:
|
|
46
|
+
"""Return the release file name for the current (or given) platform.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
BinaryNotFound: for platforms dreamTunnel does not build for.
|
|
50
|
+
"""
|
|
51
|
+
system = (system or platform.system()).lower()
|
|
52
|
+
machine = (machine or platform.machine()).lower()
|
|
53
|
+
|
|
54
|
+
if machine in _AMD64:
|
|
55
|
+
arch = "386" if machine in {"i686", "i386", "x86"} else "amd64"
|
|
56
|
+
elif machine in _ARM64:
|
|
57
|
+
arch = "arm64"
|
|
58
|
+
else:
|
|
59
|
+
raise BinaryNotFound(f"unsupported CPU architecture: {machine!r}")
|
|
60
|
+
|
|
61
|
+
if system.startswith("linux"):
|
|
62
|
+
return f"dt-linux-{arch}"
|
|
63
|
+
if system.startswith(("darwin", "mac")):
|
|
64
|
+
return f"dt-darwin-{arch}"
|
|
65
|
+
if system.startswith("win"):
|
|
66
|
+
return f"dt-windows-{arch}.exe"
|
|
67
|
+
raise BinaryNotFound(f"unsupported platform: {system!r}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _executable_name() -> str:
|
|
71
|
+
return "dt.exe" if os.name == "nt" else "dt"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _make_executable(path: Path) -> None:
|
|
75
|
+
if os.name == "nt":
|
|
76
|
+
return
|
|
77
|
+
mode = path.stat().st_mode
|
|
78
|
+
path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def download_dt(dest: Path | None = None, *, base_url: str = DEFAULT_BASE_URL) -> Path:
|
|
82
|
+
"""Download the ``dt`` binary for this platform into the SDK cache."""
|
|
83
|
+
name = platform_binary_name()
|
|
84
|
+
dest = dest or (cache_dir() / _executable_name())
|
|
85
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
|
|
87
|
+
url = f"{base_url.rstrip('/')}/dl/{name}"
|
|
88
|
+
tmp = dest.with_suffix(dest.suffix + ".part")
|
|
89
|
+
try:
|
|
90
|
+
with urllib.request.urlopen(url, timeout=60) as resp: # noqa: S310
|
|
91
|
+
data = resp.read()
|
|
92
|
+
except urllib.error.URLError as exc: # pragma: no cover - network path
|
|
93
|
+
raise BinaryNotFound(f"could not download {url}: {exc}") from exc
|
|
94
|
+
|
|
95
|
+
if len(data) < 1024:
|
|
96
|
+
raise BinaryNotFound(f"download from {url} looks truncated ({len(data)} bytes)")
|
|
97
|
+
|
|
98
|
+
tmp.write_bytes(data)
|
|
99
|
+
tmp.replace(dest)
|
|
100
|
+
_make_executable(dest)
|
|
101
|
+
return dest
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def install_dt(
|
|
105
|
+
dest: Path | None = None, *, base_url: str = DEFAULT_BASE_URL, force: bool = False
|
|
106
|
+
) -> Path:
|
|
107
|
+
"""Install ``dt`` into the cache directory and return its path."""
|
|
108
|
+
dest = dest or (cache_dir() / _executable_name())
|
|
109
|
+
if dest.exists() and not force:
|
|
110
|
+
return dest
|
|
111
|
+
return download_dt(dest, base_url=base_url)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def ensure_dt(
|
|
115
|
+
explicit: str | os.PathLike[str] | None = None,
|
|
116
|
+
*,
|
|
117
|
+
auto_install: bool = True,
|
|
118
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
119
|
+
) -> str:
|
|
120
|
+
"""Return a usable path to ``dt``, installing it if necessary."""
|
|
121
|
+
if explicit:
|
|
122
|
+
path = Path(explicit).expanduser()
|
|
123
|
+
if path.exists():
|
|
124
|
+
return str(path)
|
|
125
|
+
raise BinaryNotFound(f"dt binary not found at {path}")
|
|
126
|
+
|
|
127
|
+
env = os.environ.get("DT_BINARY")
|
|
128
|
+
if env:
|
|
129
|
+
path = Path(env).expanduser()
|
|
130
|
+
if path.exists():
|
|
131
|
+
return str(path)
|
|
132
|
+
|
|
133
|
+
found = shutil.which("dt")
|
|
134
|
+
if found:
|
|
135
|
+
return found
|
|
136
|
+
|
|
137
|
+
cached = cache_dir() / _executable_name()
|
|
138
|
+
if cached.exists():
|
|
139
|
+
return str(cached)
|
|
140
|
+
|
|
141
|
+
if not auto_install:
|
|
142
|
+
raise BinaryNotFound(
|
|
143
|
+
"dt not found on PATH; install it with "
|
|
144
|
+
"`curl -fsSL https://tunnel.swag.best/install.sh | bash` "
|
|
145
|
+
"or pass dt_binary=..."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return str(download_dt(base_url=base_url))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def python_supported() -> bool:
|
|
152
|
+
return sys.version_info >= (3, 10)
|