unitlab 2.3.28__py3-none-any.whl → 2.3.32__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/api_tunnel.py +238 -0
- unitlab/auto_tunnel.py +174 -0
- unitlab/client.py +28 -9
- unitlab/dynamic_tunnel.py +272 -0
- unitlab/easy_tunnel.py +210 -0
- unitlab/persistent_tunnel.py +422 -0
- {unitlab-2.3.28.dist-info → unitlab-2.3.32.dist-info}/METADATA +1 -1
- {unitlab-2.3.28.dist-info → unitlab-2.3.32.dist-info}/RECORD +12 -7
- {unitlab-2.3.28.dist-info → unitlab-2.3.32.dist-info}/LICENSE.md +0 -0
- {unitlab-2.3.28.dist-info → unitlab-2.3.32.dist-info}/WHEEL +0 -0
- {unitlab-2.3.28.dist-info → unitlab-2.3.32.dist-info}/entry_points.txt +0 -0
- {unitlab-2.3.28.dist-info → unitlab-2.3.32.dist-info}/top_level.txt +0 -0
unitlab/api_tunnel.py
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Simple API-based Dynamic Tunnel - Each device gets deviceid.1scan.uz
|
4
|
+
"""
|
5
|
+
|
6
|
+
import subprocess
|
7
|
+
import requests
|
8
|
+
import json
|
9
|
+
import time
|
10
|
+
import os
|
11
|
+
|
12
|
+
class APITunnel:
|
13
|
+
def __init__(self, device_id=None):
|
14
|
+
"""Initialize with device ID"""
|
15
|
+
# Hardcoded Cloudflare credentials for simplicity
|
16
|
+
self.cf_email = "muminovbobur93@gmail.com"
|
17
|
+
self.cf_api_key = "1ae47782b5e2e639fb088ee73e17b74db4b4e" # Global API Key
|
18
|
+
self.cf_account_id = "c91192ae20a5d43f65e087550d8dc89b"
|
19
|
+
self.cf_zone_id = "06ebea0ee0b228c186f97fe9a0a7c83e" # for 1scan.uz
|
20
|
+
|
21
|
+
# Clean device ID for subdomain
|
22
|
+
if device_id:
|
23
|
+
self.device_id = device_id.replace('-', '').replace('_', '').replace('.', '').lower()[:20]
|
24
|
+
else:
|
25
|
+
import uuid
|
26
|
+
self.device_id = str(uuid.uuid4())[:8]
|
27
|
+
|
28
|
+
self.tunnel_name = "agent-{}".format(self.device_id)
|
29
|
+
self.subdomain = self.device_id
|
30
|
+
self.jupyter_url = "https://{}.1scan.uz".format(self.subdomain)
|
31
|
+
|
32
|
+
self.tunnel_id = None
|
33
|
+
self.tunnel_token = None
|
34
|
+
self.jupyter_process = None
|
35
|
+
self.tunnel_process = None
|
36
|
+
|
37
|
+
def create_tunnel_via_cli(self):
|
38
|
+
"""Create tunnel using cloudflared CLI (simpler than API)"""
|
39
|
+
print("🔧 Creating tunnel: {}...".format(self.tunnel_name))
|
40
|
+
|
41
|
+
cloudflared = self.get_cloudflared_path()
|
42
|
+
|
43
|
+
# Login with cert (one-time if not logged in)
|
44
|
+
# This uses the cert.pem file if it exists
|
45
|
+
cert_path = os.path.expanduser("~/.cloudflared/cert.pem")
|
46
|
+
if not os.path.exists(cert_path):
|
47
|
+
print("📝 First time setup - logging in to Cloudflare...")
|
48
|
+
# Use service token instead of interactive login
|
49
|
+
# Or use the API to create tunnel
|
50
|
+
|
51
|
+
# Create tunnel using CLI
|
52
|
+
cmd = [cloudflared, "tunnel", "create", self.tunnel_name]
|
53
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
54
|
+
|
55
|
+
if result.returncode == 0:
|
56
|
+
# Extract tunnel ID from output
|
57
|
+
import re
|
58
|
+
match = re.search(r'Created tunnel .* with id ([a-f0-9-]+)', result.stdout)
|
59
|
+
if match:
|
60
|
+
self.tunnel_id = match.group(1)
|
61
|
+
print("✅ Tunnel created: {}".format(self.tunnel_id))
|
62
|
+
|
63
|
+
# Get the tunnel token
|
64
|
+
token_cmd = [cloudflared, "tunnel", "token", self.tunnel_name]
|
65
|
+
token_result = subprocess.run(token_cmd, capture_output=True, text=True)
|
66
|
+
if token_result.returncode == 0:
|
67
|
+
self.tunnel_token = token_result.stdout.strip()
|
68
|
+
return True
|
69
|
+
|
70
|
+
print("⚠️ Could not create tunnel via CLI, using quick tunnel instead")
|
71
|
+
return False
|
72
|
+
|
73
|
+
def create_dns_record(self):
|
74
|
+
"""Add DNS record for subdomain"""
|
75
|
+
if not self.tunnel_id:
|
76
|
+
return False
|
77
|
+
|
78
|
+
print("🔧 Creating DNS: {}.1scan.uz...".format(self.subdomain))
|
79
|
+
|
80
|
+
url = "https://api.cloudflare.com/client/v4/zones/{}/dns_records".format(self.cf_zone_id)
|
81
|
+
|
82
|
+
headers = {
|
83
|
+
"X-Auth-Email": self.cf_email,
|
84
|
+
"X-Auth-Key": self.cf_api_key,
|
85
|
+
"Content-Type": "application/json"
|
86
|
+
}
|
87
|
+
|
88
|
+
data = {
|
89
|
+
"type": "CNAME",
|
90
|
+
"name": self.subdomain,
|
91
|
+
"content": "{}.cfargotunnel.com".format(self.tunnel_id),
|
92
|
+
"proxied": True
|
93
|
+
}
|
94
|
+
|
95
|
+
response = requests.post(url, headers=headers, json=data)
|
96
|
+
if response.status_code in [200, 409]: # 409 = already exists
|
97
|
+
print("✅ DNS configured")
|
98
|
+
return True
|
99
|
+
|
100
|
+
print("⚠️ DNS setup failed: {}".format(response.text[:100]))
|
101
|
+
return False
|
102
|
+
|
103
|
+
def get_cloudflared_path(self):
|
104
|
+
"""Get or download cloudflared"""
|
105
|
+
import shutil
|
106
|
+
if shutil.which("cloudflared"):
|
107
|
+
return "cloudflared"
|
108
|
+
|
109
|
+
local_bin = os.path.expanduser("~/.local/bin/cloudflared")
|
110
|
+
if os.path.exists(local_bin):
|
111
|
+
return local_bin
|
112
|
+
|
113
|
+
# Download
|
114
|
+
print("📦 Downloading cloudflared...")
|
115
|
+
import platform
|
116
|
+
system = platform.system().lower()
|
117
|
+
arch = "amd64" if "x86" in platform.machine() else "arm64"
|
118
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
|
119
|
+
|
120
|
+
os.makedirs(os.path.dirname(local_bin), exist_ok=True)
|
121
|
+
subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
|
122
|
+
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
123
|
+
return local_bin
|
124
|
+
|
125
|
+
def start_jupyter(self):
|
126
|
+
"""Start Jupyter"""
|
127
|
+
print("🚀 Starting Jupyter...")
|
128
|
+
|
129
|
+
cmd = [
|
130
|
+
"jupyter", "notebook",
|
131
|
+
"--port", "8888",
|
132
|
+
"--no-browser",
|
133
|
+
"--ip", "0.0.0.0",
|
134
|
+
"--NotebookApp.token=''",
|
135
|
+
"--NotebookApp.password=''"
|
136
|
+
]
|
137
|
+
|
138
|
+
self.jupyter_process = subprocess.Popen(
|
139
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
140
|
+
)
|
141
|
+
|
142
|
+
time.sleep(3)
|
143
|
+
print("✅ Jupyter started")
|
144
|
+
return True
|
145
|
+
|
146
|
+
def start_tunnel(self):
|
147
|
+
"""Start tunnel - try with token first, fallback to quick tunnel"""
|
148
|
+
cloudflared = self.get_cloudflared_path()
|
149
|
+
|
150
|
+
if self.tunnel_token:
|
151
|
+
# Use token-based tunnel
|
152
|
+
print("🔧 Starting tunnel with token...")
|
153
|
+
cmd = [cloudflared, "tunnel", "run", "--token", self.tunnel_token]
|
154
|
+
elif self.tunnel_id:
|
155
|
+
# Use tunnel ID
|
156
|
+
print("🔧 Starting tunnel with ID...")
|
157
|
+
cmd = [cloudflared, "tunnel", "run", "--url", "http://localhost:8888", self.tunnel_id]
|
158
|
+
else:
|
159
|
+
# Fallback to quick tunnel
|
160
|
+
print("🔧 Starting quick tunnel (random URL)...")
|
161
|
+
cmd = [cloudflared, "tunnel", "--url", "http://localhost:8888"]
|
162
|
+
self.jupyter_url = "Check terminal output for URL"
|
163
|
+
|
164
|
+
self.tunnel_process = subprocess.Popen(
|
165
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
166
|
+
)
|
167
|
+
|
168
|
+
time.sleep(5)
|
169
|
+
print("✅ Tunnel running")
|
170
|
+
return True
|
171
|
+
|
172
|
+
def start(self):
|
173
|
+
"""Main entry point"""
|
174
|
+
try:
|
175
|
+
print("="*50)
|
176
|
+
print("🌐 API-Based Dynamic Tunnel")
|
177
|
+
print("Device: {}".format(self.device_id))
|
178
|
+
print("="*50)
|
179
|
+
|
180
|
+
# Try to create named tunnel
|
181
|
+
tunnel_created = self.create_tunnel_via_cli()
|
182
|
+
|
183
|
+
if tunnel_created:
|
184
|
+
# Add DNS record
|
185
|
+
self.create_dns_record()
|
186
|
+
|
187
|
+
# Start services
|
188
|
+
self.start_jupyter()
|
189
|
+
self.start_tunnel()
|
190
|
+
|
191
|
+
print("\n" + "="*50)
|
192
|
+
print("🎉 SUCCESS!")
|
193
|
+
if tunnel_created:
|
194
|
+
print("📍 Your permanent URL: {}".format(self.jupyter_url))
|
195
|
+
else:
|
196
|
+
print("📍 Using quick tunnel - check output for URL")
|
197
|
+
print("="*50)
|
198
|
+
|
199
|
+
return True
|
200
|
+
|
201
|
+
except Exception as e:
|
202
|
+
print("❌ Error: {}".format(e))
|
203
|
+
self.stop()
|
204
|
+
return False
|
205
|
+
|
206
|
+
def stop(self):
|
207
|
+
"""Stop everything"""
|
208
|
+
if self.jupyter_process:
|
209
|
+
self.jupyter_process.terminate()
|
210
|
+
if self.tunnel_process:
|
211
|
+
self.tunnel_process.terminate()
|
212
|
+
|
213
|
+
def run(self):
|
214
|
+
"""Run and keep alive"""
|
215
|
+
try:
|
216
|
+
if self.start():
|
217
|
+
print("\nPress Ctrl+C to stop...")
|
218
|
+
while True:
|
219
|
+
time.sleep(1)
|
220
|
+
except KeyboardInterrupt:
|
221
|
+
print("\n⏹️ Shutting down...")
|
222
|
+
self.stop()
|
223
|
+
|
224
|
+
|
225
|
+
def main():
|
226
|
+
"""Test the API tunnel"""
|
227
|
+
import platform
|
228
|
+
import uuid
|
229
|
+
|
230
|
+
hostname = platform.node().replace('.', '-')[:20]
|
231
|
+
device_id = "{}-{}".format(hostname, str(uuid.uuid4())[:8])
|
232
|
+
|
233
|
+
tunnel = APITunnel(device_id=device_id)
|
234
|
+
tunnel.run()
|
235
|
+
|
236
|
+
|
237
|
+
if __name__ == "__main__":
|
238
|
+
main()
|
unitlab/auto_tunnel.py
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Automatic Tunnel Creation - Simplest approach using cloudflared's built-in quick tunnel
|
4
|
+
No API tokens needed!
|
5
|
+
"""
|
6
|
+
|
7
|
+
import subprocess
|
8
|
+
import time
|
9
|
+
import re
|
10
|
+
import os
|
11
|
+
|
12
|
+
class AutoTunnel:
|
13
|
+
def __init__(self, device_id=None):
|
14
|
+
"""
|
15
|
+
Initialize auto tunnel - no credentials needed!
|
16
|
+
"""
|
17
|
+
self.device_id = device_id or "device"
|
18
|
+
self.jupyter_process = None
|
19
|
+
self.tunnel_process = None
|
20
|
+
self.tunnel_url = None
|
21
|
+
|
22
|
+
def get_cloudflared_path(self):
|
23
|
+
"""Get or download cloudflared binary"""
|
24
|
+
import platform
|
25
|
+
|
26
|
+
# Check if exists in system
|
27
|
+
import shutil
|
28
|
+
if shutil.which("cloudflared"):
|
29
|
+
return "cloudflared"
|
30
|
+
|
31
|
+
# Check local
|
32
|
+
local_bin = os.path.expanduser("~/.local/bin/cloudflared")
|
33
|
+
if os.path.exists(local_bin):
|
34
|
+
return local_bin
|
35
|
+
|
36
|
+
# Download it
|
37
|
+
print("📦 Downloading cloudflared...")
|
38
|
+
system = platform.system().lower()
|
39
|
+
if system == "linux":
|
40
|
+
arch = "amd64" if "x86" in platform.machine() else "arm64"
|
41
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
|
42
|
+
|
43
|
+
os.makedirs(os.path.expanduser("~/.local/bin"), exist_ok=True)
|
44
|
+
subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
|
45
|
+
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
46
|
+
print("✅ cloudflared downloaded")
|
47
|
+
return local_bin
|
48
|
+
|
49
|
+
return "cloudflared"
|
50
|
+
|
51
|
+
def start_jupyter(self):
|
52
|
+
"""Start Jupyter notebook"""
|
53
|
+
print("🚀 Starting Jupyter on port 8888...")
|
54
|
+
|
55
|
+
cmd = [
|
56
|
+
"jupyter", "notebook",
|
57
|
+
"--port", "8888",
|
58
|
+
"--no-browser",
|
59
|
+
"--ip", "0.0.0.0",
|
60
|
+
"--NotebookApp.token=''",
|
61
|
+
"--NotebookApp.password=''",
|
62
|
+
"--NotebookApp.allow_origin='*'"
|
63
|
+
]
|
64
|
+
|
65
|
+
self.jupyter_process = subprocess.Popen(
|
66
|
+
cmd,
|
67
|
+
stdout=subprocess.PIPE,
|
68
|
+
stderr=subprocess.PIPE
|
69
|
+
)
|
70
|
+
|
71
|
+
time.sleep(3)
|
72
|
+
print("✅ Jupyter started")
|
73
|
+
return True
|
74
|
+
|
75
|
+
def start_tunnel(self):
|
76
|
+
"""Start tunnel using cloudflared quick tunnel - no auth needed!"""
|
77
|
+
print("🔧 Starting automatic tunnel (no credentials needed)...")
|
78
|
+
|
79
|
+
cloudflared = self.get_cloudflared_path()
|
80
|
+
|
81
|
+
# Use cloudflared's quick tunnel feature - generates random URL
|
82
|
+
cmd = [
|
83
|
+
cloudflared,
|
84
|
+
"tunnel",
|
85
|
+
"--url", "http://localhost:8888"
|
86
|
+
]
|
87
|
+
|
88
|
+
self.tunnel_process = subprocess.Popen(
|
89
|
+
cmd,
|
90
|
+
stdout=subprocess.PIPE,
|
91
|
+
stderr=subprocess.STDOUT,
|
92
|
+
text=True,
|
93
|
+
bufsize=1
|
94
|
+
)
|
95
|
+
|
96
|
+
# Read output to get the tunnel URL
|
97
|
+
print("⏳ Waiting for tunnel URL...")
|
98
|
+
for _ in range(30): # Wait up to 30 seconds
|
99
|
+
line = self.tunnel_process.stdout.readline()
|
100
|
+
if line:
|
101
|
+
# Look for the tunnel URL in output
|
102
|
+
match = re.search(r'https://[a-zA-Z0-9-]+\.trycloudflare\.com', line)
|
103
|
+
if match:
|
104
|
+
self.tunnel_url = match.group(0)
|
105
|
+
print("✅ Tunnel created: {}".format(self.tunnel_url))
|
106
|
+
return True
|
107
|
+
time.sleep(1)
|
108
|
+
|
109
|
+
print("❌ Failed to get tunnel URL")
|
110
|
+
return False
|
111
|
+
|
112
|
+
def start(self):
|
113
|
+
"""Start everything - super simple!"""
|
114
|
+
try:
|
115
|
+
print("="*50)
|
116
|
+
print("🌐 Automatic Cloudflare Tunnel (No Auth Needed!)")
|
117
|
+
print("Device: {}".format(self.device_id))
|
118
|
+
print("="*50)
|
119
|
+
|
120
|
+
# 1. Start Jupyter
|
121
|
+
if not self.start_jupyter():
|
122
|
+
raise Exception("Failed to start Jupyter")
|
123
|
+
|
124
|
+
# 2. Start tunnel (automatic, no credentials)
|
125
|
+
if not self.start_tunnel():
|
126
|
+
raise Exception("Failed to start tunnel")
|
127
|
+
|
128
|
+
print("\n" + "="*50)
|
129
|
+
print("🎉 SUCCESS! Your Jupyter is accessible at:")
|
130
|
+
print(" {}".format(self.tunnel_url))
|
131
|
+
print("="*50)
|
132
|
+
print("\n⚠️ Note: This URL is temporary and random")
|
133
|
+
print("For persistent URLs, use Cloudflare API approach")
|
134
|
+
|
135
|
+
return True
|
136
|
+
|
137
|
+
except Exception as e:
|
138
|
+
print("❌ Error: {}".format(e))
|
139
|
+
self.stop()
|
140
|
+
return False
|
141
|
+
|
142
|
+
def stop(self):
|
143
|
+
"""Stop everything"""
|
144
|
+
if self.jupyter_process:
|
145
|
+
self.jupyter_process.terminate()
|
146
|
+
if self.tunnel_process:
|
147
|
+
self.tunnel_process.terminate()
|
148
|
+
|
149
|
+
def run(self):
|
150
|
+
"""Run and keep alive"""
|
151
|
+
try:
|
152
|
+
if self.start():
|
153
|
+
print("\nPress Ctrl+C to stop...")
|
154
|
+
while True:
|
155
|
+
time.sleep(1)
|
156
|
+
except KeyboardInterrupt:
|
157
|
+
print("\n⏹️ Shutting down...")
|
158
|
+
self.stop()
|
159
|
+
print("👋 Goodbye!")
|
160
|
+
|
161
|
+
|
162
|
+
def main():
|
163
|
+
"""Test automatic tunnel"""
|
164
|
+
import platform
|
165
|
+
device_id = platform.node()
|
166
|
+
|
167
|
+
print("Starting auto tunnel for: {}".format(device_id))
|
168
|
+
|
169
|
+
tunnel = AutoTunnel(device_id=device_id)
|
170
|
+
tunnel.run()
|
171
|
+
|
172
|
+
|
173
|
+
if __name__ == "__main__":
|
174
|
+
main()
|
unitlab/client.py
CHANGED
@@ -281,12 +281,19 @@ class UnitlabClient:
|
|
281
281
|
self.device_id = device_id
|
282
282
|
self.base_domain = base_domain
|
283
283
|
|
284
|
-
# Use
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
284
|
+
# Use persistent tunnel with API (each device gets deviceid.1scan.uz)
|
285
|
+
try:
|
286
|
+
from .persistent_tunnel import PersistentTunnel
|
287
|
+
logger.info("Using Persistent Tunnel with Cloudflare API")
|
288
|
+
self.tunnel_manager = PersistentTunnel(device_id=self.device_id)
|
289
|
+
# Don't call run() here - it has infinite loop. Call start() in setup_tunnels()
|
290
|
+
self.jupyter_url = None
|
291
|
+
self.ssh_url = None
|
292
|
+
|
293
|
+
except ImportError as e:
|
294
|
+
logger.warning(f"Could not import PersistentTunnel: {e}")
|
295
|
+
# Fallback to easy tunnel
|
296
|
+
|
290
297
|
|
291
298
|
# Setup signal handlers
|
292
299
|
signal.signal(signal.SIGINT, self._handle_shutdown)
|
@@ -392,16 +399,28 @@ class UnitlabClient:
|
|
392
399
|
|
393
400
|
logger.info("Setting up Cloudflare tunnel...")
|
394
401
|
|
395
|
-
# SimpleTunnel
|
396
|
-
# It doesn't need the jupyter_port parameter since it starts its own Jupyter
|
402
|
+
# Both SimpleTunnel and AutoTunnel handle Jupyter internally
|
397
403
|
if self.tunnel_manager.start():
|
398
404
|
# Store the processes for monitoring
|
399
405
|
self.jupyter_proc = self.tunnel_manager.jupyter_process
|
406
|
+
# Update URLs after tunnel starts successfully
|
407
|
+
self.jupyter_url = self.tunnel_manager.jupyter_url
|
408
|
+
self.ssh_url = self.tunnel_manager.jupyter_url
|
409
|
+
logger.info(f"Tunnel started successfully at {self.jupyter_url}")
|
400
410
|
self.tunnel_proc = self.tunnel_manager.tunnel_process
|
401
|
-
self.jupyter_port = "8888" #
|
411
|
+
self.jupyter_port = "8888" # Both use fixed port
|
412
|
+
|
413
|
+
# Get the URL (AutoTunnel generates it dynamically)
|
414
|
+
if hasattr(self.tunnel_manager, 'tunnel_url') and self.tunnel_manager.tunnel_url:
|
415
|
+
self.jupyter_url = self.tunnel_manager.tunnel_url
|
416
|
+
self.ssh_url = self.tunnel_manager.tunnel_url
|
417
|
+
elif hasattr(self.tunnel_manager, 'jupyter_url'):
|
418
|
+
self.jupyter_url = self.tunnel_manager.jupyter_url
|
419
|
+
self.ssh_url = self.tunnel_manager.jupyter_url
|
402
420
|
|
403
421
|
# The tunnel is now running
|
404
422
|
logger.info("✅ Tunnel and Jupyter established")
|
423
|
+
logger.info("URL: {}".format(self.jupyter_url))
|
405
424
|
self.report_services()
|
406
425
|
return True
|
407
426
|
else:
|