unitlab 2.3.32__py3-none-any.whl → 2.3.34__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 +79 -64
- unitlab/main.py +17 -43
- unitlab/persistent_tunnel.py +149 -62
- unitlab/utils.py +2 -0
- {unitlab-2.3.32.dist-info → unitlab-2.3.34.dist-info}/METADATA +12 -3
- unitlab-2.3.34.dist-info/RECORD +13 -0
- {unitlab-2.3.32.dist-info → unitlab-2.3.34.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.32.dist-info/RECORD +0 -23
- {unitlab-2.3.32.dist-info → unitlab-2.3.34.dist-info}/entry_points.txt +0 -0
- {unitlab-2.3.32.dist-info → unitlab-2.3.34.dist-info/licenses}/LICENSE.md +0 -0
- {unitlab-2.3.32.dist-info → unitlab-2.3.34.dist-info}/top_level.txt +0 -0
unitlab/persistent_tunnel.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
Persistent Tunnel - Each device gets deviceid.
|
3
|
+
Persistent Tunnel - Each device gets deviceid.unitlab-ai.com
|
4
4
|
Uses Cloudflare API to create named tunnels
|
5
5
|
"""
|
6
6
|
|
@@ -10,18 +10,32 @@ import json
|
|
10
10
|
import time
|
11
11
|
import os
|
12
12
|
import base64
|
13
|
+
from fastapi import FastAPI
|
14
|
+
import uvicorn
|
15
|
+
import threading
|
16
|
+
import psutil
|
17
|
+
|
18
|
+
|
19
|
+
api = FastAPI()
|
20
|
+
|
21
|
+
@api.get("/api-agent/")
|
22
|
+
def get_cpu_info():
|
23
|
+
cpu_usage_percent = psutil.cpu_percent(interval=1)
|
24
|
+
ram = psutil.virtual_memory()
|
25
|
+
return {"cpu_percentage": cpu_usage_percent, 'cpu_count': psutil.cpu_count(), 'ram_usage': ram.used }
|
26
|
+
|
13
27
|
|
14
28
|
class PersistentTunnel:
|
15
29
|
def __init__(self, device_id=None):
|
16
30
|
"""Initialize with device ID"""
|
17
31
|
|
18
32
|
# Cloudflare credentials (hardcoded for simplicity)
|
19
|
-
|
20
|
-
self.cf_api_key = "
|
33
|
+
|
34
|
+
self.cf_api_key = "RoIAn1t9rMqcGK7_Xja216pxbRTyFafC1jeRKIO3"
|
21
35
|
|
22
36
|
# Account and Zone IDs
|
23
|
-
self.cf_account_id = "
|
24
|
-
self.cf_zone_id = "
|
37
|
+
self.cf_account_id = "29df28cf48a30be3b1aa344b840400e6" # Your account ID
|
38
|
+
self.cf_zone_id = "eae80a730730b3b218a80dace996535a" # Zone ID for unitlab-ai.com
|
25
39
|
|
26
40
|
# Clean device ID for subdomain
|
27
41
|
if device_id:
|
@@ -32,16 +46,19 @@ class PersistentTunnel:
|
|
32
46
|
|
33
47
|
self.tunnel_name = "agent-{}".format(self.device_id)
|
34
48
|
self.subdomain = self.device_id
|
35
|
-
self.domain = "
|
49
|
+
self.domain = "unitlab-ai.com"
|
36
50
|
self.jupyter_url = "https://{}.{}".format(self.subdomain, self.domain)
|
37
|
-
|
51
|
+
self.api_expose_url = "https://{}.{}/api-agent/".format(self.subdomain, self.domain)
|
52
|
+
self.ssh_subdomain = "s{}".format(self.device_id) # Shorter SSH subdomain to avoid length issues
|
53
|
+
self.ssh_url = "{}.{}".format(self.ssh_subdomain, self.domain) # SSH on s{deviceid}.unitlab-ai.com
|
54
|
+
|
38
55
|
self.tunnel_id = None
|
39
56
|
self.tunnel_credentials = None
|
40
57
|
self.jupyter_process = None
|
41
58
|
self.tunnel_process = None
|
42
59
|
|
43
60
|
def get_zone_id(self):
|
44
|
-
"""Get Zone ID for
|
61
|
+
"""Get Zone ID for unitlab-ai.com"""
|
45
62
|
print("🔍 Getting Zone ID for {}...".format(self.domain))
|
46
63
|
|
47
64
|
url = "https://api.cloudflare.com/client/v4/zones"
|
@@ -61,11 +78,12 @@ class PersistentTunnel:
|
|
61
78
|
|
62
79
|
def _get_headers(self):
|
63
80
|
"""Get API headers for Global API Key"""
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
"
|
68
|
-
|
81
|
+
|
82
|
+
|
83
|
+
return {
|
84
|
+
"Authorization": f"Bearer {self.cf_api_key}",
|
85
|
+
"Content-Type": "application/json"
|
86
|
+
}
|
69
87
|
|
70
88
|
def get_or_create_tunnel(self):
|
71
89
|
"""Get existing tunnel or create a new one"""
|
@@ -84,18 +102,14 @@ class PersistentTunnel:
|
|
84
102
|
print("✅ Found existing tunnel: {}".format(tunnel["id"]))
|
85
103
|
self.tunnel_id = tunnel["id"]
|
86
104
|
|
87
|
-
#
|
88
|
-
print("
|
89
|
-
|
90
|
-
|
91
|
-
)
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
time.sleep(2)
|
96
|
-
else:
|
97
|
-
print("⚠️ Could not delete old tunnel, trying to create new one anyway")
|
98
|
-
break
|
105
|
+
# Tunnel exists, create a new one with unique name
|
106
|
+
print("⚠️ Tunnel with this name already exists")
|
107
|
+
import uuid
|
108
|
+
unique_suffix = str(uuid.uuid4())[:8]
|
109
|
+
self.tunnel_name = "agent-{}-{}".format(self.device_id, unique_suffix)
|
110
|
+
print("🔄 Creating new tunnel with unique name: {}".format(self.tunnel_name))
|
111
|
+
# Don't break, let it continue to create new tunnel
|
112
|
+
return self.create_new_tunnel()
|
99
113
|
|
100
114
|
# Create new tunnel
|
101
115
|
return self.create_new_tunnel()
|
@@ -137,15 +151,15 @@ class PersistentTunnel:
|
|
137
151
|
print("✅ Tunnel created: {}".format(self.tunnel_id))
|
138
152
|
return cred_file
|
139
153
|
else:
|
140
|
-
print("❌ Failed to create tunnel: {}".format(response.text
|
154
|
+
print("❌ Failed to create tunnel: {}".format(response.text))
|
141
155
|
return None
|
142
156
|
|
143
157
|
def create_dns_record(self):
|
144
|
-
"""Create DNS CNAME
|
158
|
+
"""Create DNS CNAME records for main domain and SSH subdomain"""
|
145
159
|
if not self.tunnel_id:
|
146
160
|
return False
|
147
161
|
|
148
|
-
print("🔧 Creating DNS
|
162
|
+
print("🔧 Creating DNS records...")
|
149
163
|
|
150
164
|
# Get zone ID if we don't have it
|
151
165
|
if self.cf_zone_id == "NEED_ZONE_ID_FOR_1SCAN_UZ":
|
@@ -154,6 +168,7 @@ class PersistentTunnel:
|
|
154
168
|
url = "https://api.cloudflare.com/client/v4/zones/{}/dns_records".format(self.cf_zone_id)
|
155
169
|
headers = self._get_headers()
|
156
170
|
|
171
|
+
# Create main subdomain record for Jupyter and API
|
157
172
|
data = {
|
158
173
|
"type": "CNAME",
|
159
174
|
"name": self.subdomain,
|
@@ -165,14 +180,33 @@ class PersistentTunnel:
|
|
165
180
|
response = requests.post(url, headers=headers, json=data)
|
166
181
|
|
167
182
|
if response.status_code in [200, 201]:
|
168
|
-
print("✅ DNS record created")
|
169
|
-
return True
|
183
|
+
print("✅ Main DNS record created: {}.{}".format(self.subdomain, self.domain))
|
170
184
|
elif "already exists" in response.text:
|
171
|
-
print("⚠️ DNS record already exists")
|
172
|
-
return True
|
185
|
+
print("⚠️ Main DNS record already exists: {}.{}".format(self.subdomain, self.domain))
|
173
186
|
else:
|
174
|
-
print("❌ Failed to create DNS: {}".format(response.text[:200]))
|
187
|
+
print("❌ Failed to create main DNS: {}".format(response.text[:200]))
|
175
188
|
return False
|
189
|
+
|
190
|
+
# Create SSH subdomain record (s{deviceid}.unitlab-ai.com)
|
191
|
+
ssh_data = {
|
192
|
+
"type": "CNAME",
|
193
|
+
"name": self.ssh_subdomain,
|
194
|
+
"content": "{}.cfargotunnel.com".format(self.tunnel_id),
|
195
|
+
"proxied": True,
|
196
|
+
"ttl": 1
|
197
|
+
}
|
198
|
+
|
199
|
+
ssh_response = requests.post(url, headers=headers, json=ssh_data)
|
200
|
+
|
201
|
+
if ssh_response.status_code in [200, 201]:
|
202
|
+
print("✅ SSH DNS record created: {}.{}".format(self.ssh_subdomain, self.domain))
|
203
|
+
elif "already exists" in ssh_response.text:
|
204
|
+
print("⚠️ SSH DNS record already exists: {}.{}".format(self.ssh_subdomain, self.domain))
|
205
|
+
else:
|
206
|
+
print("⚠️ Could not create SSH DNS: {}".format(ssh_response.text[:200]))
|
207
|
+
# SSH is optional, so we continue even if SSH DNS fails
|
208
|
+
|
209
|
+
return True
|
176
210
|
|
177
211
|
def create_tunnel_config(self, cred_file):
|
178
212
|
"""Create tunnel config file"""
|
@@ -181,11 +215,24 @@ class PersistentTunnel:
|
|
181
215
|
f.write("tunnel: {}\n".format(self.tunnel_id))
|
182
216
|
f.write("credentials-file: {}\n\n".format(cred_file))
|
183
217
|
f.write("ingress:\n")
|
218
|
+
|
219
|
+
# SSH service on dedicated subdomain (s{deviceid}.unitlab-ai.com)
|
220
|
+
f.write(" - hostname: {}.{}\n".format(self.ssh_subdomain, self.domain))
|
221
|
+
f.write(" service: ssh://localhost:22\n")
|
222
|
+
|
223
|
+
# API (more specific path goes first)
|
224
|
+
f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
|
225
|
+
f.write(" path: /api-agent/*\n")
|
226
|
+
f.write(" service: http://localhost:8001\n")
|
227
|
+
|
228
|
+
# Jupyter (general hostname for HTTP)
|
184
229
|
f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
|
185
230
|
f.write(" service: http://localhost:8888\n")
|
231
|
+
|
232
|
+
# Catch-all 404 (MUST be last!)
|
186
233
|
f.write(" - service: http_status:404\n")
|
187
|
-
|
188
|
-
|
234
|
+
return config_file
|
235
|
+
|
189
236
|
|
190
237
|
def get_cloudflared_path(self):
|
191
238
|
"""Get or download cloudflared for any platform"""
|
@@ -259,6 +306,9 @@ class PersistentTunnel:
|
|
259
306
|
print("✅ cloudflared downloaded successfully")
|
260
307
|
return local_bin
|
261
308
|
|
309
|
+
|
310
|
+
|
311
|
+
|
262
312
|
def start_jupyter(self):
|
263
313
|
"""Start Jupyter"""
|
264
314
|
print("🚀 Starting Jupyter...")
|
@@ -278,31 +328,59 @@ class PersistentTunnel:
|
|
278
328
|
self.jupyter_process = subprocess.Popen(
|
279
329
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
280
330
|
)
|
281
|
-
|
331
|
+
|
282
332
|
time.sleep(3)
|
283
333
|
print("✅ Jupyter started")
|
284
334
|
return True
|
285
335
|
|
286
|
-
def
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
)
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
336
|
+
def start_api(self):
|
337
|
+
def run_api():
|
338
|
+
uvicorn.run(
|
339
|
+
api,
|
340
|
+
port=8001
|
341
|
+
)
|
342
|
+
|
343
|
+
api_thread = threading.Thread(target=run_api, daemon=True)
|
344
|
+
api_thread.start()
|
345
|
+
print('API is started')
|
346
|
+
|
347
|
+
def start_tunnel(self, config_file):
|
348
|
+
"""Start tunnel with config"""
|
349
|
+
print("🔧 Starting tunnel...")
|
350
|
+
|
351
|
+
cloudflared = self.get_cloudflared_path()
|
352
|
+
|
353
|
+
cmd = [
|
354
|
+
cloudflared,
|
355
|
+
"tunnel",
|
356
|
+
"--config", config_file,
|
357
|
+
"run"
|
358
|
+
]
|
359
|
+
|
360
|
+
self.tunnel_process = subprocess.Popen(
|
361
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
362
|
+
)
|
363
|
+
|
364
|
+
time.sleep(2)
|
365
|
+
|
366
|
+
# Check if process is still running
|
367
|
+
if self.tunnel_process.poll() is not None:
|
368
|
+
print("❌ Tunnel process died!")
|
369
|
+
# Try to read error output
|
370
|
+
try:
|
371
|
+
stdout, stderr = self.tunnel_process.communicate(timeout=1)
|
372
|
+
if stderr:
|
373
|
+
print(f"Error: {stderr.decode()}")
|
374
|
+
if stdout:
|
375
|
+
print(f"Output: {stdout.decode()}")
|
376
|
+
except:
|
377
|
+
pass
|
378
|
+
return False
|
379
|
+
|
380
|
+
print("✅ Tunnel running at {}".format(self.jupyter_url))
|
381
|
+
print("✅ API running at {}".format(self.api_expose_url))
|
382
|
+
print("✅ SSH available at {}".format(self.ssh_url))
|
383
|
+
return True
|
306
384
|
|
307
385
|
def start(self):
|
308
386
|
"""Main entry point"""
|
@@ -317,9 +395,7 @@ class PersistentTunnel:
|
|
317
395
|
|
318
396
|
# 1. Get existing or create new tunnel via API
|
319
397
|
cred_file = self.get_or_create_tunnel()
|
320
|
-
|
321
|
-
print("⚠️ Falling back to quick tunnel")
|
322
|
-
return self.start_quick_tunnel()
|
398
|
+
|
323
399
|
|
324
400
|
# 2. Create DNS record
|
325
401
|
self.create_dns_record()
|
@@ -329,13 +405,24 @@ class PersistentTunnel:
|
|
329
405
|
|
330
406
|
# 4. Start services
|
331
407
|
self.start_jupyter()
|
408
|
+
self.start_api()
|
332
409
|
self.start_tunnel(config_file)
|
333
410
|
|
334
411
|
print("\n" + "="*50)
|
335
|
-
print("🎉 SUCCESS! Persistent
|
336
|
-
print(" {}".format(self.jupyter_url))
|
337
|
-
print("
|
412
|
+
print("🎉 SUCCESS! Persistent URLs created:")
|
413
|
+
print("📔 Jupyter: {}".format(self.jupyter_url))
|
414
|
+
print("🔧 API: {}".format(self.api_expose_url))
|
415
|
+
print("🔐 SSH: {}".format(self.ssh_url))
|
416
|
+
print("")
|
417
|
+
print("SSH Connection Command:")
|
418
|
+
import getpass
|
419
|
+
current_user = getpass.getuser()
|
420
|
+
print("ssh -o ProxyCommand='cloudflared access ssh --hostname {}' {}@{}".format(
|
421
|
+
self.ssh_url, current_user, self.ssh_url))
|
422
|
+
print("")
|
423
|
+
print("Tunnel ID: {}".format(self.tunnel_id))
|
338
424
|
print("="*50)
|
425
|
+
|
339
426
|
|
340
427
|
return True
|
341
428
|
|
unitlab/utils.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: unitlab
|
3
|
-
Version: 2.3.
|
3
|
+
Version: 2.3.34
|
4
4
|
Home-page: https://github.com/teamunitlab/unitlab-sdk
|
5
5
|
Author: Unitlab Inc.
|
6
6
|
Author-email: team@unitlab.ai
|
@@ -25,4 +25,13 @@ Requires-Dist: psutil
|
|
25
25
|
Requires-Dist: pyyaml
|
26
26
|
Requires-Dist: jupyter
|
27
27
|
Requires-Dist: python-dotenv
|
28
|
-
|
28
|
+
Requires-Dist: uvicorn
|
29
|
+
Requires-Dist: fastapi
|
30
|
+
Dynamic: author
|
31
|
+
Dynamic: author-email
|
32
|
+
Dynamic: classifier
|
33
|
+
Dynamic: home-page
|
34
|
+
Dynamic: keywords
|
35
|
+
Dynamic: license
|
36
|
+
Dynamic: license-file
|
37
|
+
Dynamic: requires-dist
|
@@ -0,0 +1,13 @@
|
|
1
|
+
unitlab/__init__.py,sha256=Wtk5kQ_MTlxtd3mxJIn2qHVK5URrVcasMMPjD3BtrVM,214
|
2
|
+
unitlab/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
|
3
|
+
unitlab/client.py,sha256=LMZ7HFjRxzPV2IrCXlP2GETlY0vWvAxP0RrjSXOn_Jk,26015
|
4
|
+
unitlab/exceptions.py,sha256=68Tr6LreEzjQ3Vns8HAaWdtewtkNUJOvPazbf6NSnXU,950
|
5
|
+
unitlab/main.py,sha256=EbQNO-Z5drNQjDXJp_sIs5a3WgPoqxaXxpdFGzMWm6k,4416
|
6
|
+
unitlab/persistent_tunnel.py,sha256=XDJo2PPq4EjEtI4vT68LIGUUq7WV4m0bnXYKPfY51cY,21180
|
7
|
+
unitlab/utils.py,sha256=9gPRu-d6pbhSoVdll1GXe4eoz_uFYOSbYArFDQdlUZs,1922
|
8
|
+
unitlab-2.3.34.dist-info/licenses/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
|
9
|
+
unitlab-2.3.34.dist-info/METADATA,sha256=ctnyV_ZT0vuOzGMoH0N09v0nalsR79WQCGdi_D-ncw0,1046
|
10
|
+
unitlab-2.3.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
+
unitlab-2.3.34.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
|
12
|
+
unitlab-2.3.34.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
|
13
|
+
unitlab-2.3.34.dist-info/RECORD,,
|
unitlab/api_tunnel.py
DELETED
@@ -1,238 +0,0 @@
|
|
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()
|