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.
@@ -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