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/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"""
|
@@ -133,15 +151,15 @@ class PersistentTunnel:
|
|
133
151
|
print("✅ Tunnel created: {}".format(self.tunnel_id))
|
134
152
|
return cred_file
|
135
153
|
else:
|
136
|
-
print("❌ Failed to create tunnel: {}".format(response.text
|
154
|
+
print("❌ Failed to create tunnel: {}".format(response.text))
|
137
155
|
return None
|
138
156
|
|
139
157
|
def create_dns_record(self):
|
140
|
-
"""Create DNS CNAME
|
158
|
+
"""Create DNS CNAME records for main domain and SSH subdomain"""
|
141
159
|
if not self.tunnel_id:
|
142
160
|
return False
|
143
161
|
|
144
|
-
print("🔧 Creating DNS
|
162
|
+
print("🔧 Creating DNS records...")
|
145
163
|
|
146
164
|
# Get zone ID if we don't have it
|
147
165
|
if self.cf_zone_id == "NEED_ZONE_ID_FOR_1SCAN_UZ":
|
@@ -150,6 +168,7 @@ class PersistentTunnel:
|
|
150
168
|
url = "https://api.cloudflare.com/client/v4/zones/{}/dns_records".format(self.cf_zone_id)
|
151
169
|
headers = self._get_headers()
|
152
170
|
|
171
|
+
# Create main subdomain record for Jupyter and API
|
153
172
|
data = {
|
154
173
|
"type": "CNAME",
|
155
174
|
"name": self.subdomain,
|
@@ -161,13 +180,83 @@ class PersistentTunnel:
|
|
161
180
|
response = requests.post(url, headers=headers, json=data)
|
162
181
|
|
163
182
|
if response.status_code in [200, 201]:
|
164
|
-
print("✅ DNS record created")
|
165
|
-
return True
|
183
|
+
print("✅ Main DNS record created: {}.{}".format(self.subdomain, self.domain))
|
166
184
|
elif "already exists" in response.text:
|
167
|
-
print("⚠️ DNS record already exists")
|
185
|
+
print("⚠️ Main DNS record already exists: {}.{}".format(self.subdomain, self.domain))
|
186
|
+
else:
|
187
|
+
print("❌ Failed to create main DNS: {}".format(response.text[:200]))
|
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
|
210
|
+
|
211
|
+
def create_access_application(self):
|
212
|
+
"""Create Cloudflare Access application for SSH with bypass policy"""
|
213
|
+
print("🔧 Creating Access application for SSH...")
|
214
|
+
|
215
|
+
# Create Access application
|
216
|
+
app_url = "https://api.cloudflare.com/client/v4/zones/{}/access/apps".format(self.cf_zone_id)
|
217
|
+
headers = self._get_headers()
|
218
|
+
|
219
|
+
app_data = {
|
220
|
+
"name": "SSH-{}".format(self.device_id),
|
221
|
+
"domain": "{}.{}".format(self.ssh_subdomain, self.domain),
|
222
|
+
"type": "ssh",
|
223
|
+
"session_duration": "24h",
|
224
|
+
"auto_redirect_to_identity": False
|
225
|
+
}
|
226
|
+
|
227
|
+
app_response = requests.post(app_url, headers=headers, json=app_data)
|
228
|
+
|
229
|
+
if app_response.status_code in [200, 201]:
|
230
|
+
app_id = app_response.json()["result"]["id"]
|
231
|
+
print("✅ Access application created: {}".format(app_id))
|
232
|
+
|
233
|
+
# Create bypass policy (no authentication required)
|
234
|
+
policy_url = "https://api.cloudflare.com/client/v4/zones/{}/access/apps/{}/policies".format(
|
235
|
+
self.cf_zone_id, app_id
|
236
|
+
)
|
237
|
+
|
238
|
+
policy_data = {
|
239
|
+
"name": "Public Access",
|
240
|
+
"decision": "bypass",
|
241
|
+
"include": [
|
242
|
+
{"everyone": {}}
|
243
|
+
],
|
244
|
+
"precedence": 1
|
245
|
+
}
|
246
|
+
|
247
|
+
policy_response = requests.post(policy_url, headers=headers, json=policy_data)
|
248
|
+
|
249
|
+
if policy_response.status_code in [200, 201]:
|
250
|
+
print("✅ Bypass policy created - SSH is publicly accessible")
|
251
|
+
return True
|
252
|
+
else:
|
253
|
+
print("⚠️ Could not create bypass policy: {}".format(policy_response.text[:200]))
|
254
|
+
return False
|
255
|
+
elif "already exists" in app_response.text:
|
256
|
+
print("⚠️ Access application already exists")
|
168
257
|
return True
|
169
258
|
else:
|
170
|
-
print("
|
259
|
+
print("⚠️ Could not create Access application: {}".format(app_response.text[:200]))
|
171
260
|
return False
|
172
261
|
|
173
262
|
def create_tunnel_config(self, cred_file):
|
@@ -177,11 +266,24 @@ class PersistentTunnel:
|
|
177
266
|
f.write("tunnel: {}\n".format(self.tunnel_id))
|
178
267
|
f.write("credentials-file: {}\n\n".format(cred_file))
|
179
268
|
f.write("ingress:\n")
|
269
|
+
|
270
|
+
# SSH service on dedicated subdomain (s{deviceid}.unitlab-ai.com)
|
271
|
+
f.write(" - hostname: {}.{}\n".format(self.ssh_subdomain, self.domain))
|
272
|
+
f.write(" service: ssh://localhost:22\n")
|
273
|
+
|
274
|
+
# API (more specific path goes first)
|
275
|
+
f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
|
276
|
+
f.write(" path: /api-agent/*\n")
|
277
|
+
f.write(" service: http://localhost:8001\n")
|
278
|
+
|
279
|
+
# Jupyter (general hostname for HTTP)
|
180
280
|
f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
|
181
281
|
f.write(" service: http://localhost:8888\n")
|
282
|
+
|
283
|
+
# Catch-all 404 (MUST be last!)
|
182
284
|
f.write(" - service: http_status:404\n")
|
183
|
-
|
184
|
-
|
285
|
+
return config_file
|
286
|
+
|
185
287
|
|
186
288
|
def get_cloudflared_path(self):
|
187
289
|
"""Get or download cloudflared for any platform"""
|
@@ -255,6 +357,9 @@ class PersistentTunnel:
|
|
255
357
|
print("✅ cloudflared downloaded successfully")
|
256
358
|
return local_bin
|
257
359
|
|
360
|
+
|
361
|
+
|
362
|
+
|
258
363
|
def start_jupyter(self):
|
259
364
|
"""Start Jupyter"""
|
260
365
|
print("🚀 Starting Jupyter...")
|
@@ -274,31 +379,59 @@ class PersistentTunnel:
|
|
274
379
|
self.jupyter_process = subprocess.Popen(
|
275
380
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
276
381
|
)
|
277
|
-
|
382
|
+
|
278
383
|
time.sleep(3)
|
279
384
|
print("✅ Jupyter started")
|
280
385
|
return True
|
281
386
|
|
282
|
-
def
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
)
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
387
|
+
def start_api(self):
|
388
|
+
def run_api():
|
389
|
+
uvicorn.run(
|
390
|
+
api,
|
391
|
+
port=8001
|
392
|
+
)
|
393
|
+
|
394
|
+
api_thread = threading.Thread(target=run_api, daemon=True)
|
395
|
+
api_thread.start()
|
396
|
+
print('API is started')
|
397
|
+
|
398
|
+
def start_tunnel(self, config_file):
|
399
|
+
"""Start tunnel with config"""
|
400
|
+
print("🔧 Starting tunnel...")
|
401
|
+
|
402
|
+
cloudflared = self.get_cloudflared_path()
|
403
|
+
|
404
|
+
cmd = [
|
405
|
+
cloudflared,
|
406
|
+
"tunnel",
|
407
|
+
"--config", config_file,
|
408
|
+
"run"
|
409
|
+
]
|
410
|
+
|
411
|
+
self.tunnel_process = subprocess.Popen(
|
412
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
413
|
+
)
|
414
|
+
|
415
|
+
time.sleep(2)
|
416
|
+
|
417
|
+
# Check if process is still running
|
418
|
+
if self.tunnel_process.poll() is not None:
|
419
|
+
print("❌ Tunnel process died!")
|
420
|
+
# Try to read error output
|
421
|
+
try:
|
422
|
+
stdout, stderr = self.tunnel_process.communicate(timeout=1)
|
423
|
+
if stderr:
|
424
|
+
print(f"Error: {stderr.decode()}")
|
425
|
+
if stdout:
|
426
|
+
print(f"Output: {stdout.decode()}")
|
427
|
+
except:
|
428
|
+
pass
|
429
|
+
return False
|
430
|
+
|
431
|
+
print("✅ Tunnel running at {}".format(self.jupyter_url))
|
432
|
+
print("✅ API running at {}".format(self.api_expose_url))
|
433
|
+
print("✅ SSH available at {}".format(self.ssh_url))
|
434
|
+
return True
|
302
435
|
|
303
436
|
def start(self):
|
304
437
|
"""Main entry point"""
|
@@ -313,25 +446,37 @@ class PersistentTunnel:
|
|
313
446
|
|
314
447
|
# 1. Get existing or create new tunnel via API
|
315
448
|
cred_file = self.get_or_create_tunnel()
|
316
|
-
|
317
|
-
print("⚠️ Falling back to quick tunnel")
|
318
|
-
return self.start_quick_tunnel()
|
449
|
+
|
319
450
|
|
320
451
|
# 2. Create DNS record
|
321
452
|
self.create_dns_record()
|
322
453
|
|
323
|
-
# 3. Create
|
454
|
+
# 3. Create Access application for SSH
|
455
|
+
self.create_access_application()
|
456
|
+
|
457
|
+
# 4. Create config
|
324
458
|
config_file = self.create_tunnel_config(cred_file)
|
325
459
|
|
326
|
-
#
|
460
|
+
# 5. Start services
|
327
461
|
self.start_jupyter()
|
462
|
+
self.start_api()
|
328
463
|
self.start_tunnel(config_file)
|
329
464
|
|
330
465
|
print("\n" + "="*50)
|
331
|
-
print("🎉 SUCCESS! Persistent
|
332
|
-
print(" {}".format(self.jupyter_url))
|
333
|
-
print("
|
466
|
+
print("🎉 SUCCESS! Persistent URLs created:")
|
467
|
+
print("📔 Jupyter: {}".format(self.jupyter_url))
|
468
|
+
print("🔧 API: {}".format(self.api_expose_url))
|
469
|
+
print("🔐 SSH: {}".format(self.ssh_url))
|
470
|
+
print("")
|
471
|
+
print("SSH Connection Command:")
|
472
|
+
import getpass
|
473
|
+
current_user = getpass.getuser()
|
474
|
+
print("ssh -o ProxyCommand='cloudflared access ssh --hostname {}' {}@{}".format(
|
475
|
+
self.ssh_url, current_user, self.ssh_url))
|
476
|
+
print("")
|
477
|
+
print("Tunnel ID: {}".format(self.tunnel_id))
|
334
478
|
print("="*50)
|
479
|
+
|
335
480
|
|
336
481
|
return True
|
337
482
|
|
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.35
|
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=wqREtDuYc5ixeloPEGm0hp1sdUtB59sB1bIJjBcO1y0,25983
|
4
|
+
unitlab/exceptions.py,sha256=68Tr6LreEzjQ3Vns8HAaWdtewtkNUJOvPazbf6NSnXU,950
|
5
|
+
unitlab/main.py,sha256=kazASmzhPaAcf9hsPZdewcry_vplsrRLfziPxKlPT70,4425
|
6
|
+
unitlab/persistent_tunnel.py,sha256=AHiOTVkxAlwsV0-19wxuX6-I_1l3rwv9hQiCSyLOFrA,23410
|
7
|
+
unitlab/utils.py,sha256=9gPRu-d6pbhSoVdll1GXe4eoz_uFYOSbYArFDQdlUZs,1922
|
8
|
+
unitlab-2.3.35.dist-info/licenses/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
|
9
|
+
unitlab-2.3.35.dist-info/METADATA,sha256=x1Oucw_bpzTjYQbkIl56g2f7I-28eE5vmWFR0TqsKvA,1046
|
10
|
+
unitlab-2.3.35.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
+
unitlab-2.3.35.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
|
12
|
+
unitlab-2.3.35.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
|
13
|
+
unitlab-2.3.35.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()
|