unitlab 2.3.18__py3-none-any.whl → 2.3.23__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,6 +1,7 @@
1
1
  """
2
2
  Cloudflare API-based Tunnel Configuration
3
3
  Uses API to dynamically manage DNS and routes
4
+ SIMPLIFIED VERSION with unique tunnel names
4
5
  """
5
6
 
6
7
  import os
@@ -8,6 +9,7 @@ import requests
8
9
  import subprocess
9
10
  import time
10
11
  import logging
12
+ import uuid
11
13
  from pathlib import Path
12
14
  from .binary_manager import CloudflaredBinaryManager
13
15
 
@@ -18,99 +20,85 @@ try:
18
20
  if env_path.exists():
19
21
  load_dotenv(env_path)
20
22
  except ImportError:
21
- pass # dotenv not installed, use system env vars only
22
-
23
- logger = logging.getLogger(__name__)
24
-
23
+ pass
25
24
 
26
25
  class CloudflareAPITunnel:
27
- def __init__(self, base_domain, device_id):
26
+ def __init__(self, device_id, base_domain="1scan.uz"):
28
27
  """
29
- Initialize API-based tunnel manager
28
+ Initialize Cloudflare tunnel with API configuration
29
+ Each device gets a unique tunnel with UUID
30
30
  """
31
- self.base_domain = "1scan.uz"
32
31
  self.device_id = device_id
32
+ self.base_domain = base_domain
33
33
 
34
- # Clean device ID for subdomain
35
- self.clean_device_id = device_id.replace('-', '').replace('_', '').lower()[:20]
34
+ # Clean device ID for use in hostnames (remove special chars)
35
+ self.clean_device_id = device_id.replace(' ', '').replace('-', '').replace('.', '').replace('_', '')[:24]
36
36
 
37
- # Cloudflare IDs - hardcoded for zero-config experience
38
- self.zone_id = "78182c3883adad79d8f1026851a68176"
39
- self.account_id = "c91192ae20a5d43f65e087550d8dc89b"
40
- self.tunnel_id = "0777fc10-49c4-472d-8661-f60d80d6184d" # unitlab-agent tunnel
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}"
41
40
 
42
- # API token - hardcoded for zero-config experience
43
- # This token only has DNS edit permissions for 1scan.uz - limited scope for safety
44
- self.api_token = "LJLe6QMOtpN0MeuLQ05_zUKKxVm4vEibkC8lxSJd"
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
45
 
46
- if not self.api_token:
47
- logger.warning("Using fallback tunnel configuration without API management.")
46
+ # Hardcoded Cloudflare credentials
47
+ self.account_id = "c91192ae20a5d43f65e087550d8dc89b"
48
+ self.api_token = "xkfCGvxHeiU7xvz0OhfYUICZvJ7rM4NmgdMSB5jy"
49
+ self.zone_id = "f17ca0e9cf056e87afb019c88f936ac9"
48
50
 
49
- # API setup
51
+ # API configuration
50
52
  self.api_base = "https://api.cloudflare.com/client/v4"
51
53
  self.headers = {
52
54
  "Authorization": f"Bearer {self.api_token}",
53
55
  "Content-Type": "application/json"
54
- } if self.api_token else {}
55
-
56
- # URLs for services - simplified for Jupyter only
57
- self.jupyter_subdomain = f"j{self.clean_device_id}"
58
- self.jupyter_url = f"https://{self.jupyter_subdomain}.{self.base_domain}"
59
-
60
- # Keep SSH URLs for compatibility but they won't work yet
61
- self.ssh_subdomain = f"s{self.clean_device_id}"
62
- self.ssh_hostname = f"{self.ssh_subdomain}.{self.base_domain}"
63
- self.ssh_url = self.ssh_hostname
56
+ }
64
57
 
58
+ # Track created resources for cleanup
59
+ self.tunnel_id = None
65
60
  self.tunnel_process = None
66
61
  self.created_dns_records = []
67
62
  self.tunnel_config_file = None
68
63
 
69
- # Try to initialize binary manager, but don't fail if it doesn't work
70
- try:
71
- self.binary_manager = CloudflaredBinaryManager()
72
- except Exception as e:
73
- logger.warning(f"Binary manager initialization failed: {e}")
74
- self.binary_manager = None
75
-
64
+ # Binary manager for cloudflared
65
+ self.binary_manager = CloudflaredBinaryManager()
66
+
76
67
  def create_dns_records(self):
77
68
  """
78
- Create DNS CNAME records for this device
69
+ Create DNS records pointing to the tunnel
79
70
  """
80
- if not self.api_token:
81
- print("⚠️ No API token configured. Skipping DNS creation.")
82
- print(" Assuming DNS records already exist or will be created manually.")
83
- return True
84
-
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
+
85
75
  print(f"📡 Creating DNS records for device {self.device_id}...")
86
76
 
87
- records = [
88
- {"name": self.jupyter_subdomain, "comment": f"Jupyter for {self.device_id}"},
89
- {"name": self.ssh_subdomain, "comment": f"SSH for {self.device_id}"}
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"}
90
80
  ]
91
81
 
92
- for record in records:
82
+ for record in dns_records:
93
83
  try:
94
84
  # Check if record exists
95
85
  check_url = f"{self.api_base}/zones/{self.zone_id}/dns_records"
96
- params = {"name": f"{record['name']}.{self.base_domain}", "type": "CNAME"}
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)
97
88
 
98
- response = requests.get(check_url, headers=self.headers, params=params)
99
- existing = response.json()
100
-
101
- if existing.get("result") and len(existing["result"]) > 0:
102
- # Record exists
103
- print(f" ✓ DNS record {record['name']}.{self.base_domain} already exists")
104
- continue
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
105
94
 
106
95
  # Create new record
107
96
  data = {
108
97
  "type": "CNAME",
109
- "name": record["name"],
110
- "content": f"{self.tunnel_id}.cfargotunnel.com",
111
- "ttl": 1, # Auto
112
- "proxied": True,
113
- "comment": record["comment"]
98
+ "name": record['name'],
99
+ "content": record['content'],
100
+ "ttl": 1,
101
+ "proxied": True
114
102
  }
115
103
 
116
104
  response = requests.post(check_url, headers=self.headers, json=data)
@@ -130,157 +118,58 @@ class CloudflareAPITunnel:
130
118
  continue
131
119
 
132
120
  return True
133
-
134
- def update_tunnel_config(self, jupyter_port=8888, ssh_port=22):
121
+
122
+ def create_device_tunnel(self):
135
123
  """
136
- Update tunnel configuration via API
124
+ Create a unique tunnel for this device
125
+ Each tunnel gets a unique UUID to avoid conflicts
137
126
  """
138
- if not self.api_token:
139
- print("⚠️ No API token. Tunnel will use existing configuration.")
140
- return True
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}"
141
130
 
142
- print(f"🔧 Configuring tunnel routes...")
131
+ print(f"📦 Creating new tunnel: {tunnel_name}")
143
132
 
144
- # Get current tunnel config first
145
- get_url = f"{self.api_base}/accounts/{self.account_id}/tunnels/{self.tunnel_id}/configurations"
133
+ create_url = f"{self.api_base}/accounts/{self.account_id}/tunnels"
146
134
 
147
- try:
148
- # Get existing config
149
- response = requests.get(get_url, headers=self.headers)
150
- current_config = response.json() if response.status_code == 200 else {}
151
-
152
- # Build new ingress rules
153
- new_ingress = [
154
- {
155
- "hostname": f"{self.jupyter_subdomain}.{self.base_domain}",
156
- "service": f"http://localhost:{jupyter_port}",
157
- "originRequest": {
158
- "noTLSVerify": True
159
- }
160
- },
161
- {
162
- "hostname": f"{self.ssh_subdomain}.{self.base_domain}",
163
- "service": f"ssh://localhost:{ssh_port}"
164
- }
165
- ]
135
+ # Generate secret
136
+ tunnel_secret = os.urandom(32).hex()
137
+
138
+ create_data = {
139
+ "name": tunnel_name,
140
+ "tunnel_secret": tunnel_secret
141
+ }
142
+
143
+ create_response = requests.post(create_url, headers=self.headers, json=create_data)
144
+
145
+ if create_response.status_code in [200, 201]:
146
+ tunnel = create_response.json()['result']
147
+ print(f"✅ Created tunnel: {tunnel_name}")
166
148
 
167
- # Merge with existing ingress if any
168
- if current_config.get("success") and current_config.get("result"):
169
- existing_ingress = current_config["result"].get("config", {}).get("ingress", [])
170
-
171
- # Filter out our hostnames from existing
172
- filtered_ingress = [
173
- rule for rule in existing_ingress
174
- if rule.get("hostname") not in [
175
- f"{self.jupyter_subdomain}.{self.base_domain}",
176
- f"{self.ssh_subdomain}.{self.base_domain}"
177
- ] and rule.get("service") != "http_status:404"
178
- ]
179
-
180
- # Combine
181
- new_ingress = new_ingress + filtered_ingress
149
+ # Add the secret to the tunnel info (API doesn't return it)
150
+ tunnel['tunnel_secret'] = tunnel_secret
182
151
 
183
- # Add catch-all at the end
184
- new_ingress.append({"service": "http_status:404"})
152
+ # Save credentials for this tunnel
153
+ self._save_tunnel_credentials(tunnel)
185
154
 
186
- # Update configuration
187
- config_data = {
188
- "config": {
189
- "ingress": new_ingress
190
- }
191
- }
155
+ # Configure tunnel routes
156
+ self._configure_tunnel_routes(tunnel['id'])
192
157
 
193
- put_url = f"{self.api_base}/accounts/{self.account_id}/tunnels/{self.tunnel_id}/configurations"
194
- response = requests.put(put_url, headers=self.headers, json=config_data)
158
+ # Store tunnel ID for DNS creation
159
+ self.tunnel_id = tunnel['id']
195
160
 
196
- if response.status_code == 200:
197
- print(f" ✅ Tunnel routes configured")
198
- return True
199
- else:
200
- print(f" ⚠️ Route configuration status: {response.status_code}")
201
- # Continue anyway - routes might be configured manually
202
- return True
203
-
204
- except Exception as e:
205
- print(f" ⚠️ Could not update routes via API: {e}")
206
- print(" Assuming routes are configured in dashboard.")
207
- return True
208
-
209
- def create_device_tunnel(self, retry_on_fail=True):
210
- """
211
- Create a unique tunnel for this device if it doesn't exist
212
- """
213
- tunnel_name = f"device-{self.clean_device_id}"
214
- print(f"🔍 Checking for tunnel: {tunnel_name}")
215
-
216
- # Check if tunnel already exists
217
- list_url = f"{self.api_base}/accounts/{self.account_id}/tunnels"
218
- response = requests.get(list_url, headers=self.headers)
219
-
220
- if response.status_code == 200:
221
- tunnels = response.json().get('result', [])
222
- existing_tunnel = None
161
+ # Create DNS records for this device
162
+ self.create_dns_records()
223
163
 
224
- for tunnel in tunnels:
225
- if tunnel['name'] == tunnel_name:
226
- existing_tunnel = tunnel
227
- print(f"✅ Found existing tunnel: {tunnel_name}")
228
- break
229
-
230
- if not existing_tunnel:
231
- # Create new tunnel
232
- print(f"📦 Creating new tunnel: {tunnel_name}")
233
- create_url = f"{self.api_base}/accounts/{self.account_id}/tunnels"
234
- # Generate secret first
235
- tunnel_secret = os.urandom(32).hex()
236
-
237
- create_data = {
238
- "name": tunnel_name,
239
- "tunnel_secret": tunnel_secret
240
- }
241
-
242
- create_response = requests.post(create_url, headers=self.headers, json=create_data)
243
-
244
- if create_response.status_code in [200, 201]:
245
- existing_tunnel = create_response.json()['result']
246
- print(f"✅ Created tunnel: {tunnel_name}")
247
-
248
- # Add the secret to the tunnel info (API doesn't return it)
249
- existing_tunnel['tunnel_secret'] = tunnel_secret
250
-
251
- # Save credentials for this tunnel
252
- self._save_tunnel_credentials(existing_tunnel)
253
-
254
- # Configure tunnel routes
255
- self._configure_tunnel_routes(existing_tunnel['id'])
256
-
257
- # Create DNS records for this device
258
- self.create_dns_records()
259
- else:
260
- print(f"❌ Failed to create tunnel: {create_response.text}")
261
- return None
262
- else:
263
- # Tunnel exists - but we need to ensure it can be used
264
- print(f"♻️ Found existing tunnel, setting up for use")
265
-
266
- # Store tunnel info for later use
267
- existing_tunnel['needs_token'] = True # Mark that we'll need to use token
268
-
269
- # Configure tunnel routes (creates config file)
270
- self._configure_tunnel_routes(existing_tunnel['id'])
271
-
272
- # Ensure DNS records exist
273
- self.create_dns_records()
274
-
275
- return existing_tunnel
276
-
277
- return None
164
+ return tunnel
165
+ else:
166
+ print(f"❌ Failed to create tunnel: {create_response.text}")
167
+ return None
278
168
 
279
169
  def _configure_tunnel_routes(self, tunnel_id):
280
170
  """
281
171
  Configure ingress routes for the device tunnel
282
- The tunnel needs to be configured with a config file, not via API
283
- So we'll create a config file for it
172
+ Creates a config file for cloudflared
284
173
  """
285
174
  import yaml
286
175
 
@@ -346,14 +235,14 @@ class CloudflareAPITunnel:
346
235
  }
347
236
 
348
237
  with open(creds_file, 'w') as f:
349
- json.dump(credentials, f)
238
+ json.dump(credentials, f, indent=2)
350
239
 
351
- print(f"💾 Saved credentials to: {creds_file}")
240
+ print(f" Saved tunnel credentials: {creds_file}")
352
241
  return creds_file
353
242
 
354
243
  def start_tunnel_with_token(self):
355
244
  """
356
- Start tunnel using the existing service token
245
+ Start tunnel using API-created tunnel with UUID
357
246
  """
358
247
  try:
359
248
  print("🚀 Starting Cloudflare tunnel...")
@@ -363,139 +252,46 @@ class CloudflareAPITunnel:
363
252
  if not cloudflared_path:
364
253
  raise RuntimeError("Failed to obtain cloudflared binary")
365
254
 
366
- # Create or get existing tunnel for this device
255
+ # Create a new unique tunnel
367
256
  device_tunnel = self.create_device_tunnel()
368
257
 
369
- # Now set up DNS and routes via API after tunnel is created/found
370
- if self.api_token and device_tunnel:
371
- self.tunnel_id = device_tunnel['id'] # Ensure tunnel_id is set
372
- self.create_dns_records()
373
- self.update_tunnel_config()
258
+ if not device_tunnel:
259
+ print("❌ Could not create device tunnel")
260
+ return None
374
261
 
375
- # Check if we need to recreate the tunnel
376
- if device_tunnel and device_tunnel.get('needs_recreation'):
377
- print("🔄 Recreating tunnel due to access issues...")
378
- # Delete the old tunnel
379
- tunnel_id = device_tunnel['id']
380
- delete_url = f"{self.api_base}/accounts/{self.account_id}/tunnels/{tunnel_id}"
381
- delete_response = requests.delete(delete_url, headers=self.headers)
382
- if delete_response.status_code in [200, 204, 404]:
383
- print(f" ✅ Deleted old tunnel")
384
- # Try creating a new one with a timestamp suffix
385
- import time
386
- self.clean_device_id = f"{self.device_id.replace(' ', '').replace('-', '').replace('.', '').replace('_', '')[:24]}-{int(time.time())}"
387
- device_tunnel = self.create_device_tunnel(retry_on_fail=False)
388
- if device_tunnel:
389
- print(f" ✅ Created new tunnel")
262
+ tunnel_id = device_tunnel['id']
263
+ tunnel_name = device_tunnel['name']
390
264
 
391
- if not device_tunnel:
392
- print("❌ Could not create/find device tunnel")
393
- # Fallback to shared tunnel if API fails
394
- print("⚠️ Falling back to shared tunnel...")
395
- service_token = "eyJhIjoiYzkxMTkyYWUyMGE1ZDQzZjY1ZTA4NzU1MGQ4ZGM4OWIiLCJ0IjoiMDc3N2ZjMTAtNDljNC00NzJkLTg2NjEtZjYwZDgwZDYxODRkIiwicyI6Ik9XRTNaak5tTVdVdE1tWTRaUzAwTmpoakLTazBmalF0WXpjek1tSm1ZVGt4WlRRMCJ9"
265
+ print(f"🚇 Starting tunnel: {tunnel_name} ({tunnel_id})")
266
+
267
+ # Check credentials file exists
268
+ creds_file = Path.home() / '.cloudflared' / f"{tunnel_id}.json"
269
+ config_file = Path.home() / '.cloudflared' / f'config-{tunnel_id}.yml'
270
+
271
+ if not creds_file.exists():
272
+ print("❌ Error: No credentials file found for newly created tunnel")
273
+ return None
274
+
275
+ # Use config file if it exists, otherwise use credentials file
276
+ if config_file.exists():
396
277
  cmd = [
397
278
  cloudflared_path,
398
279
  "tunnel",
399
280
  "--no-autoupdate",
400
- "run",
401
- "--token",
402
- service_token
281
+ "--config", str(config_file),
282
+ "run"
403
283
  ]
404
284
  else:
405
- tunnel_id = device_tunnel['id']
406
- tunnel_name = device_tunnel['name']
407
-
408
- print(f"🚇 Starting tunnel: {tunnel_name} ({tunnel_id})")
409
-
410
- # Check if credentials file exists
411
- creds_file = Path.home() / '.cloudflared' / f"{tunnel_id}.json"
412
- config_file = Path.home() / '.cloudflared' / f'config-{tunnel_id}.yml'
413
- cmd = None # Initialize cmd
414
-
415
- if not creds_file.exists() or device_tunnel.get('needs_token'):
416
- # Try to recreate credentials from stored secret
417
- if device_tunnel.get('tunnel_secret'):
418
- self._save_tunnel_credentials(device_tunnel)
419
- # Continue to use credentials below
420
- else:
421
- print("⚠️ No stored credentials, requesting tunnel token...")
422
- # Get token for this tunnel
423
- token_url = f"{self.api_base}/accounts/{self.account_id}/tunnels/{tunnel_id}/token"
424
- print(f" Token URL: {token_url}")
425
-
426
- token_response = requests.get(token_url, headers=self.headers)
427
- print(f" Token response status: {token_response.status_code}")
428
-
429
- if token_response.status_code == 200:
430
- result = token_response.json()
431
- if 'result' in result:
432
- token = result['result']
433
- print(f" ✅ Got tunnel token")
434
- else:
435
- print(f" ❌ No token in response: {result}")
436
- return None
437
-
438
- # Check if config file exists with ingress rules
439
- if config_file.exists():
440
- # Use token with config file for ingress rules
441
- print(f" Using token with config file: {config_file}")
442
- cmd = [
443
- cloudflared_path,
444
- "tunnel",
445
- "--no-autoupdate",
446
- "--config", str(config_file),
447
- "run",
448
- "--token", token
449
- ]
450
- else:
451
- # Use token directly
452
- print(f" Using token directly (no config file)")
453
- cmd = [
454
- cloudflared_path,
455
- "tunnel",
456
- "--no-autoupdate",
457
- "run",
458
- "--token", token
459
- ]
460
- else:
461
- print(f"❌ Could not get tunnel token: {token_response.status_code}")
462
- if token_response.text:
463
- print(f" Error: {token_response.text}")
464
-
465
- # If 404, tunnel doesn't exist or we don't have access
466
- if token_response.status_code == 404:
467
- print(f"🔄 Tunnel not accessible (404), will try to recreate...")
468
- # Mark for recreation
469
- device_tunnel['needs_recreation'] = True
470
- return None
471
- else:
472
- print(f" ℹ️ Token endpoint returned {token_response.status_code}")
473
- print(f" This might mean the tunnel needs to be recreated manually")
474
- return None
475
-
476
- # Only use credentials if we haven't already set cmd with token
477
- if cmd is None and creds_file.exists():
478
- # Check if config file exists
479
- if config_file.exists():
480
- # Run tunnel with config file (includes routes)
481
- cmd = [
482
- cloudflared_path,
483
- "tunnel",
484
- "--no-autoupdate",
485
- "--config", str(config_file),
486
- "run"
487
- ]
488
- else:
489
- # Fallback to credentials file only
490
- cmd = [
491
- cloudflared_path,
492
- "tunnel",
493
- "--no-autoupdate",
494
- "--credentials-file", str(creds_file),
495
- "run",
496
- tunnel_id
497
- ]
285
+ cmd = [
286
+ cloudflared_path,
287
+ "tunnel",
288
+ "--no-autoupdate",
289
+ "--credentials-file", str(creds_file),
290
+ "run",
291
+ tunnel_id
292
+ ]
498
293
 
294
+ # Start tunnel process
499
295
  self.tunnel_process = subprocess.Popen(
500
296
  cmd,
501
297
  stdout=subprocess.PIPE,
@@ -522,17 +318,16 @@ class CloudflareAPITunnel:
522
318
  except Exception as e:
523
319
  print(f"❌ Error starting tunnel: {e}")
524
320
  return None
525
-
321
+
526
322
  def setup(self, jupyter_port=8888):
527
323
  """
528
324
  Setup and start tunnel (maintains compatibility)
529
325
  """
530
326
  return self.start_tunnel_with_token()
531
-
327
+
532
328
  def stop(self):
533
329
  """
534
330
  Stop the tunnel if running
535
- Note: We keep the tunnel configuration for next run
536
331
  """
537
332
  if self.tunnel_process and self.tunnel_process.poll() is None:
538
333
  print("Stopping tunnel...")
@@ -542,12 +337,10 @@ class CloudflareAPITunnel:
542
337
  except subprocess.TimeoutExpired:
543
338
  self.tunnel_process.kill()
544
339
  print("Tunnel stopped")
545
- print("ℹ️ Tunnel configuration preserved for next run")
546
-
340
+
547
341
  def _ensure_cloudflared(self):
548
342
  """
549
343
  Ensure cloudflared binary is available
550
- Downloads it if necessary
551
344
  """
552
345
  print("🔍 Checking for cloudflared binary...")
553
346
 
@@ -558,95 +351,17 @@ class CloudflareAPITunnel:
558
351
  print(f"✅ Using cloudflared from binary manager: {path}")
559
352
  return path
560
353
  except Exception as e:
561
- logger.warning(f"Binary manager failed, will download directly: {e}")
562
-
563
- # Direct download fallback - simplified version
564
- import platform
565
- import urllib.request
566
- import ssl
567
-
568
- # Create SSL context that handles certificate issues
569
- ssl_context = ssl.create_default_context()
570
- ssl_context.check_hostname = False
571
- ssl_context.verify_mode = ssl.CERT_NONE
572
-
573
- cache_dir = Path.home() / '.unitlab' / 'bin'
574
- cache_dir.mkdir(parents=True, exist_ok=True)
575
-
576
- cloudflared_path = cache_dir / 'cloudflared'
577
- if platform.system() == 'Windows':
578
- cloudflared_path = cache_dir / 'cloudflared.exe'
579
-
580
- # If already exists, use it
581
- if cloudflared_path.exists():
582
- print(f"✅ Using cached cloudflared: {cloudflared_path}")
583
- return str(cloudflared_path)
584
-
585
- # Download based on platform
586
- system = platform.system().lower()
587
- machine = platform.machine().lower()
588
-
589
- print(f"📥 Downloading cloudflared for {system}/{machine}...")
590
-
591
- if system == 'linux':
592
- if machine in ['x86_64', 'amd64']:
593
- url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64'
594
- elif machine in ['aarch64', 'arm64']:
595
- url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64'
596
- else:
597
- url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-386'
598
- elif system == 'darwin':
599
- url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz'
600
- elif system == 'windows':
601
- url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe'
602
- else:
603
- raise RuntimeError(f"Unsupported platform: {system}")
354
+ print(f"⚠️ Binary manager failed: {e}")
604
355
 
356
+ # Fallback to system cloudflared
605
357
  try:
606
- # Download the file with SSL context
607
- req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
608
-
609
- # Special handling for macOS .tgz files
610
- if system == 'darwin':
611
- import tarfile
612
- import io
613
-
614
- with urllib.request.urlopen(req, context=ssl_context) as response:
615
- data = response.read()
616
-
617
- # Extract from tar.gz
618
- with tarfile.open(fileobj=io.BytesIO(data), mode='r:gz') as tar:
619
- tar.extract('cloudflared', cache_dir)
620
- else:
621
- # Direct binary download for Linux/Windows
622
- with urllib.request.urlopen(req, context=ssl_context) as response:
623
- with open(cloudflared_path, 'wb') as out_file:
624
- out_file.write(response.read())
625
-
626
- # Make executable on Unix
627
- if system != 'windows':
628
- import stat
629
- cloudflared_path.chmod(cloudflared_path.stat().st_mode | stat.S_IEXEC)
630
-
631
- print(f"✅ Downloaded cloudflared to: {cloudflared_path}")
632
- return str(cloudflared_path)
633
-
634
- except Exception as e:
635
- print(f"❌ Failed to download cloudflared: {e}")
636
- raise RuntimeError(f"Could not download cloudflared: {e}")
637
-
638
- def cleanup_dns(self):
639
- """
640
- Remove created DNS records (optional cleanup)
641
- """
642
- if not self.api_token or not self.created_dns_records:
643
- return
358
+ result = subprocess.run(['which', 'cloudflared'], capture_output=True, text=True)
359
+ if result.returncode == 0:
360
+ path = result.stdout.strip()
361
+ print(f"✅ Found system cloudflared: {path}")
362
+ return path
363
+ except:
364
+ pass
644
365
 
645
- print("🧹 Cleaning up DNS records...")
646
- for record_id in self.created_dns_records:
647
- try:
648
- url = f"{self.api_base}/zones/{self.zone_id}/dns_records/{record_id}"
649
- requests.delete(url, headers=self.headers)
650
- print(f" Deleted record {record_id}")
651
- except:
652
- pass
366
+ print(" cloudflared not found")
367
+ return None