unitlab 2.3.20__tar.gz → 2.3.23__tar.gz
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-2.3.20/src/unitlab.egg-info → unitlab-2.3.23}/PKG-INFO +1 -1
- {unitlab-2.3.20 → unitlab-2.3.23}/setup.py +1 -1
- unitlab-2.3.23/src/unitlab/cloudflare_api_tunnel.py +367 -0
- unitlab-2.3.20/src/unitlab/cloudflare_api_tunnel.py → unitlab-2.3.23/src/unitlab/cloudflare_api_tunnel_backup.py +95 -104
- {unitlab-2.3.20 → unitlab-2.3.23/src/unitlab.egg-info}/PKG-INFO +1 -1
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab.egg-info/SOURCES.txt +1 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/LICENSE.md +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/README.md +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/setup.cfg +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab/__init__.py +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab/__main__.py +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab/binary_manager.py +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab/client.py +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab/exceptions.py +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab/main.py +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab/tunnel_config.py +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab/tunnel_service_token.py +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab/utils.py +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab.egg-info/dependency_links.txt +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab.egg-info/entry_points.txt +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab.egg-info/requires.txt +0 -0
- {unitlab-2.3.20 → unitlab-2.3.23}/src/unitlab.egg-info/top_level.txt +0 -0
@@ -0,0 +1,367 @@
|
|
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 = "xkfCGvxHeiU7xvz0OhfYUICZvJ7rM4NmgdMSB5jy"
|
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
|
+
print(f"📦 Creating new tunnel: {tunnel_name}")
|
132
|
+
|
133
|
+
create_url = f"{self.api_base}/accounts/{self.account_id}/tunnels"
|
134
|
+
|
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}")
|
148
|
+
|
149
|
+
# Add the secret to the tunnel info (API doesn't return it)
|
150
|
+
tunnel['tunnel_secret'] = tunnel_secret
|
151
|
+
|
152
|
+
# Save credentials for this tunnel
|
153
|
+
self._save_tunnel_credentials(tunnel)
|
154
|
+
|
155
|
+
# Configure tunnel routes
|
156
|
+
self._configure_tunnel_routes(tunnel['id'])
|
157
|
+
|
158
|
+
# Store tunnel ID for DNS creation
|
159
|
+
self.tunnel_id = tunnel['id']
|
160
|
+
|
161
|
+
# Create DNS records for this device
|
162
|
+
self.create_dns_records()
|
163
|
+
|
164
|
+
return tunnel
|
165
|
+
else:
|
166
|
+
print(f"❌ Failed to create tunnel: {create_response.text}")
|
167
|
+
return None
|
168
|
+
|
169
|
+
def _configure_tunnel_routes(self, tunnel_id):
|
170
|
+
"""
|
171
|
+
Configure ingress routes for the device tunnel
|
172
|
+
Creates a config file for cloudflared
|
173
|
+
"""
|
174
|
+
import yaml
|
175
|
+
|
176
|
+
# Create config file for this tunnel
|
177
|
+
config_dir = Path.home() / '.cloudflared'
|
178
|
+
config_dir.mkdir(exist_ok=True)
|
179
|
+
config_file = config_dir / f'config-{tunnel_id}.yml'
|
180
|
+
|
181
|
+
config = {
|
182
|
+
"tunnel": tunnel_id,
|
183
|
+
"credentials-file": str(config_dir / f"{tunnel_id}.json"),
|
184
|
+
"ingress": [
|
185
|
+
{
|
186
|
+
"hostname": f"{self.jupyter_subdomain}.{self.base_domain}",
|
187
|
+
"service": "http://localhost:8888",
|
188
|
+
"originRequest": {
|
189
|
+
"noTLSVerify": True
|
190
|
+
}
|
191
|
+
},
|
192
|
+
{
|
193
|
+
"hostname": f"{self.ssh_subdomain}.{self.base_domain}",
|
194
|
+
"service": "ssh://localhost:22"
|
195
|
+
},
|
196
|
+
{
|
197
|
+
"service": "http_status:404"
|
198
|
+
}
|
199
|
+
]
|
200
|
+
}
|
201
|
+
|
202
|
+
with open(config_file, 'w') as f:
|
203
|
+
yaml.dump(config, f)
|
204
|
+
|
205
|
+
print(f"✅ Created tunnel config: {config_file}")
|
206
|
+
self.tunnel_config_file = config_file
|
207
|
+
|
208
|
+
def _save_tunnel_credentials(self, tunnel_info):
|
209
|
+
"""
|
210
|
+
Save tunnel credentials locally for this device
|
211
|
+
Credentials must be base64 encoded for cloudflared
|
212
|
+
"""
|
213
|
+
import base64
|
214
|
+
import json
|
215
|
+
|
216
|
+
creds_dir = Path.home() / '.cloudflared'
|
217
|
+
creds_dir.mkdir(exist_ok=True)
|
218
|
+
|
219
|
+
creds_file = creds_dir / f"{tunnel_info['id']}.json"
|
220
|
+
|
221
|
+
# Get the secret - it should be hex string
|
222
|
+
secret_hex = tunnel_info.get('tunnel_secret') or tunnel_info.get('secret')
|
223
|
+
if secret_hex:
|
224
|
+
# Convert hex to bytes then to base64
|
225
|
+
secret_bytes = bytes.fromhex(secret_hex)
|
226
|
+
secret_b64 = base64.b64encode(secret_bytes).decode('ascii')
|
227
|
+
else:
|
228
|
+
print("⚠️ No tunnel secret found")
|
229
|
+
return None
|
230
|
+
|
231
|
+
credentials = {
|
232
|
+
"AccountTag": self.account_id,
|
233
|
+
"TunnelSecret": secret_b64, # Must be base64!
|
234
|
+
"TunnelID": tunnel_info['id']
|
235
|
+
}
|
236
|
+
|
237
|
+
with open(creds_file, 'w') as f:
|
238
|
+
json.dump(credentials, f, indent=2)
|
239
|
+
|
240
|
+
print(f"✅ Saved tunnel credentials: {creds_file}")
|
241
|
+
return creds_file
|
242
|
+
|
243
|
+
def start_tunnel_with_token(self):
|
244
|
+
"""
|
245
|
+
Start tunnel using API-created tunnel with UUID
|
246
|
+
"""
|
247
|
+
try:
|
248
|
+
print("🚀 Starting Cloudflare tunnel...")
|
249
|
+
|
250
|
+
# Ensure cloudflared is available
|
251
|
+
cloudflared_path = self._ensure_cloudflared()
|
252
|
+
if not cloudflared_path:
|
253
|
+
raise RuntimeError("Failed to obtain cloudflared binary")
|
254
|
+
|
255
|
+
# Create a new unique tunnel
|
256
|
+
device_tunnel = self.create_device_tunnel()
|
257
|
+
|
258
|
+
if not device_tunnel:
|
259
|
+
print("❌ Could not create device tunnel")
|
260
|
+
return None
|
261
|
+
|
262
|
+
tunnel_id = device_tunnel['id']
|
263
|
+
tunnel_name = device_tunnel['name']
|
264
|
+
|
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():
|
277
|
+
cmd = [
|
278
|
+
cloudflared_path,
|
279
|
+
"tunnel",
|
280
|
+
"--no-autoupdate",
|
281
|
+
"--config", str(config_file),
|
282
|
+
"run"
|
283
|
+
]
|
284
|
+
else:
|
285
|
+
cmd = [
|
286
|
+
cloudflared_path,
|
287
|
+
"tunnel",
|
288
|
+
"--no-autoupdate",
|
289
|
+
"--credentials-file", str(creds_file),
|
290
|
+
"run",
|
291
|
+
tunnel_id
|
292
|
+
]
|
293
|
+
|
294
|
+
# Start tunnel process
|
295
|
+
self.tunnel_process = subprocess.Popen(
|
296
|
+
cmd,
|
297
|
+
stdout=subprocess.PIPE,
|
298
|
+
stderr=subprocess.STDOUT,
|
299
|
+
text=True,
|
300
|
+
bufsize=1
|
301
|
+
)
|
302
|
+
|
303
|
+
print("⏳ Waiting for tunnel to connect...")
|
304
|
+
time.sleep(5)
|
305
|
+
|
306
|
+
if self.tunnel_process.poll() is None:
|
307
|
+
print("✅ Tunnel is running!")
|
308
|
+
print(f"📌 Device ID: {self.clean_device_id}")
|
309
|
+
print(f"📌 Jupyter URL: {self.jupyter_url}")
|
310
|
+
print(f"📌 SSH hostname: {self.ssh_hostname}")
|
311
|
+
print(f"📌 SSH command: ssh -o ProxyCommand='cloudflared access ssh --hostname {self.ssh_hostname}' user@localhost")
|
312
|
+
return self.tunnel_process
|
313
|
+
else:
|
314
|
+
output = self.tunnel_process.stdout.read() if self.tunnel_process.stdout else ""
|
315
|
+
print(f"❌ Tunnel failed to start: {output}")
|
316
|
+
return None
|
317
|
+
|
318
|
+
except Exception as e:
|
319
|
+
print(f"❌ Error starting tunnel: {e}")
|
320
|
+
return None
|
321
|
+
|
322
|
+
def setup(self, jupyter_port=8888):
|
323
|
+
"""
|
324
|
+
Setup and start tunnel (maintains compatibility)
|
325
|
+
"""
|
326
|
+
return self.start_tunnel_with_token()
|
327
|
+
|
328
|
+
def stop(self):
|
329
|
+
"""
|
330
|
+
Stop the tunnel if running
|
331
|
+
"""
|
332
|
+
if self.tunnel_process and self.tunnel_process.poll() is None:
|
333
|
+
print("Stopping tunnel...")
|
334
|
+
self.tunnel_process.terminate()
|
335
|
+
try:
|
336
|
+
self.tunnel_process.wait(timeout=5)
|
337
|
+
except subprocess.TimeoutExpired:
|
338
|
+
self.tunnel_process.kill()
|
339
|
+
print("Tunnel stopped")
|
340
|
+
|
341
|
+
def _ensure_cloudflared(self):
|
342
|
+
"""
|
343
|
+
Ensure cloudflared binary is available
|
344
|
+
"""
|
345
|
+
print("🔍 Checking for cloudflared binary...")
|
346
|
+
|
347
|
+
# Try binary manager first
|
348
|
+
if self.binary_manager:
|
349
|
+
try:
|
350
|
+
path = self.binary_manager.get_binary_path()
|
351
|
+
print(f"✅ Using cloudflared from binary manager: {path}")
|
352
|
+
return path
|
353
|
+
except Exception as e:
|
354
|
+
print(f"⚠️ Binary manager failed: {e}")
|
355
|
+
|
356
|
+
# Fallback to system cloudflared
|
357
|
+
try:
|
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
|
365
|
+
|
366
|
+
print("❌ cloudflared not found")
|
367
|
+
return None
|
@@ -206,75 +206,47 @@ class CloudflareAPITunnel:
|
|
206
206
|
print(" Assuming routes are configured in dashboard.")
|
207
207
|
return True
|
208
208
|
|
209
|
-
def create_device_tunnel(self
|
209
|
+
def create_device_tunnel(self):
|
210
210
|
"""
|
211
|
-
Create a unique tunnel for this device
|
211
|
+
Create a unique tunnel for this device
|
212
|
+
Each tunnel gets a unique UUID to avoid conflicts
|
212
213
|
"""
|
213
|
-
|
214
|
-
|
214
|
+
import uuid
|
215
|
+
# Always use a unique name with UUID
|
216
|
+
unique_id = str(uuid.uuid4())[:8]
|
217
|
+
tunnel_name = f"device-{self.clean_device_id}-{unique_id}"
|
218
|
+
# Since we use UUID, no need to check - just create new tunnel
|
219
|
+
create_url = f"{self.api_base}/accounts/{self.account_id}/tunnels"
|
220
|
+
# Generate secret first
|
221
|
+
tunnel_secret = os.urandom(32).hex()
|
222
|
+
|
223
|
+
create_data = {
|
224
|
+
"name": tunnel_name,
|
225
|
+
"tunnel_secret": tunnel_secret
|
226
|
+
}
|
215
227
|
|
216
|
-
|
217
|
-
list_url = f"{self.api_base}/accounts/{self.account_id}/tunnels"
|
218
|
-
response = requests.get(list_url, headers=self.headers)
|
228
|
+
create_response = requests.post(create_url, headers=self.headers, json=create_data)
|
219
229
|
|
220
|
-
if
|
221
|
-
|
222
|
-
|
230
|
+
if create_response.status_code in [200, 201]:
|
231
|
+
tunnel = create_response.json()['result']
|
232
|
+
print(f"✅ Created tunnel: {tunnel_name}")
|
223
233
|
|
224
|
-
|
225
|
-
|
226
|
-
existing_tunnel = tunnel
|
227
|
-
print(f"✅ Found existing tunnel: {tunnel_name}")
|
228
|
-
break
|
234
|
+
# Add the secret to the tunnel info (API doesn't return it)
|
235
|
+
tunnel['tunnel_secret'] = tunnel_secret
|
229
236
|
|
230
|
-
|
231
|
-
|
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()
|
237
|
+
# Save credentials for this tunnel
|
238
|
+
self._save_tunnel_credentials(tunnel)
|
274
239
|
|
275
|
-
|
276
|
-
|
277
|
-
|
240
|
+
# Configure tunnel routes
|
241
|
+
self._configure_tunnel_routes(tunnel['id'])
|
242
|
+
|
243
|
+
# Create DNS records for this device
|
244
|
+
self.create_dns_records()
|
245
|
+
|
246
|
+
return tunnel
|
247
|
+
else:
|
248
|
+
print(f"❌ Failed to create tunnel: {create_response.text}")
|
249
|
+
return None
|
278
250
|
|
279
251
|
def _configure_tunnel_routes(self, tunnel_id):
|
280
252
|
"""
|
@@ -363,7 +335,7 @@ class CloudflareAPITunnel:
|
|
363
335
|
if not cloudflared_path:
|
364
336
|
raise RuntimeError("Failed to obtain cloudflared binary")
|
365
337
|
|
366
|
-
#
|
338
|
+
# Always create a new unique tunnel with UUID
|
367
339
|
device_tunnel = self.create_device_tunnel()
|
368
340
|
|
369
341
|
# Now set up DNS and routes via API after tunnel is created/found
|
@@ -372,21 +344,6 @@ class CloudflareAPITunnel:
|
|
372
344
|
self.create_dns_records()
|
373
345
|
self.update_tunnel_config()
|
374
346
|
|
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
|
-
self.clean_device_id = f"{self.device_id.replace(' ', '').replace('-', '').replace('.', '').replace('_', '')[:24]}-{int(time.time())}"
|
386
|
-
device_tunnel = self.create_device_tunnel(retry_on_fail=False)
|
387
|
-
if device_tunnel:
|
388
|
-
print(f" ✅ Created new tunnel")
|
389
|
-
|
390
347
|
if not device_tunnel:
|
391
348
|
print("❌ Could not create/find device tunnel")
|
392
349
|
# Fallback to shared tunnel if API fails
|
@@ -411,21 +368,12 @@ class CloudflareAPITunnel:
|
|
411
368
|
config_file = Path.home() / '.cloudflared' / f'config-{tunnel_id}.yml'
|
412
369
|
cmd = None # Initialize cmd
|
413
370
|
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
print("⚠️ No stored credentials, requesting tunnel token...")
|
421
|
-
# Get token for this tunnel
|
422
|
-
token_url = f"{self.api_base}/accounts/{self.account_id}/tunnels/{tunnel_id}/token"
|
423
|
-
print(f" Token URL: {token_url}")
|
424
|
-
|
425
|
-
token_response = requests.get(token_url, headers=self.headers)
|
426
|
-
print(f" Token response status: {token_response.status_code}")
|
427
|
-
|
428
|
-
if token_response.status_code == 200:
|
371
|
+
# Since we always create new tunnels, credentials should exist
|
372
|
+
if not creds_file.exists():
|
373
|
+
print("❌ Error: No credentials file found for newly created tunnel")
|
374
|
+
return None
|
375
|
+
|
376
|
+
# Use credentials to run tunnel
|
429
377
|
result = token_response.json()
|
430
378
|
if 'result' in result:
|
431
379
|
token = result['result']
|
@@ -463,11 +411,58 @@ class CloudflareAPITunnel:
|
|
463
411
|
|
464
412
|
# If 404, tunnel doesn't exist or we don't have access
|
465
413
|
if token_response.status_code == 404:
|
466
|
-
print(f"🔄 Tunnel not accessible (404),
|
467
|
-
#
|
468
|
-
|
469
|
-
|
470
|
-
|
414
|
+
print(f"🔄 Tunnel not accessible (404), need to recreate...")
|
415
|
+
# Delete the old tunnel immediately
|
416
|
+
delete_url = f"{self.api_base}/accounts/{self.account_id}/tunnels/{tunnel_id}"
|
417
|
+
delete_response = requests.delete(delete_url, headers=self.headers)
|
418
|
+
if delete_response.status_code in [200, 204, 404]:
|
419
|
+
print(f" ✅ Deleted old tunnel")
|
420
|
+
|
421
|
+
# Create a new tunnel with timestamp
|
422
|
+
import uuid
|
423
|
+
# Use a random suffix to ensure uniqueness
|
424
|
+
random_suffix = str(uuid.uuid4())[:8]
|
425
|
+
self.clean_device_id = f"{self.device_id.replace(' ', '').replace('-', '').replace('.', '').replace('_', '')[:16]}-{random_suffix}"
|
426
|
+
print(f" Creating new tunnel with ID: {self.clean_device_id}")
|
427
|
+
|
428
|
+
new_tunnel = self.create_device_tunnel(force_new=False)
|
429
|
+
|
430
|
+
if new_tunnel and new_tunnel.get('tunnel_secret'):
|
431
|
+
print(f" ✅ Created new tunnel: {new_tunnel['name']}")
|
432
|
+
# Use the new tunnel directly - update references
|
433
|
+
device_tunnel = new_tunnel
|
434
|
+
tunnel_id = new_tunnel['id']
|
435
|
+
tunnel_name = new_tunnel['name']
|
436
|
+
|
437
|
+
# Check if credentials were saved
|
438
|
+
creds_file = Path.home() / '.cloudflared' / f"{tunnel_id}.json"
|
439
|
+
if creds_file.exists():
|
440
|
+
print(f" ✅ Credentials saved for new tunnel")
|
441
|
+
# Set up command to run with credentials
|
442
|
+
config_file = Path.home() / '.cloudflared' / f'config-{tunnel_id}.yml'
|
443
|
+
if config_file.exists():
|
444
|
+
cmd = [
|
445
|
+
cloudflared_path,
|
446
|
+
"tunnel",
|
447
|
+
"--no-autoupdate",
|
448
|
+
"--config", str(config_file),
|
449
|
+
"run"
|
450
|
+
]
|
451
|
+
else:
|
452
|
+
cmd = [
|
453
|
+
cloudflared_path,
|
454
|
+
"tunnel",
|
455
|
+
"--no-autoupdate",
|
456
|
+
"--credentials-file", str(creds_file),
|
457
|
+
"run",
|
458
|
+
tunnel_id
|
459
|
+
]
|
460
|
+
else:
|
461
|
+
print(f" ❌ No credentials for new tunnel, cannot proceed")
|
462
|
+
return None
|
463
|
+
else:
|
464
|
+
print(f" ❌ Failed to create replacement tunnel")
|
465
|
+
return None
|
471
466
|
else:
|
472
467
|
print(f" ℹ️ Token endpoint returned {token_response.status_code}")
|
473
468
|
print(f" This might mean the tunnel needs to be recreated manually")
|
@@ -521,12 +516,8 @@ class CloudflareAPITunnel:
|
|
521
516
|
print(f"❌ Tunnel failed to start: {output}")
|
522
517
|
return None
|
523
518
|
else:
|
524
|
-
# If no cmd,
|
525
|
-
|
526
|
-
# The recreation happens above before this point
|
527
|
-
print("❌ Failed to start tunnel after recreation attempt")
|
528
|
-
else:
|
529
|
-
print("❌ No valid command to start tunnel")
|
519
|
+
# If no cmd, something went wrong
|
520
|
+
print("❌ No valid command to start tunnel")
|
530
521
|
return None
|
531
522
|
|
532
523
|
except Exception as e:
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|