unitlab 2.3.33__py3-none-any.whl → 2.3.35__py3-none-any.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.
- unitlab/client.py +78 -64
- unitlab/main.py +17 -32
- unitlab/persistent_tunnel.py +196 -51
- unitlab/utils.py +2 -0
- {unitlab-2.3.33.dist-info → unitlab-2.3.35.dist-info}/METADATA +12 -3
- unitlab-2.3.35.dist-info/RECORD +13 -0
- {unitlab-2.3.33.dist-info → unitlab-2.3.35.dist-info}/WHEEL +1 -1
- unitlab/api_tunnel.py +0 -238
- unitlab/auto_tunnel.py +0 -174
- unitlab/binary_manager.py +0 -154
- unitlab/cloudflare_api_tunnel.py +0 -379
- unitlab/cloudflare_api_tunnel_backup.py +0 -653
- unitlab/dynamic_tunnel.py +0 -272
- unitlab/easy_tunnel.py +0 -210
- unitlab/simple_tunnel.py +0 -205
- unitlab/tunnel_config.py +0 -204
- unitlab/tunnel_service_token.py +0 -104
- unitlab-2.3.33.dist-info/RECORD +0 -23
- {unitlab-2.3.33.dist-info → unitlab-2.3.35.dist-info}/entry_points.txt +0 -0
- {unitlab-2.3.33.dist-info → unitlab-2.3.35.dist-info/licenses}/LICENSE.md +0 -0
- {unitlab-2.3.33.dist-info → unitlab-2.3.35.dist-info}/top_level.txt +0 -0
unitlab/cloudflare_api_tunnel.py
DELETED
@@ -1,379 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Cloudflare API-based Tunnel Configuration
|
3
|
-
Uses API to dynamically manage DNS and routes
|
4
|
-
SIMPLIFIED VERSION with unique tunnel names
|
5
|
-
"""
|
6
|
-
|
7
|
-
import os
|
8
|
-
import requests
|
9
|
-
import subprocess
|
10
|
-
import time
|
11
|
-
import logging
|
12
|
-
import uuid
|
13
|
-
from pathlib import Path
|
14
|
-
from .binary_manager import CloudflaredBinaryManager
|
15
|
-
|
16
|
-
# Try to load .env file if it exists
|
17
|
-
try:
|
18
|
-
from dotenv import load_dotenv
|
19
|
-
env_path = Path(__file__).parent.parent.parent / '.env'
|
20
|
-
if env_path.exists():
|
21
|
-
load_dotenv(env_path)
|
22
|
-
except ImportError:
|
23
|
-
pass
|
24
|
-
|
25
|
-
class CloudflareAPITunnel:
|
26
|
-
def __init__(self, device_id, base_domain="1scan.uz"):
|
27
|
-
"""
|
28
|
-
Initialize Cloudflare tunnel with API configuration
|
29
|
-
Each device gets a unique tunnel with UUID
|
30
|
-
"""
|
31
|
-
self.device_id = device_id
|
32
|
-
self.base_domain = base_domain
|
33
|
-
|
34
|
-
# Clean device ID for use in hostnames (remove special chars)
|
35
|
-
self.clean_device_id = device_id.replace(' ', '').replace('-', '').replace('.', '').replace('_', '')[:24]
|
36
|
-
|
37
|
-
# Subdomains for this device (j for jupyter, s for ssh)
|
38
|
-
self.jupyter_subdomain = f"j{self.clean_device_id}"
|
39
|
-
self.ssh_subdomain = f"s{self.clean_device_id}"
|
40
|
-
|
41
|
-
# URLs for access
|
42
|
-
self.jupyter_url = f"https://{self.jupyter_subdomain}.{base_domain}"
|
43
|
-
self.ssh_hostname = f"{self.ssh_subdomain}.{base_domain}"
|
44
|
-
self.ssh_url = self.ssh_hostname # Backward compatibility
|
45
|
-
|
46
|
-
# Hardcoded Cloudflare credentials
|
47
|
-
self.account_id = "c91192ae20a5d43f65e087550d8dc89b"
|
48
|
-
self.api_token = "LJLe6QMOtpN0MeuLQ05_zUKKxVm4vEibkC8lxSJd"
|
49
|
-
self.zone_id = "f17ca0e9cf056e87afb019c88f936ac9"
|
50
|
-
|
51
|
-
# API configuration
|
52
|
-
self.api_base = "https://api.cloudflare.com/client/v4"
|
53
|
-
self.headers = {
|
54
|
-
"Authorization": f"Bearer {self.api_token}",
|
55
|
-
"Content-Type": "application/json"
|
56
|
-
}
|
57
|
-
|
58
|
-
# Track created resources for cleanup
|
59
|
-
self.tunnel_id = None
|
60
|
-
self.tunnel_process = None
|
61
|
-
self.created_dns_records = []
|
62
|
-
self.tunnel_config_file = None
|
63
|
-
|
64
|
-
# Binary manager for cloudflared
|
65
|
-
self.binary_manager = CloudflaredBinaryManager()
|
66
|
-
|
67
|
-
def create_dns_records(self):
|
68
|
-
"""
|
69
|
-
Create DNS records pointing to the tunnel
|
70
|
-
"""
|
71
|
-
if not self.api_token or not self.tunnel_id:
|
72
|
-
print("⚠️ Cannot create DNS records without API token and tunnel ID")
|
73
|
-
return False
|
74
|
-
|
75
|
-
print(f"📡 Creating DNS records for device {self.device_id}...")
|
76
|
-
|
77
|
-
dns_records = [
|
78
|
-
{"name": self.jupyter_subdomain, "content": f"{self.tunnel_id}.cfargotunnel.com"},
|
79
|
-
{"name": self.ssh_subdomain, "content": f"{self.tunnel_id}.cfargotunnel.com"}
|
80
|
-
]
|
81
|
-
|
82
|
-
for record in dns_records:
|
83
|
-
try:
|
84
|
-
# Check if record exists
|
85
|
-
check_url = f"{self.api_base}/zones/{self.zone_id}/dns_records"
|
86
|
-
check_params = {"name": f"{record['name']}.{self.base_domain}", "type": "CNAME"}
|
87
|
-
check_response = requests.get(check_url, headers=self.headers, params=check_params)
|
88
|
-
|
89
|
-
if check_response.status_code == 200:
|
90
|
-
existing = check_response.json().get('result', [])
|
91
|
-
if existing:
|
92
|
-
print(f" ✓ DNS record {record['name']}.{self.base_domain} already exists")
|
93
|
-
continue
|
94
|
-
|
95
|
-
# Create new record
|
96
|
-
data = {
|
97
|
-
"type": "CNAME",
|
98
|
-
"name": record['name'],
|
99
|
-
"content": record['content'],
|
100
|
-
"ttl": 1,
|
101
|
-
"proxied": True
|
102
|
-
}
|
103
|
-
|
104
|
-
response = requests.post(check_url, headers=self.headers, json=data)
|
105
|
-
|
106
|
-
if response.status_code == 200:
|
107
|
-
result = response.json()
|
108
|
-
if result.get("success"):
|
109
|
-
print(f" ✅ Created DNS: {record['name']}.{self.base_domain}")
|
110
|
-
self.created_dns_records.append(result["result"]["id"])
|
111
|
-
else:
|
112
|
-
print(f" ⚠️ Failed to create {record['name']}: {result.get('errors')}")
|
113
|
-
else:
|
114
|
-
print(f" ❌ HTTP error {response.status_code} for {record['name']}")
|
115
|
-
|
116
|
-
except Exception as e:
|
117
|
-
print(f" ❌ Error creating DNS record: {e}")
|
118
|
-
continue
|
119
|
-
|
120
|
-
return True
|
121
|
-
|
122
|
-
def create_device_tunnel(self):
|
123
|
-
"""
|
124
|
-
Create a unique tunnel for this device
|
125
|
-
Each tunnel gets a unique UUID to avoid conflicts
|
126
|
-
"""
|
127
|
-
# Always use a unique name with UUID
|
128
|
-
unique_id = str(uuid.uuid4())[:8]
|
129
|
-
tunnel_name = f"device-{self.clean_device_id}-{unique_id}"
|
130
|
-
|
131
|
-
# Update clean_device_id to include UUID for DNS records
|
132
|
-
self.clean_device_id_with_uuid = f"{self.clean_device_id}{unique_id}"
|
133
|
-
|
134
|
-
# Update subdomains with UUID
|
135
|
-
self.jupyter_subdomain = f"j{self.clean_device_id_with_uuid}"
|
136
|
-
self.ssh_subdomain = f"s{self.clean_device_id_with_uuid}"
|
137
|
-
|
138
|
-
# Update URLs with new subdomains
|
139
|
-
self.jupyter_url = f"https://{self.jupyter_subdomain}.{self.base_domain}"
|
140
|
-
self.ssh_hostname = f"{self.ssh_subdomain}.{self.base_domain}"
|
141
|
-
self.ssh_url = self.ssh_hostname # Backward compatibility
|
142
|
-
|
143
|
-
print(f"📦 Creating new tunnel: {tunnel_name}")
|
144
|
-
|
145
|
-
create_url = f"{self.api_base}/accounts/{self.account_id}/tunnels"
|
146
|
-
|
147
|
-
# Generate secret
|
148
|
-
tunnel_secret = os.urandom(32).hex()
|
149
|
-
|
150
|
-
create_data = {
|
151
|
-
"name": tunnel_name,
|
152
|
-
"tunnel_secret": tunnel_secret
|
153
|
-
}
|
154
|
-
|
155
|
-
create_response = requests.post(create_url, headers=self.headers, json=create_data)
|
156
|
-
|
157
|
-
if create_response.status_code in [200, 201]:
|
158
|
-
tunnel = create_response.json()['result']
|
159
|
-
print(f"✅ Created tunnel: {tunnel_name}")
|
160
|
-
|
161
|
-
# Add the secret to the tunnel info (API doesn't return it)
|
162
|
-
tunnel['tunnel_secret'] = tunnel_secret
|
163
|
-
|
164
|
-
# Save credentials for this tunnel
|
165
|
-
self._save_tunnel_credentials(tunnel)
|
166
|
-
|
167
|
-
# Configure tunnel routes
|
168
|
-
self._configure_tunnel_routes(tunnel['id'])
|
169
|
-
|
170
|
-
# Store tunnel ID for DNS creation
|
171
|
-
self.tunnel_id = tunnel['id']
|
172
|
-
|
173
|
-
# Create DNS records for this device
|
174
|
-
self.create_dns_records()
|
175
|
-
|
176
|
-
return tunnel
|
177
|
-
else:
|
178
|
-
print(f"❌ Failed to create tunnel: {create_response.text}")
|
179
|
-
return None
|
180
|
-
|
181
|
-
def _configure_tunnel_routes(self, tunnel_id):
|
182
|
-
"""
|
183
|
-
Configure ingress routes for the device tunnel
|
184
|
-
Creates a config file for cloudflared
|
185
|
-
"""
|
186
|
-
import yaml
|
187
|
-
|
188
|
-
# Create config file for this tunnel
|
189
|
-
config_dir = Path.home() / '.cloudflared'
|
190
|
-
config_dir.mkdir(exist_ok=True)
|
191
|
-
config_file = config_dir / f'config-{tunnel_id}.yml'
|
192
|
-
|
193
|
-
config = {
|
194
|
-
"tunnel": tunnel_id,
|
195
|
-
"credentials-file": str(config_dir / f"{tunnel_id}.json"),
|
196
|
-
"ingress": [
|
197
|
-
{
|
198
|
-
"hostname": f"{self.jupyter_subdomain}.{self.base_domain}",
|
199
|
-
"service": "http://localhost:8888",
|
200
|
-
"originRequest": {
|
201
|
-
"noTLSVerify": True
|
202
|
-
}
|
203
|
-
},
|
204
|
-
{
|
205
|
-
"hostname": f"{self.ssh_subdomain}.{self.base_domain}",
|
206
|
-
"service": "ssh://localhost:22"
|
207
|
-
},
|
208
|
-
{
|
209
|
-
"service": "http_status:404"
|
210
|
-
}
|
211
|
-
]
|
212
|
-
}
|
213
|
-
|
214
|
-
with open(config_file, 'w') as f:
|
215
|
-
yaml.dump(config, f)
|
216
|
-
|
217
|
-
print(f"✅ Created tunnel config: {config_file}")
|
218
|
-
self.tunnel_config_file = config_file
|
219
|
-
|
220
|
-
def _save_tunnel_credentials(self, tunnel_info):
|
221
|
-
"""
|
222
|
-
Save tunnel credentials locally for this device
|
223
|
-
Credentials must be base64 encoded for cloudflared
|
224
|
-
"""
|
225
|
-
import base64
|
226
|
-
import json
|
227
|
-
|
228
|
-
creds_dir = Path.home() / '.cloudflared'
|
229
|
-
creds_dir.mkdir(exist_ok=True)
|
230
|
-
|
231
|
-
creds_file = creds_dir / f"{tunnel_info['id']}.json"
|
232
|
-
|
233
|
-
# Get the secret - it should be hex string
|
234
|
-
secret_hex = tunnel_info.get('tunnel_secret') or tunnel_info.get('secret')
|
235
|
-
if secret_hex:
|
236
|
-
# Convert hex to bytes then to base64
|
237
|
-
secret_bytes = bytes.fromhex(secret_hex)
|
238
|
-
secret_b64 = base64.b64encode(secret_bytes).decode('ascii')
|
239
|
-
else:
|
240
|
-
print("⚠️ No tunnel secret found")
|
241
|
-
return None
|
242
|
-
|
243
|
-
credentials = {
|
244
|
-
"AccountTag": self.account_id,
|
245
|
-
"TunnelSecret": secret_b64, # Must be base64!
|
246
|
-
"TunnelID": tunnel_info['id']
|
247
|
-
}
|
248
|
-
|
249
|
-
with open(creds_file, 'w') as f:
|
250
|
-
json.dump(credentials, f, indent=2)
|
251
|
-
|
252
|
-
print(f"✅ Saved tunnel credentials: {creds_file}")
|
253
|
-
return creds_file
|
254
|
-
|
255
|
-
def start_tunnel_with_token(self):
|
256
|
-
"""
|
257
|
-
Start tunnel using API-created tunnel with UUID
|
258
|
-
"""
|
259
|
-
try:
|
260
|
-
print("🚀 Starting Cloudflare tunnel...")
|
261
|
-
|
262
|
-
# Ensure cloudflared is available
|
263
|
-
cloudflared_path = self._ensure_cloudflared()
|
264
|
-
if not cloudflared_path:
|
265
|
-
raise RuntimeError("Failed to obtain cloudflared binary")
|
266
|
-
|
267
|
-
# Create a new unique tunnel
|
268
|
-
device_tunnel = self.create_device_tunnel()
|
269
|
-
|
270
|
-
if not device_tunnel:
|
271
|
-
print("❌ Could not create device tunnel")
|
272
|
-
return None
|
273
|
-
|
274
|
-
tunnel_id = device_tunnel['id']
|
275
|
-
tunnel_name = device_tunnel['name']
|
276
|
-
|
277
|
-
print(f"🚇 Starting tunnel: {tunnel_name} ({tunnel_id})")
|
278
|
-
|
279
|
-
# Check credentials file exists
|
280
|
-
creds_file = Path.home() / '.cloudflared' / f"{tunnel_id}.json"
|
281
|
-
config_file = Path.home() / '.cloudflared' / f'config-{tunnel_id}.yml'
|
282
|
-
|
283
|
-
if not creds_file.exists():
|
284
|
-
print("❌ Error: No credentials file found for newly created tunnel")
|
285
|
-
return None
|
286
|
-
|
287
|
-
# Use config file if it exists, otherwise use credentials file
|
288
|
-
if config_file.exists():
|
289
|
-
cmd = [
|
290
|
-
cloudflared_path,
|
291
|
-
"tunnel",
|
292
|
-
"--no-autoupdate",
|
293
|
-
"--config", str(config_file),
|
294
|
-
"run"
|
295
|
-
]
|
296
|
-
else:
|
297
|
-
cmd = [
|
298
|
-
cloudflared_path,
|
299
|
-
"tunnel",
|
300
|
-
"--no-autoupdate",
|
301
|
-
"--credentials-file", str(creds_file),
|
302
|
-
"run",
|
303
|
-
tunnel_id
|
304
|
-
]
|
305
|
-
|
306
|
-
# Start tunnel process
|
307
|
-
self.tunnel_process = subprocess.Popen(
|
308
|
-
cmd,
|
309
|
-
stdout=subprocess.PIPE,
|
310
|
-
stderr=subprocess.STDOUT,
|
311
|
-
text=True,
|
312
|
-
bufsize=1
|
313
|
-
)
|
314
|
-
|
315
|
-
print("⏳ Waiting for tunnel to connect...")
|
316
|
-
time.sleep(5)
|
317
|
-
|
318
|
-
if self.tunnel_process.poll() is None:
|
319
|
-
print("✅ Tunnel is running!")
|
320
|
-
print(f"📌 Device ID: {self.clean_device_id}")
|
321
|
-
print(f"📌 Jupyter URL: {self.jupyter_url}")
|
322
|
-
print(f"📌 SSH hostname: {self.ssh_hostname}")
|
323
|
-
print(f"📌 SSH command: ssh -o ProxyCommand='cloudflared access ssh --hostname {self.ssh_hostname}' user@localhost")
|
324
|
-
return self.tunnel_process
|
325
|
-
else:
|
326
|
-
output = self.tunnel_process.stdout.read() if self.tunnel_process.stdout else ""
|
327
|
-
print(f"❌ Tunnel failed to start: {output}")
|
328
|
-
return None
|
329
|
-
|
330
|
-
except Exception as e:
|
331
|
-
print(f"❌ Error starting tunnel: {e}")
|
332
|
-
return None
|
333
|
-
|
334
|
-
def setup(self, jupyter_port=8888):
|
335
|
-
"""
|
336
|
-
Setup and start tunnel (maintains compatibility)
|
337
|
-
"""
|
338
|
-
return self.start_tunnel_with_token()
|
339
|
-
|
340
|
-
def stop(self):
|
341
|
-
"""
|
342
|
-
Stop the tunnel if running
|
343
|
-
"""
|
344
|
-
if self.tunnel_process and self.tunnel_process.poll() is None:
|
345
|
-
print("Stopping tunnel...")
|
346
|
-
self.tunnel_process.terminate()
|
347
|
-
try:
|
348
|
-
self.tunnel_process.wait(timeout=5)
|
349
|
-
except subprocess.TimeoutExpired:
|
350
|
-
self.tunnel_process.kill()
|
351
|
-
print("Tunnel stopped")
|
352
|
-
|
353
|
-
def _ensure_cloudflared(self):
|
354
|
-
"""
|
355
|
-
Ensure cloudflared binary is available
|
356
|
-
"""
|
357
|
-
print("🔍 Checking for cloudflared binary...")
|
358
|
-
|
359
|
-
# Try binary manager first
|
360
|
-
if self.binary_manager:
|
361
|
-
try:
|
362
|
-
path = self.binary_manager.get_binary_path()
|
363
|
-
print(f"✅ Using cloudflared from binary manager: {path}")
|
364
|
-
return path
|
365
|
-
except Exception as e:
|
366
|
-
print(f"⚠️ Binary manager failed: {e}")
|
367
|
-
|
368
|
-
# Fallback to system cloudflared
|
369
|
-
try:
|
370
|
-
result = subprocess.run(['which', 'cloudflared'], capture_output=True, text=True)
|
371
|
-
if result.returncode == 0:
|
372
|
-
path = result.stdout.strip()
|
373
|
-
print(f"✅ Found system cloudflared: {path}")
|
374
|
-
return path
|
375
|
-
except:
|
376
|
-
pass
|
377
|
-
|
378
|
-
print("❌ cloudflared not found")
|
379
|
-
return None
|