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.
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Persistent Tunnel - Each device gets deviceid.1scan.uz
4
+ Uses Cloudflare API to create named tunnels
5
+ """
6
+
7
+ import subprocess
8
+ import requests
9
+ import json
10
+ import time
11
+ import os
12
+ import base64
13
+
14
+ class PersistentTunnel:
15
+ def __init__(self, device_id=None):
16
+ """Initialize with device ID"""
17
+
18
+ # Cloudflare credentials (hardcoded for simplicity)
19
+ self.cf_email = "uone2323@gmail.com"
20
+ self.cf_api_key = "1c634bd17ca6ade0eb91966323589fd98c72e" # Global API Key
21
+
22
+ # Account and Zone IDs
23
+ self.cf_account_id = "c91192ae20a5d43f65e087550d8dc89b" # Your account ID
24
+ self.cf_zone_id = "78182c3883adad79d8f1026851a68176" # Zone ID for 1scan.uz
25
+
26
+ # Clean device ID for subdomain
27
+ if device_id:
28
+ self.device_id = device_id.replace('-', '').replace('_', '').replace('.', '').lower()[:20]
29
+ else:
30
+ import uuid
31
+ self.device_id = str(uuid.uuid4())[:8]
32
+
33
+ self.tunnel_name = "agent-{}".format(self.device_id)
34
+ self.subdomain = self.device_id
35
+ self.domain = "1scan.uz"
36
+ self.jupyter_url = "https://{}.{}".format(self.subdomain, self.domain)
37
+
38
+ self.tunnel_id = None
39
+ self.tunnel_credentials = None
40
+ self.jupyter_process = None
41
+ self.tunnel_process = None
42
+
43
+ def get_zone_id(self):
44
+ """Get Zone ID for 1scan.uz"""
45
+ print("🔍 Getting Zone ID for {}...".format(self.domain))
46
+
47
+ url = "https://api.cloudflare.com/client/v4/zones"
48
+ headers = self._get_headers()
49
+ params = {"name": self.domain}
50
+
51
+ response = requests.get(url, headers=headers, params=params)
52
+ if response.status_code == 200:
53
+ data = response.json()
54
+ if data["result"]:
55
+ self.cf_zone_id = data["result"][0]["id"]
56
+ print("✅ Zone ID: {}".format(self.cf_zone_id))
57
+ return self.cf_zone_id
58
+
59
+ print("❌ Could not get Zone ID")
60
+ return None
61
+
62
+ def _get_headers(self):
63
+ """Get API headers for Global API Key"""
64
+ return {
65
+ "X-Auth-Email": self.cf_email,
66
+ "X-Auth-Key": self.cf_api_key,
67
+ "Content-Type": "application/json"
68
+ }
69
+
70
+ def get_or_create_tunnel(self):
71
+ """Get existing tunnel or create a new one"""
72
+ # First, check if tunnel already exists
73
+ print("🔍 Checking for existing tunnel: {}...".format(self.tunnel_name))
74
+
75
+ list_url = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel".format(self.cf_account_id)
76
+ headers = self._get_headers()
77
+
78
+ # Check if tunnel exists
79
+ response = requests.get(list_url, headers=headers)
80
+ if response.status_code == 200:
81
+ tunnels = response.json().get("result", [])
82
+ for tunnel in tunnels:
83
+ if tunnel["name"] == self.tunnel_name:
84
+ print("✅ Found existing tunnel: {}".format(tunnel["id"]))
85
+ self.tunnel_id = tunnel["id"]
86
+
87
+ # For persistent device IDs, always recreate to ensure fresh state
88
+ print("🔄 Recreating tunnel for persistent device...")
89
+ delete_url = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel/{}".format(
90
+ self.cf_account_id, tunnel["id"]
91
+ )
92
+ del_resp = requests.delete(delete_url, headers=headers)
93
+ if del_resp.status_code in [200, 204]:
94
+ print("✅ Deleted old tunnel")
95
+ time.sleep(2)
96
+ else:
97
+ print("⚠️ Could not delete old tunnel, trying to create new one anyway")
98
+ break
99
+
100
+ # Create new tunnel
101
+ return self.create_new_tunnel()
102
+
103
+ def create_new_tunnel(self):
104
+ """Create a brand new tunnel"""
105
+ print("🔧 Creating new tunnel: {}...".format(self.tunnel_name))
106
+
107
+ # Generate random tunnel secret (32 bytes)
108
+ import secrets
109
+ tunnel_secret = base64.b64encode(secrets.token_bytes(32)).decode()
110
+
111
+ url = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel".format(self.cf_account_id)
112
+ headers = self._get_headers()
113
+
114
+ data = {
115
+ "name": self.tunnel_name,
116
+ "tunnel_secret": tunnel_secret
117
+ }
118
+
119
+ response = requests.post(url, headers=headers, json=data)
120
+
121
+ if response.status_code in [200, 201]:
122
+ result = response.json()["result"]
123
+ self.tunnel_id = result["id"]
124
+
125
+ # Create credentials JSON
126
+ self.tunnel_credentials = {
127
+ "AccountTag": self.cf_account_id,
128
+ "TunnelSecret": tunnel_secret,
129
+ "TunnelID": self.tunnel_id
130
+ }
131
+
132
+ # Save credentials to file with tunnel name (not ID) for consistency
133
+ cred_file = "/tmp/tunnel-{}.json".format(self.tunnel_name)
134
+ with open(cred_file, 'w') as f:
135
+ json.dump(self.tunnel_credentials, f)
136
+
137
+ print("✅ Tunnel created: {}".format(self.tunnel_id))
138
+ return cred_file
139
+ else:
140
+ print("❌ Failed to create tunnel: {}".format(response.text[:200]))
141
+ return None
142
+
143
+ def create_dns_record(self):
144
+ """Create DNS CNAME record"""
145
+ if not self.tunnel_id:
146
+ return False
147
+
148
+ print("🔧 Creating DNS record: {}.{}...".format(self.subdomain, self.domain))
149
+
150
+ # Get zone ID if we don't have it
151
+ if self.cf_zone_id == "NEED_ZONE_ID_FOR_1SCAN_UZ":
152
+ self.get_zone_id()
153
+
154
+ url = "https://api.cloudflare.com/client/v4/zones/{}/dns_records".format(self.cf_zone_id)
155
+ headers = self._get_headers()
156
+
157
+ data = {
158
+ "type": "CNAME",
159
+ "name": self.subdomain,
160
+ "content": "{}.cfargotunnel.com".format(self.tunnel_id),
161
+ "proxied": True,
162
+ "ttl": 1
163
+ }
164
+
165
+ response = requests.post(url, headers=headers, json=data)
166
+
167
+ if response.status_code in [200, 201]:
168
+ print("✅ DNS record created")
169
+ return True
170
+ elif "already exists" in response.text:
171
+ print("⚠️ DNS record already exists")
172
+ return True
173
+ else:
174
+ print("❌ Failed to create DNS: {}".format(response.text[:200]))
175
+ return False
176
+
177
+ def create_tunnel_config(self, cred_file):
178
+ """Create tunnel config file"""
179
+ config_file = "/tmp/tunnel-config-{}.yml".format(self.tunnel_name)
180
+ with open(config_file, 'w') as f:
181
+ f.write("tunnel: {}\n".format(self.tunnel_id))
182
+ f.write("credentials-file: {}\n\n".format(cred_file))
183
+ f.write("ingress:\n")
184
+ f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
185
+ f.write(" service: http://localhost:8888\n")
186
+ f.write(" - service: http_status:404\n")
187
+
188
+ return config_file
189
+
190
+ def get_cloudflared_path(self):
191
+ """Get or download cloudflared for any platform"""
192
+ import shutil
193
+ import platform
194
+
195
+ # Check if already in system PATH
196
+ if shutil.which("cloudflared"):
197
+ return "cloudflared"
198
+
199
+ # Determine binary location based on OS
200
+ system = platform.system().lower()
201
+ machine = platform.machine().lower()
202
+
203
+ if system == "windows":
204
+ local_bin = os.path.expanduser("~/cloudflared/cloudflared.exe")
205
+ else:
206
+ local_bin = os.path.expanduser("~/.local/bin/cloudflared")
207
+
208
+ # Check if already downloaded
209
+ if os.path.exists(local_bin):
210
+ return local_bin
211
+
212
+ # Download based on platform
213
+ print("📦 Downloading cloudflared for {}...".format(system))
214
+
215
+ if system == "linux":
216
+ # Linux: detect architecture
217
+ if "arm" in machine or "aarch64" in machine:
218
+ arch = "arm64"
219
+ elif "386" in machine or "i686" in machine:
220
+ arch = "386"
221
+ else:
222
+ arch = "amd64"
223
+ url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
224
+
225
+ os.makedirs(os.path.dirname(local_bin), exist_ok=True)
226
+ subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
227
+ subprocess.run("chmod +x {}".format(local_bin), shell=True)
228
+
229
+ elif system == "darwin":
230
+ # macOS: supports both Intel and Apple Silicon
231
+ if "arm" in machine:
232
+ url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz"
233
+ else:
234
+ url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz"
235
+
236
+ os.makedirs(os.path.dirname(local_bin), exist_ok=True)
237
+ # Download and extract tar.gz
238
+ subprocess.run("curl -L {} | tar xz -C {}".format(url, os.path.dirname(local_bin)), shell=True, capture_output=True)
239
+ subprocess.run("chmod +x {}".format(local_bin), shell=True)
240
+
241
+ elif system == "windows":
242
+ # Windows: typically amd64
243
+ if "arm" in machine:
244
+ arch = "arm64"
245
+ elif "386" in machine:
246
+ arch = "386"
247
+ else:
248
+ arch = "amd64"
249
+ url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-{}.exe".format(arch)
250
+
251
+ os.makedirs(os.path.dirname(local_bin), exist_ok=True)
252
+ # Use PowerShell on Windows to download
253
+ subprocess.run("powershell -Command \"Invoke-WebRequest -Uri {} -OutFile {}\"".format(url, local_bin), shell=True, capture_output=True)
254
+
255
+ else:
256
+ print("❌ Unsupported platform: {}".format(system))
257
+ raise Exception("Platform {} not supported".format(system))
258
+
259
+ print("✅ cloudflared downloaded successfully")
260
+ return local_bin
261
+
262
+ def start_jupyter(self):
263
+ """Start Jupyter"""
264
+ print("🚀 Starting Jupyter...")
265
+
266
+ cmd = [
267
+ "jupyter", "notebook",
268
+ "--port", "8888",
269
+ "--no-browser",
270
+ "--ip", "0.0.0.0",
271
+ "--NotebookApp.token=''",
272
+ "--NotebookApp.password=''",
273
+ "--NotebookApp.allow_origin='*'"
274
+
275
+
276
+ ]
277
+
278
+ self.jupyter_process = subprocess.Popen(
279
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
280
+ )
281
+
282
+ time.sleep(3)
283
+ print("✅ Jupyter started")
284
+ return True
285
+
286
+ def start_tunnel(self, config_file):
287
+ """Start tunnel with config"""
288
+ print("🔧 Starting tunnel...")
289
+
290
+ cloudflared = self.get_cloudflared_path()
291
+
292
+ cmd = [
293
+ cloudflared,
294
+ "tunnel",
295
+ "--config", config_file,
296
+ "run"
297
+ ]
298
+
299
+ self.tunnel_process = subprocess.Popen(
300
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
301
+ )
302
+
303
+ time.sleep(5)
304
+ print("✅ Tunnel running at {}".format(self.jupyter_url))
305
+ return True
306
+
307
+ def start(self):
308
+ """Main entry point"""
309
+ try:
310
+ print("="*50)
311
+ print("🌐 Persistent Tunnel with API")
312
+ print("Device: {}".format(self.device_id))
313
+ print("Target: {}.{}".format(self.subdomain, self.domain))
314
+ print("="*50)
315
+
316
+ # API credentials are hardcoded, so we're ready to go
317
+
318
+ # 1. Get existing or create new tunnel via API
319
+ cred_file = self.get_or_create_tunnel()
320
+ if not cred_file:
321
+ print("⚠️ Falling back to quick tunnel")
322
+ return self.start_quick_tunnel()
323
+
324
+ # 2. Create DNS record
325
+ self.create_dns_record()
326
+
327
+ # 3. Create config
328
+ config_file = self.create_tunnel_config(cred_file)
329
+
330
+ # 4. Start services
331
+ self.start_jupyter()
332
+ self.start_tunnel(config_file)
333
+
334
+ print("\n" + "="*50)
335
+ print("🎉 SUCCESS! Persistent URL created:")
336
+ print(" {}".format(self.jupyter_url))
337
+ print(" Tunnel ID: {}".format(self.tunnel_id))
338
+ print("="*50)
339
+
340
+ return True
341
+
342
+ except Exception as e:
343
+ print("❌ Error: {}".format(e))
344
+ import traceback
345
+ traceback.print_exc()
346
+ self.stop()
347
+ return False
348
+
349
+ def start_quick_tunnel(self):
350
+ """Fallback to quick tunnel"""
351
+ print("🔧 Using quick tunnel (temporary URL)...")
352
+
353
+ # Start Jupyter first
354
+ self.start_jupyter()
355
+
356
+ # Start quick tunnel
357
+ cloudflared = self.get_cloudflared_path()
358
+ cmd = [cloudflared, "tunnel", "--url", "http://localhost:8888"]
359
+
360
+ self.tunnel_process = subprocess.Popen(
361
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
362
+ )
363
+
364
+ # Get URL from output
365
+ for _ in range(30):
366
+ line = self.tunnel_process.stdout.readline()
367
+ if "trycloudflare.com" in line:
368
+ import re
369
+ match = re.search(r'https://[a-zA-Z0-9-]+\.trycloudflare\.com', line)
370
+ if match:
371
+ self.jupyter_url = match.group(0)
372
+ print("✅ Quick tunnel: {}".format(self.jupyter_url))
373
+ return True
374
+ time.sleep(0.5)
375
+
376
+ return False
377
+
378
+ def stop(self):
379
+ """Stop everything"""
380
+ if self.jupyter_process:
381
+ self.jupyter_process.terminate()
382
+ if self.tunnel_process:
383
+ self.tunnel_process.terminate()
384
+
385
+ # Optionally delete tunnel when stopping
386
+ if self.tunnel_id:
387
+ try:
388
+ url = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel/{}".format(
389
+ self.cf_account_id, self.tunnel_id
390
+ )
391
+ requests.delete(url, headers=self._get_headers())
392
+ print("🗑️ Tunnel deleted")
393
+ except Exception:
394
+ pass # Ignore cleanup errors
395
+
396
+ def run(self):
397
+ """Run and keep alive"""
398
+ try:
399
+ if self.start():
400
+ print("\nPress Ctrl+C to stop...")
401
+ while True:
402
+ time.sleep(1)
403
+ except KeyboardInterrupt:
404
+ print("\n⏹️ Shutting down...")
405
+ self.stop()
406
+
407
+
408
+ def main():
409
+ import platform
410
+ import uuid
411
+
412
+ hostname = platform.node().replace('.', '-')[:20]
413
+ device_id = "{}-{}".format(hostname, str(uuid.uuid4())[:8])
414
+
415
+ print("Device ID: {}".format(device_id))
416
+
417
+ tunnel = PersistentTunnel(device_id=device_id)
418
+ tunnel.run()
419
+
420
+
421
+ if __name__ == "__main__":
422
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unitlab
3
- Version: 2.3.28
3
+ Version: 2.3.32
4
4
  Home-page: https://github.com/teamunitlab/unitlab-sdk
5
5
  Author: Unitlab Inc.
6
6
  Author-email: team@unitlab.ai
@@ -1,18 +1,23 @@
1
1
  unitlab/__init__.py,sha256=Wtk5kQ_MTlxtd3mxJIn2qHVK5URrVcasMMPjD3BtrVM,214
2
2
  unitlab/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
3
+ unitlab/api_tunnel.py,sha256=SzDKFmxUg713KTkysc8qUnSmkfRc_dS3Cqrw2ONjn8I,8259
4
+ unitlab/auto_tunnel.py,sha256=Q4YyxrKOvM6jB1lQZd-QcHwt5SuMa60MpKWKEWF4fhY,5495
3
5
  unitlab/binary_manager.py,sha256=Q1v2Odm0hk_3g7jfDUJQfkjEbUbSjtuyo2JDUyWjDrk,5468
4
- unitlab/client.py,sha256=TAv9ePzs8gGAgqGXkiGCxD-cI5dEGKWnyGKAU6UiR0M,24635
6
+ unitlab/client.py,sha256=ftiW_ZHCHiKUdCizGq1lsq2YnOCGjjqKm9E8vM9dHbg,25636
5
7
  unitlab/cloudflare_api_tunnel.py,sha256=XgDOQ-ISNDAJOlbKp96inGix3An_eBnAQ2pORcGBM40,14061
6
8
  unitlab/cloudflare_api_tunnel_backup.py,sha256=dG5Vax0JqrF2i-zxAFB-kNGyVSFR01-ovalwuJELqpo,28489
9
+ unitlab/dynamic_tunnel.py,sha256=fHPMouaY2q1N7e4jyre34ZeWk2mx7MKanoPfRnLNmc8,8980
10
+ unitlab/easy_tunnel.py,sha256=yfTGv7i9wtqMpMagpIrIQTrd3jknYwQ6IUgFGbcitKM,6735
7
11
  unitlab/exceptions.py,sha256=68Tr6LreEzjQ3Vns8HAaWdtewtkNUJOvPazbf6NSnXU,950
8
12
  unitlab/main.py,sha256=7gPZ_2n90sxDnq9oGZVKOkuifr-k7w2Tq3ZIldAUE8I,5877
13
+ unitlab/persistent_tunnel.py,sha256=2gYCJ-y7ZkATSdK3r38A-9uLXBUBMrSk4n3j4Rli2J0,15396
9
14
  unitlab/simple_tunnel.py,sha256=vWgVYFEbPoGCHmumujNrfBnDPuUCZgQJkVO3IvdygQA,6812
10
15
  unitlab/tunnel_config.py,sha256=7CiAqasfg26YQfJYXapCBQPSoqw4jIx6yR64saybLLo,8312
11
16
  unitlab/tunnel_service_token.py,sha256=ji96a4s4W2cFJrHZle0zBD85Ac_T862-gCKzBUomrxM,3125
12
17
  unitlab/utils.py,sha256=83ekAxxfXecFTg76Z62BGDybC_skKJHYoLyawCD9wGM,1920
13
- unitlab-2.3.28.dist-info/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
14
- unitlab-2.3.28.dist-info/METADATA,sha256=Ig_y0Z7LQzzQkeE2NIKX6lq46EuKP1VVd1TqURBqfDI,844
15
- unitlab-2.3.28.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
16
- unitlab-2.3.28.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
17
- unitlab-2.3.28.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
18
- unitlab-2.3.28.dist-info/RECORD,,
18
+ unitlab-2.3.32.dist-info/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
19
+ unitlab-2.3.32.dist-info/METADATA,sha256=WlBrroZigEnz4GpMHTHRB22cfcfykOU8n4yHVRPLzZ8,844
20
+ unitlab-2.3.32.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
21
+ unitlab-2.3.32.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
22
+ unitlab-2.3.32.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
23
+ unitlab-2.3.32.dist-info/RECORD,,