unitlab 2.3.28__py3-none-any.whl → 2.3.29__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/api_tunnel.py +238 -0
- unitlab/auto_tunnel.py +174 -0
- unitlab/client.py +31 -9
- unitlab/dynamic_tunnel.py +272 -0
- unitlab/easy_tunnel.py +210 -0
- unitlab/persistent_tunnel.py +348 -0
- {unitlab-2.3.28.dist-info → unitlab-2.3.29.dist-info}/METADATA +1 -1
- {unitlab-2.3.28.dist-info → unitlab-2.3.29.dist-info}/RECORD +12 -7
- {unitlab-2.3.28.dist-info → unitlab-2.3.29.dist-info}/LICENSE.md +0 -0
- {unitlab-2.3.28.dist-info → unitlab-2.3.29.dist-info}/WHEEL +0 -0
- {unitlab-2.3.28.dist-info → unitlab-2.3.29.dist-info}/entry_points.txt +0 -0
- {unitlab-2.3.28.dist-info → unitlab-2.3.29.dist-info}/top_level.txt +0 -0
unitlab/api_tunnel.py
ADDED
@@ -0,0 +1,238 @@
|
|
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()
|
unitlab/auto_tunnel.py
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Automatic Tunnel Creation - Simplest approach using cloudflared's built-in quick tunnel
|
4
|
+
No API tokens needed!
|
5
|
+
"""
|
6
|
+
|
7
|
+
import subprocess
|
8
|
+
import time
|
9
|
+
import re
|
10
|
+
import os
|
11
|
+
|
12
|
+
class AutoTunnel:
|
13
|
+
def __init__(self, device_id=None):
|
14
|
+
"""
|
15
|
+
Initialize auto tunnel - no credentials needed!
|
16
|
+
"""
|
17
|
+
self.device_id = device_id or "device"
|
18
|
+
self.jupyter_process = None
|
19
|
+
self.tunnel_process = None
|
20
|
+
self.tunnel_url = None
|
21
|
+
|
22
|
+
def get_cloudflared_path(self):
|
23
|
+
"""Get or download cloudflared binary"""
|
24
|
+
import platform
|
25
|
+
|
26
|
+
# Check if exists in system
|
27
|
+
import shutil
|
28
|
+
if shutil.which("cloudflared"):
|
29
|
+
return "cloudflared"
|
30
|
+
|
31
|
+
# Check local
|
32
|
+
local_bin = os.path.expanduser("~/.local/bin/cloudflared")
|
33
|
+
if os.path.exists(local_bin):
|
34
|
+
return local_bin
|
35
|
+
|
36
|
+
# Download it
|
37
|
+
print("📦 Downloading cloudflared...")
|
38
|
+
system = platform.system().lower()
|
39
|
+
if system == "linux":
|
40
|
+
arch = "amd64" if "x86" in platform.machine() else "arm64"
|
41
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
|
42
|
+
|
43
|
+
os.makedirs(os.path.expanduser("~/.local/bin"), exist_ok=True)
|
44
|
+
subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
|
45
|
+
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
46
|
+
print("✅ cloudflared downloaded")
|
47
|
+
return local_bin
|
48
|
+
|
49
|
+
return "cloudflared"
|
50
|
+
|
51
|
+
def start_jupyter(self):
|
52
|
+
"""Start Jupyter notebook"""
|
53
|
+
print("🚀 Starting Jupyter on port 8888...")
|
54
|
+
|
55
|
+
cmd = [
|
56
|
+
"jupyter", "notebook",
|
57
|
+
"--port", "8888",
|
58
|
+
"--no-browser",
|
59
|
+
"--ip", "0.0.0.0",
|
60
|
+
"--NotebookApp.token=''",
|
61
|
+
"--NotebookApp.password=''",
|
62
|
+
"--NotebookApp.allow_origin='*'"
|
63
|
+
]
|
64
|
+
|
65
|
+
self.jupyter_process = subprocess.Popen(
|
66
|
+
cmd,
|
67
|
+
stdout=subprocess.PIPE,
|
68
|
+
stderr=subprocess.PIPE
|
69
|
+
)
|
70
|
+
|
71
|
+
time.sleep(3)
|
72
|
+
print("✅ Jupyter started")
|
73
|
+
return True
|
74
|
+
|
75
|
+
def start_tunnel(self):
|
76
|
+
"""Start tunnel using cloudflared quick tunnel - no auth needed!"""
|
77
|
+
print("🔧 Starting automatic tunnel (no credentials needed)...")
|
78
|
+
|
79
|
+
cloudflared = self.get_cloudflared_path()
|
80
|
+
|
81
|
+
# Use cloudflared's quick tunnel feature - generates random URL
|
82
|
+
cmd = [
|
83
|
+
cloudflared,
|
84
|
+
"tunnel",
|
85
|
+
"--url", "http://localhost:8888"
|
86
|
+
]
|
87
|
+
|
88
|
+
self.tunnel_process = subprocess.Popen(
|
89
|
+
cmd,
|
90
|
+
stdout=subprocess.PIPE,
|
91
|
+
stderr=subprocess.STDOUT,
|
92
|
+
text=True,
|
93
|
+
bufsize=1
|
94
|
+
)
|
95
|
+
|
96
|
+
# Read output to get the tunnel URL
|
97
|
+
print("⏳ Waiting for tunnel URL...")
|
98
|
+
for _ in range(30): # Wait up to 30 seconds
|
99
|
+
line = self.tunnel_process.stdout.readline()
|
100
|
+
if line:
|
101
|
+
# Look for the tunnel URL in output
|
102
|
+
match = re.search(r'https://[a-zA-Z0-9-]+\.trycloudflare\.com', line)
|
103
|
+
if match:
|
104
|
+
self.tunnel_url = match.group(0)
|
105
|
+
print("✅ Tunnel created: {}".format(self.tunnel_url))
|
106
|
+
return True
|
107
|
+
time.sleep(1)
|
108
|
+
|
109
|
+
print("❌ Failed to get tunnel URL")
|
110
|
+
return False
|
111
|
+
|
112
|
+
def start(self):
|
113
|
+
"""Start everything - super simple!"""
|
114
|
+
try:
|
115
|
+
print("="*50)
|
116
|
+
print("🌐 Automatic Cloudflare Tunnel (No Auth Needed!)")
|
117
|
+
print("Device: {}".format(self.device_id))
|
118
|
+
print("="*50)
|
119
|
+
|
120
|
+
# 1. Start Jupyter
|
121
|
+
if not self.start_jupyter():
|
122
|
+
raise Exception("Failed to start Jupyter")
|
123
|
+
|
124
|
+
# 2. Start tunnel (automatic, no credentials)
|
125
|
+
if not self.start_tunnel():
|
126
|
+
raise Exception("Failed to start tunnel")
|
127
|
+
|
128
|
+
print("\n" + "="*50)
|
129
|
+
print("🎉 SUCCESS! Your Jupyter is accessible at:")
|
130
|
+
print(" {}".format(self.tunnel_url))
|
131
|
+
print("="*50)
|
132
|
+
print("\n⚠️ Note: This URL is temporary and random")
|
133
|
+
print("For persistent URLs, use Cloudflare API approach")
|
134
|
+
|
135
|
+
return True
|
136
|
+
|
137
|
+
except Exception as e:
|
138
|
+
print("❌ Error: {}".format(e))
|
139
|
+
self.stop()
|
140
|
+
return False
|
141
|
+
|
142
|
+
def stop(self):
|
143
|
+
"""Stop everything"""
|
144
|
+
if self.jupyter_process:
|
145
|
+
self.jupyter_process.terminate()
|
146
|
+
if self.tunnel_process:
|
147
|
+
self.tunnel_process.terminate()
|
148
|
+
|
149
|
+
def run(self):
|
150
|
+
"""Run and keep alive"""
|
151
|
+
try:
|
152
|
+
if self.start():
|
153
|
+
print("\nPress Ctrl+C to stop...")
|
154
|
+
while True:
|
155
|
+
time.sleep(1)
|
156
|
+
except KeyboardInterrupt:
|
157
|
+
print("\n⏹️ Shutting down...")
|
158
|
+
self.stop()
|
159
|
+
print("👋 Goodbye!")
|
160
|
+
|
161
|
+
|
162
|
+
def main():
|
163
|
+
"""Test automatic tunnel"""
|
164
|
+
import platform
|
165
|
+
device_id = platform.node()
|
166
|
+
|
167
|
+
print("Starting auto tunnel for: {}".format(device_id))
|
168
|
+
|
169
|
+
tunnel = AutoTunnel(device_id=device_id)
|
170
|
+
tunnel.run()
|
171
|
+
|
172
|
+
|
173
|
+
if __name__ == "__main__":
|
174
|
+
main()
|
unitlab/client.py
CHANGED
@@ -281,12 +281,26 @@ class UnitlabClient:
|
|
281
281
|
self.device_id = device_id
|
282
282
|
self.base_domain = base_domain
|
283
283
|
|
284
|
-
# Use
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
284
|
+
# Use persistent tunnel with API (each device gets deviceid.1scan.uz)
|
285
|
+
try:
|
286
|
+
from .persistent_tunnel import PersistentTunnel
|
287
|
+
logger.info("Using Persistent Tunnel with Cloudflare API")
|
288
|
+
self.tunnel_manager = PersistentTunnel(device_id=self.device_id)
|
289
|
+
# URLs will be set after tunnel starts
|
290
|
+
self.jupyter_url = self.tunnel_manager.jupyter_url
|
291
|
+
self.ssh_url = self.jupyter_url
|
292
|
+
except ImportError as e:
|
293
|
+
logger.warning(f"Could not import PersistentTunnel: {e}")
|
294
|
+
# Fallback to easy tunnel
|
295
|
+
try:
|
296
|
+
from .easy_tunnel import EasyTunnelAdapter
|
297
|
+
logger.info("Using Easy Dynamic Tunnel (random URL)")
|
298
|
+
self.tunnel_manager = EasyTunnelAdapter(device_id=self.device_id)
|
299
|
+
self.jupyter_url = None
|
300
|
+
self.ssh_url = None
|
301
|
+
except ImportError:
|
302
|
+
logger.error("No tunnel implementation available!")
|
303
|
+
raise
|
290
304
|
|
291
305
|
# Setup signal handlers
|
292
306
|
signal.signal(signal.SIGINT, self._handle_shutdown)
|
@@ -392,16 +406,24 @@ class UnitlabClient:
|
|
392
406
|
|
393
407
|
logger.info("Setting up Cloudflare tunnel...")
|
394
408
|
|
395
|
-
# SimpleTunnel
|
396
|
-
# It doesn't need the jupyter_port parameter since it starts its own Jupyter
|
409
|
+
# Both SimpleTunnel and AutoTunnel handle Jupyter internally
|
397
410
|
if self.tunnel_manager.start():
|
398
411
|
# Store the processes for monitoring
|
399
412
|
self.jupyter_proc = self.tunnel_manager.jupyter_process
|
400
413
|
self.tunnel_proc = self.tunnel_manager.tunnel_process
|
401
|
-
self.jupyter_port = "8888" #
|
414
|
+
self.jupyter_port = "8888" # Both use fixed port
|
415
|
+
|
416
|
+
# Get the URL (AutoTunnel generates it dynamically)
|
417
|
+
if hasattr(self.tunnel_manager, 'tunnel_url') and self.tunnel_manager.tunnel_url:
|
418
|
+
self.jupyter_url = self.tunnel_manager.tunnel_url
|
419
|
+
self.ssh_url = self.tunnel_manager.tunnel_url
|
420
|
+
elif hasattr(self.tunnel_manager, 'jupyter_url'):
|
421
|
+
self.jupyter_url = self.tunnel_manager.jupyter_url
|
422
|
+
self.ssh_url = self.tunnel_manager.jupyter_url
|
402
423
|
|
403
424
|
# The tunnel is now running
|
404
425
|
logger.info("✅ Tunnel and Jupyter established")
|
426
|
+
logger.info("URL: {}".format(self.jupyter_url))
|
405
427
|
self.report_services()
|
406
428
|
return True
|
407
429
|
else:
|
@@ -0,0 +1,272 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Dynamic Cloudflare Tunnel - Creates a unique tunnel for each device via API
|
4
|
+
Simple and automatic!
|
5
|
+
"""
|
6
|
+
|
7
|
+
import subprocess
|
8
|
+
import requests
|
9
|
+
import json
|
10
|
+
import time
|
11
|
+
import os
|
12
|
+
|
13
|
+
class DynamicTunnel:
|
14
|
+
def __init__(self, device_id=None):
|
15
|
+
"""
|
16
|
+
Initialize with device ID for unique tunnel creation
|
17
|
+
"""
|
18
|
+
# Cloudflare API credentials (hardcoded for simplicity)
|
19
|
+
self.cf_api_token = os.getenv("CF_API_TOKEN", "YOUR_API_TOKEN_HERE")
|
20
|
+
self.cf_account_id = os.getenv("CF_ACCOUNT_ID", "c91192ae20a5d43f65e087550d8dc89b")
|
21
|
+
self.cf_zone_id = os.getenv("CF_ZONE_ID", "YOUR_ZONE_ID_HERE")
|
22
|
+
|
23
|
+
# Domain config
|
24
|
+
self.domain = "1scan.uz"
|
25
|
+
|
26
|
+
# Generate clean device ID
|
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
|
+
# Tunnel will be created with this name
|
34
|
+
self.tunnel_name = "agent-{}".format(self.device_id)
|
35
|
+
self.subdomain = self.device_id
|
36
|
+
self.jupyter_url = "https://{}.{}".format(self.subdomain, self.domain)
|
37
|
+
|
38
|
+
self.tunnel_id = None
|
39
|
+
self.tunnel_token = None
|
40
|
+
self.jupyter_process = None
|
41
|
+
self.tunnel_process = None
|
42
|
+
|
43
|
+
def create_tunnel(self):
|
44
|
+
"""Create a new tunnel via Cloudflare API"""
|
45
|
+
print("🔧 Creating tunnel: {}".format(self.tunnel_name))
|
46
|
+
|
47
|
+
url = "https://api.cloudflare.com/client/v4/accounts/{}/tunnels".format(self.cf_account_id)
|
48
|
+
|
49
|
+
headers = {
|
50
|
+
"Authorization": "Bearer {}".format(self.cf_api_token),
|
51
|
+
"Content-Type": "application/json"
|
52
|
+
}
|
53
|
+
|
54
|
+
data = {
|
55
|
+
"name": self.tunnel_name,
|
56
|
+
"tunnel_secret": None # Let Cloudflare generate it
|
57
|
+
}
|
58
|
+
|
59
|
+
try:
|
60
|
+
response = requests.post(url, headers=headers, json=data)
|
61
|
+
result = response.json()
|
62
|
+
|
63
|
+
if response.status_code == 200 and result.get("success"):
|
64
|
+
self.tunnel_id = result["result"]["id"]
|
65
|
+
self.tunnel_token = result["result"]["token"]
|
66
|
+
print("✅ Tunnel created: {}".format(self.tunnel_id))
|
67
|
+
return True
|
68
|
+
else:
|
69
|
+
print("❌ Failed to create tunnel: {}".format(result.get("errors", "Unknown error")))
|
70
|
+
return False
|
71
|
+
|
72
|
+
except Exception as e:
|
73
|
+
print("❌ Error creating tunnel: {}".format(e))
|
74
|
+
return False
|
75
|
+
|
76
|
+
def create_dns_record(self):
|
77
|
+
"""Create DNS record for the tunnel"""
|
78
|
+
print("🔧 Creating DNS record: {}.{}".format(self.subdomain, self.domain))
|
79
|
+
|
80
|
+
url = "https://api.cloudflare.com/client/v4/zones/{}/dns_records".format(self.cf_zone_id)
|
81
|
+
|
82
|
+
headers = {
|
83
|
+
"Authorization": "Bearer {}".format(self.cf_api_token),
|
84
|
+
"Content-Type": "application/json"
|
85
|
+
}
|
86
|
+
|
87
|
+
data = {
|
88
|
+
"type": "CNAME",
|
89
|
+
"name": self.subdomain,
|
90
|
+
"content": "{}.cfargotunnel.com".format(self.tunnel_id),
|
91
|
+
"proxied": True
|
92
|
+
}
|
93
|
+
|
94
|
+
try:
|
95
|
+
response = requests.post(url, headers=headers, json=data)
|
96
|
+
result = response.json()
|
97
|
+
|
98
|
+
if response.status_code == 200 and result.get("success"):
|
99
|
+
print("✅ DNS record created")
|
100
|
+
return True
|
101
|
+
else:
|
102
|
+
# Might already exist, try to update
|
103
|
+
print("⚠️ DNS might already exist, continuing...")
|
104
|
+
return True
|
105
|
+
|
106
|
+
except Exception as e:
|
107
|
+
print("⚠️ DNS error (continuing): {}".format(e))
|
108
|
+
return True # Continue anyway
|
109
|
+
|
110
|
+
def get_cloudflared_path(self):
|
111
|
+
"""Get or download cloudflared binary"""
|
112
|
+
import platform
|
113
|
+
|
114
|
+
# Check if exists
|
115
|
+
try:
|
116
|
+
import shutil
|
117
|
+
if shutil.which("cloudflared"):
|
118
|
+
return "cloudflared"
|
119
|
+
except:
|
120
|
+
pass
|
121
|
+
|
122
|
+
# Check local
|
123
|
+
local_bin = os.path.expanduser("~/.local/bin/cloudflared")
|
124
|
+
if os.path.exists(local_bin):
|
125
|
+
return local_bin
|
126
|
+
|
127
|
+
# Download
|
128
|
+
print("📦 Downloading cloudflared...")
|
129
|
+
system = platform.system().lower()
|
130
|
+
if system == "linux":
|
131
|
+
arch = "amd64" if "x86" in platform.machine() else "arm64"
|
132
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
|
133
|
+
|
134
|
+
os.makedirs(os.path.expanduser("~/.local/bin"), exist_ok=True)
|
135
|
+
subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True)
|
136
|
+
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
137
|
+
print("✅ cloudflared downloaded")
|
138
|
+
return local_bin
|
139
|
+
|
140
|
+
return "cloudflared"
|
141
|
+
|
142
|
+
def start_jupyter(self):
|
143
|
+
"""Start Jupyter notebook"""
|
144
|
+
print("🚀 Starting Jupyter...")
|
145
|
+
|
146
|
+
cmd = [
|
147
|
+
"jupyter", "notebook",
|
148
|
+
"--port", "8888",
|
149
|
+
"--no-browser",
|
150
|
+
"--ip", "0.0.0.0",
|
151
|
+
"--NotebookApp.token=''",
|
152
|
+
"--NotebookApp.password=''",
|
153
|
+
"--NotebookApp.allow_origin='*'"
|
154
|
+
]
|
155
|
+
|
156
|
+
self.jupyter_process = subprocess.Popen(
|
157
|
+
cmd,
|
158
|
+
stdout=subprocess.PIPE,
|
159
|
+
stderr=subprocess.PIPE
|
160
|
+
)
|
161
|
+
|
162
|
+
time.sleep(3)
|
163
|
+
print("✅ Jupyter started on port 8888")
|
164
|
+
return True
|
165
|
+
|
166
|
+
def start_tunnel(self):
|
167
|
+
"""Start the tunnel using the token"""
|
168
|
+
print("🔧 Starting tunnel...")
|
169
|
+
|
170
|
+
cloudflared = self.get_cloudflared_path()
|
171
|
+
|
172
|
+
# Use the token to run tunnel
|
173
|
+
cmd = [
|
174
|
+
cloudflared,
|
175
|
+
"tunnel",
|
176
|
+
"run",
|
177
|
+
"--token", self.tunnel_token
|
178
|
+
]
|
179
|
+
|
180
|
+
self.tunnel_process = subprocess.Popen(
|
181
|
+
cmd,
|
182
|
+
stdout=subprocess.PIPE,
|
183
|
+
stderr=subprocess.PIPE
|
184
|
+
)
|
185
|
+
|
186
|
+
time.sleep(5)
|
187
|
+
print("✅ Tunnel running at {}".format(self.jupyter_url))
|
188
|
+
return True
|
189
|
+
|
190
|
+
def start(self):
|
191
|
+
"""Main entry point - creates everything dynamically"""
|
192
|
+
try:
|
193
|
+
print("="*50)
|
194
|
+
print("🌐 Dynamic Cloudflare Tunnel")
|
195
|
+
print("Device ID: {}".format(self.device_id))
|
196
|
+
print("="*50)
|
197
|
+
|
198
|
+
# 1. Create tunnel via API
|
199
|
+
if not self.create_tunnel():
|
200
|
+
raise Exception("Failed to create tunnel")
|
201
|
+
|
202
|
+
# 2. Create DNS record
|
203
|
+
self.create_dns_record()
|
204
|
+
|
205
|
+
# 3. Start Jupyter
|
206
|
+
if not self.start_jupyter():
|
207
|
+
raise Exception("Failed to start Jupyter")
|
208
|
+
|
209
|
+
# 4. Start tunnel
|
210
|
+
if not self.start_tunnel():
|
211
|
+
raise Exception("Failed to start tunnel")
|
212
|
+
|
213
|
+
print("\n" + "="*50)
|
214
|
+
print("🎉 SUCCESS! Your unique tunnel is ready:")
|
215
|
+
print(" {}".format(self.jupyter_url))
|
216
|
+
print(" Tunnel ID: {}".format(self.tunnel_id))
|
217
|
+
print("="*50)
|
218
|
+
|
219
|
+
return True
|
220
|
+
|
221
|
+
except Exception as e:
|
222
|
+
print("❌ Error: {}".format(e))
|
223
|
+
self.cleanup()
|
224
|
+
return False
|
225
|
+
|
226
|
+
def cleanup(self):
|
227
|
+
"""Clean up resources"""
|
228
|
+
if self.jupyter_process:
|
229
|
+
self.jupyter_process.terminate()
|
230
|
+
if self.tunnel_process:
|
231
|
+
self.tunnel_process.terminate()
|
232
|
+
|
233
|
+
# Optionally delete tunnel via API
|
234
|
+
if self.tunnel_id and self.cf_api_token != "YOUR_API_TOKEN_HERE":
|
235
|
+
try:
|
236
|
+
url = "https://api.cloudflare.com/client/v4/accounts/{}/tunnels/{}".format(
|
237
|
+
self.cf_account_id, self.tunnel_id
|
238
|
+
)
|
239
|
+
headers = {"Authorization": "Bearer {}".format(self.cf_api_token)}
|
240
|
+
requests.delete(url, headers=headers)
|
241
|
+
print("🗑️ Tunnel deleted")
|
242
|
+
except:
|
243
|
+
pass
|
244
|
+
|
245
|
+
def run(self):
|
246
|
+
"""Run and keep alive"""
|
247
|
+
try:
|
248
|
+
if self.start():
|
249
|
+
while True:
|
250
|
+
time.sleep(1)
|
251
|
+
except KeyboardInterrupt:
|
252
|
+
print("\n⏹️ Shutting down...")
|
253
|
+
self.cleanup()
|
254
|
+
print("👋 Goodbye!")
|
255
|
+
|
256
|
+
|
257
|
+
def main():
|
258
|
+
"""Test dynamic tunnel creation"""
|
259
|
+
import platform
|
260
|
+
import uuid
|
261
|
+
|
262
|
+
hostname = platform.node().replace('.', '-')[:20]
|
263
|
+
device_id = "{}-{}".format(hostname, str(uuid.uuid4())[:8])
|
264
|
+
|
265
|
+
print("Creating dynamic tunnel for: {}".format(device_id))
|
266
|
+
|
267
|
+
tunnel = DynamicTunnel(device_id=device_id)
|
268
|
+
tunnel.run()
|
269
|
+
|
270
|
+
|
271
|
+
if __name__ == "__main__":
|
272
|
+
main()
|
unitlab/easy_tunnel.py
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Easiest Dynamic Tunnel - Each device gets its own tunnel at deviceid.1scan.uz
|
4
|
+
Using Cloudflare API with service token
|
5
|
+
"""
|
6
|
+
|
7
|
+
import subprocess
|
8
|
+
import time
|
9
|
+
import os
|
10
|
+
import requests
|
11
|
+
import json
|
12
|
+
|
13
|
+
class EasyTunnel:
|
14
|
+
def __init__(self, device_id=None):
|
15
|
+
"""Initialize with device ID"""
|
16
|
+
# Generate clean device ID
|
17
|
+
if device_id:
|
18
|
+
self.device_id = device_id.replace('-', '').replace('_', '').replace('.', '').lower()[:20]
|
19
|
+
else:
|
20
|
+
import uuid
|
21
|
+
self.device_id = str(uuid.uuid4())[:8]
|
22
|
+
|
23
|
+
self.subdomain = self.device_id
|
24
|
+
self.jupyter_url = "https://{}.1scan.uz".format(self.subdomain)
|
25
|
+
|
26
|
+
# Processes
|
27
|
+
self.jupyter_process = None
|
28
|
+
self.tunnel_process = None
|
29
|
+
|
30
|
+
# We'll use service tokens (created per tunnel)
|
31
|
+
self.tunnel_token = None
|
32
|
+
|
33
|
+
def get_cloudflared_path(self):
|
34
|
+
"""Get or download cloudflared binary"""
|
35
|
+
import shutil
|
36
|
+
if shutil.which("cloudflared"):
|
37
|
+
return "cloudflared"
|
38
|
+
|
39
|
+
local_bin = os.path.expanduser("~/.local/bin/cloudflared")
|
40
|
+
if os.path.exists(local_bin):
|
41
|
+
return local_bin
|
42
|
+
|
43
|
+
# Download it
|
44
|
+
print("📦 Downloading cloudflared...")
|
45
|
+
import platform
|
46
|
+
system = platform.system().lower()
|
47
|
+
if system == "linux":
|
48
|
+
arch = "amd64" if "x86" in platform.machine() else "arm64"
|
49
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
|
50
|
+
|
51
|
+
os.makedirs(os.path.dirname(local_bin), exist_ok=True)
|
52
|
+
subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
|
53
|
+
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
54
|
+
print("✅ cloudflared downloaded")
|
55
|
+
return local_bin
|
56
|
+
|
57
|
+
return "cloudflared"
|
58
|
+
|
59
|
+
def create_quick_tunnel(self):
|
60
|
+
"""Use Cloudflare Quick Tunnel - no auth needed, but random URL"""
|
61
|
+
print("🔧 Creating quick tunnel (no auth needed)...")
|
62
|
+
|
63
|
+
cloudflared = self.get_cloudflared_path()
|
64
|
+
|
65
|
+
# Quick tunnel command - generates random URL
|
66
|
+
cmd = [
|
67
|
+
cloudflared,
|
68
|
+
"tunnel",
|
69
|
+
"--url", "http://localhost:8888",
|
70
|
+
"--no-tls-verify",
|
71
|
+
"--metrics", "localhost:0" # Disable metrics
|
72
|
+
]
|
73
|
+
|
74
|
+
self.tunnel_process = subprocess.Popen(
|
75
|
+
cmd,
|
76
|
+
stdout=subprocess.PIPE,
|
77
|
+
stderr=subprocess.STDOUT,
|
78
|
+
text=True,
|
79
|
+
bufsize=1
|
80
|
+
)
|
81
|
+
|
82
|
+
# Read output to get URL
|
83
|
+
print("⏳ Getting tunnel URL...")
|
84
|
+
for i in range(30):
|
85
|
+
line = self.tunnel_process.stdout.readline()
|
86
|
+
if line and "trycloudflare.com" in line:
|
87
|
+
# Extract URL from output
|
88
|
+
import re
|
89
|
+
match = re.search(r'https://[a-zA-Z0-9-]+\.trycloudflare\.com', line)
|
90
|
+
if match:
|
91
|
+
self.jupyter_url = match.group(0)
|
92
|
+
print("✅ Quick tunnel URL: {}".format(self.jupyter_url))
|
93
|
+
return True
|
94
|
+
time.sleep(0.5)
|
95
|
+
|
96
|
+
return False
|
97
|
+
|
98
|
+
def start_jupyter(self):
|
99
|
+
"""Start Jupyter notebook"""
|
100
|
+
print("🚀 Starting Jupyter...")
|
101
|
+
|
102
|
+
cmd = [
|
103
|
+
"jupyter", "notebook",
|
104
|
+
"--port", "8888",
|
105
|
+
"--no-browser",
|
106
|
+
"--ip", "0.0.0.0",
|
107
|
+
"--NotebookApp.token=''",
|
108
|
+
"--NotebookApp.password=''",
|
109
|
+
"--NotebookApp.allow_origin='*'"
|
110
|
+
]
|
111
|
+
|
112
|
+
self.jupyter_process = subprocess.Popen(
|
113
|
+
cmd,
|
114
|
+
stdout=subprocess.PIPE,
|
115
|
+
stderr=subprocess.PIPE
|
116
|
+
)
|
117
|
+
|
118
|
+
time.sleep(3)
|
119
|
+
print("✅ Jupyter started on port 8888")
|
120
|
+
return True
|
121
|
+
|
122
|
+
def start(self):
|
123
|
+
"""Start everything - super simple"""
|
124
|
+
try:
|
125
|
+
print("="*50)
|
126
|
+
print("🌐 Easy Dynamic Tunnel")
|
127
|
+
print("Device ID: {}".format(self.device_id))
|
128
|
+
print("="*50)
|
129
|
+
|
130
|
+
# 1. Start Jupyter
|
131
|
+
if not self.start_jupyter():
|
132
|
+
raise Exception("Failed to start Jupyter")
|
133
|
+
|
134
|
+
# 2. Create tunnel (quick tunnel for now)
|
135
|
+
if not self.create_quick_tunnel():
|
136
|
+
raise Exception("Failed to create tunnel")
|
137
|
+
|
138
|
+
print("\n" + "="*50)
|
139
|
+
print("🎉 SUCCESS! Your Jupyter is accessible at:")
|
140
|
+
print(" {}".format(self.jupyter_url))
|
141
|
+
print("="*50)
|
142
|
+
print("\n📝 Note: For persistent URLs at {}.1scan.uz,".format(self.subdomain))
|
143
|
+
print(" we need Cloudflare API integration")
|
144
|
+
|
145
|
+
return True
|
146
|
+
|
147
|
+
except Exception as e:
|
148
|
+
print("❌ Error: {}".format(e))
|
149
|
+
self.stop()
|
150
|
+
return False
|
151
|
+
|
152
|
+
def stop(self):
|
153
|
+
"""Stop everything"""
|
154
|
+
if self.jupyter_process:
|
155
|
+
self.jupyter_process.terminate()
|
156
|
+
self.jupyter_process = None
|
157
|
+
if self.tunnel_process:
|
158
|
+
self.tunnel_process.terminate()
|
159
|
+
self.tunnel_process = None
|
160
|
+
|
161
|
+
def run(self):
|
162
|
+
"""Run and keep alive"""
|
163
|
+
try:
|
164
|
+
if self.start():
|
165
|
+
print("\nPress Ctrl+C to stop...")
|
166
|
+
while True:
|
167
|
+
time.sleep(1)
|
168
|
+
except KeyboardInterrupt:
|
169
|
+
print("\n⏹️ Shutting down...")
|
170
|
+
self.stop()
|
171
|
+
print("👋 Goodbye!")
|
172
|
+
|
173
|
+
|
174
|
+
# For integration with existing client.py
|
175
|
+
class EasyTunnelAdapter:
|
176
|
+
"""Adapter to make EasyTunnel work with existing client.py interface"""
|
177
|
+
def __init__(self, device_id):
|
178
|
+
self.tunnel = EasyTunnel(device_id)
|
179
|
+
self.jupyter_process = None
|
180
|
+
self.tunnel_process = None
|
181
|
+
self.jupyter_url = None
|
182
|
+
|
183
|
+
def start(self):
|
184
|
+
"""Start method compatible with client.py"""
|
185
|
+
if self.tunnel.start():
|
186
|
+
self.jupyter_process = self.tunnel.jupyter_process
|
187
|
+
self.tunnel_process = self.tunnel.tunnel_process
|
188
|
+
self.jupyter_url = self.tunnel.jupyter_url
|
189
|
+
return True
|
190
|
+
return False
|
191
|
+
|
192
|
+
def stop(self):
|
193
|
+
"""Stop method compatible with client.py"""
|
194
|
+
self.tunnel.stop()
|
195
|
+
|
196
|
+
|
197
|
+
def main():
|
198
|
+
"""Test the easy tunnel"""
|
199
|
+
import platform
|
200
|
+
import uuid
|
201
|
+
|
202
|
+
hostname = platform.node().replace('.', '-')[:20]
|
203
|
+
device_id = "{}-{}".format(hostname, str(uuid.uuid4())[:8])
|
204
|
+
|
205
|
+
tunnel = EasyTunnel(device_id=device_id)
|
206
|
+
tunnel.run()
|
207
|
+
|
208
|
+
|
209
|
+
if __name__ == "__main__":
|
210
|
+
main()
|
@@ -0,0 +1,348 @@
|
|
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 create_tunnel(self):
|
71
|
+
"""Create a new tunnel via API"""
|
72
|
+
print("🔧 Creating tunnel: {}...".format(self.tunnel_name))
|
73
|
+
|
74
|
+
# Generate random tunnel secret (32 bytes)
|
75
|
+
import secrets
|
76
|
+
tunnel_secret = base64.b64encode(secrets.token_bytes(32)).decode()
|
77
|
+
|
78
|
+
url = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel".format(self.cf_account_id)
|
79
|
+
headers = self._get_headers()
|
80
|
+
|
81
|
+
data = {
|
82
|
+
"name": self.tunnel_name,
|
83
|
+
"tunnel_secret": tunnel_secret
|
84
|
+
}
|
85
|
+
|
86
|
+
response = requests.post(url, headers=headers, json=data)
|
87
|
+
|
88
|
+
if response.status_code in [200, 201]:
|
89
|
+
result = response.json()["result"]
|
90
|
+
self.tunnel_id = result["id"]
|
91
|
+
|
92
|
+
# Create credentials JSON
|
93
|
+
self.tunnel_credentials = {
|
94
|
+
"AccountTag": self.cf_account_id,
|
95
|
+
"TunnelSecret": tunnel_secret,
|
96
|
+
"TunnelID": self.tunnel_id
|
97
|
+
}
|
98
|
+
|
99
|
+
# Save credentials to file
|
100
|
+
cred_file = "/tmp/tunnel-{}.json".format(self.tunnel_id)
|
101
|
+
with open(cred_file, 'w') as f:
|
102
|
+
json.dump(self.tunnel_credentials, f)
|
103
|
+
|
104
|
+
print("✅ Tunnel created: {}".format(self.tunnel_id))
|
105
|
+
return cred_file
|
106
|
+
else:
|
107
|
+
print("❌ Failed to create tunnel: {}".format(response.text[:200]))
|
108
|
+
return None
|
109
|
+
|
110
|
+
def create_dns_record(self):
|
111
|
+
"""Create DNS CNAME record"""
|
112
|
+
if not self.tunnel_id:
|
113
|
+
return False
|
114
|
+
|
115
|
+
print("🔧 Creating DNS record: {}.{}...".format(self.subdomain, self.domain))
|
116
|
+
|
117
|
+
# Get zone ID if we don't have it
|
118
|
+
if self.cf_zone_id == "NEED_ZONE_ID_FOR_1SCAN_UZ":
|
119
|
+
self.get_zone_id()
|
120
|
+
|
121
|
+
url = "https://api.cloudflare.com/client/v4/zones/{}/dns_records".format(self.cf_zone_id)
|
122
|
+
headers = self._get_headers()
|
123
|
+
|
124
|
+
data = {
|
125
|
+
"type": "CNAME",
|
126
|
+
"name": self.subdomain,
|
127
|
+
"content": "{}.cfargotunnel.com".format(self.tunnel_id),
|
128
|
+
"proxied": True,
|
129
|
+
"ttl": 1
|
130
|
+
}
|
131
|
+
|
132
|
+
response = requests.post(url, headers=headers, json=data)
|
133
|
+
|
134
|
+
if response.status_code in [200, 201]:
|
135
|
+
print("✅ DNS record created")
|
136
|
+
return True
|
137
|
+
elif "already exists" in response.text:
|
138
|
+
print("⚠️ DNS record already exists")
|
139
|
+
return True
|
140
|
+
else:
|
141
|
+
print("❌ Failed to create DNS: {}".format(response.text[:200]))
|
142
|
+
return False
|
143
|
+
|
144
|
+
def create_tunnel_config(self, cred_file):
|
145
|
+
"""Create tunnel config file"""
|
146
|
+
config = {
|
147
|
+
"ingress": [
|
148
|
+
{
|
149
|
+
"hostname": "{}.{}".format(self.subdomain, self.domain),
|
150
|
+
"service": "http://localhost:8888"
|
151
|
+
},
|
152
|
+
{
|
153
|
+
"service": "http_status:404"
|
154
|
+
}
|
155
|
+
]
|
156
|
+
}
|
157
|
+
|
158
|
+
config_file = "/tmp/tunnel-config-{}.yml".format(self.tunnel_id)
|
159
|
+
with open(config_file, 'w') as f:
|
160
|
+
f.write("tunnel: {}\n".format(self.tunnel_id))
|
161
|
+
f.write("credentials-file: {}\n\n".format(cred_file))
|
162
|
+
f.write("ingress:\n")
|
163
|
+
f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
|
164
|
+
f.write(" service: http://localhost:8888\n")
|
165
|
+
f.write(" - service: http_status:404\n")
|
166
|
+
|
167
|
+
return config_file
|
168
|
+
|
169
|
+
def get_cloudflared_path(self):
|
170
|
+
"""Get or download cloudflared"""
|
171
|
+
import shutil
|
172
|
+
if shutil.which("cloudflared"):
|
173
|
+
return "cloudflared"
|
174
|
+
|
175
|
+
local_bin = os.path.expanduser("~/.local/bin/cloudflared")
|
176
|
+
if os.path.exists(local_bin):
|
177
|
+
return local_bin
|
178
|
+
|
179
|
+
# Download
|
180
|
+
print("📦 Downloading cloudflared...")
|
181
|
+
import platform
|
182
|
+
system = platform.system().lower()
|
183
|
+
arch = "amd64" if "x86" in platform.machine() else "arm64"
|
184
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
|
185
|
+
|
186
|
+
os.makedirs(os.path.dirname(local_bin), exist_ok=True)
|
187
|
+
subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
|
188
|
+
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
189
|
+
return local_bin
|
190
|
+
|
191
|
+
def start_jupyter(self):
|
192
|
+
"""Start Jupyter"""
|
193
|
+
print("🚀 Starting Jupyter...")
|
194
|
+
|
195
|
+
cmd = [
|
196
|
+
"jupyter", "notebook",
|
197
|
+
"--port", "8888",
|
198
|
+
"--no-browser",
|
199
|
+
"--ip", "0.0.0.0",
|
200
|
+
"--NotebookApp.token=''",
|
201
|
+
"--NotebookApp.password=''"
|
202
|
+
]
|
203
|
+
|
204
|
+
self.jupyter_process = subprocess.Popen(
|
205
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
206
|
+
)
|
207
|
+
|
208
|
+
time.sleep(3)
|
209
|
+
print("✅ Jupyter started")
|
210
|
+
return True
|
211
|
+
|
212
|
+
def start_tunnel(self, config_file):
|
213
|
+
"""Start tunnel with config"""
|
214
|
+
print("🔧 Starting tunnel...")
|
215
|
+
|
216
|
+
cloudflared = self.get_cloudflared_path()
|
217
|
+
|
218
|
+
cmd = [
|
219
|
+
cloudflared,
|
220
|
+
"tunnel",
|
221
|
+
"--config", config_file,
|
222
|
+
"run"
|
223
|
+
]
|
224
|
+
|
225
|
+
self.tunnel_process = subprocess.Popen(
|
226
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
227
|
+
)
|
228
|
+
|
229
|
+
time.sleep(5)
|
230
|
+
print("✅ Tunnel running at {}".format(self.jupyter_url))
|
231
|
+
return True
|
232
|
+
|
233
|
+
def start(self):
|
234
|
+
"""Main entry point"""
|
235
|
+
try:
|
236
|
+
print("="*50)
|
237
|
+
print("🌐 Persistent Tunnel with API")
|
238
|
+
print("Device: {}".format(self.device_id))
|
239
|
+
print("Target: {}.{}".format(self.subdomain, self.domain))
|
240
|
+
print("="*50)
|
241
|
+
|
242
|
+
# API credentials are hardcoded, so we're ready to go
|
243
|
+
|
244
|
+
# 1. Create tunnel via API
|
245
|
+
cred_file = self.create_tunnel()
|
246
|
+
if not cred_file:
|
247
|
+
print("⚠️ Falling back to quick tunnel")
|
248
|
+
return self.start_quick_tunnel()
|
249
|
+
|
250
|
+
# 2. Create DNS record
|
251
|
+
self.create_dns_record()
|
252
|
+
|
253
|
+
# 3. Create config
|
254
|
+
config_file = self.create_tunnel_config(cred_file)
|
255
|
+
|
256
|
+
# 4. Start services
|
257
|
+
self.start_jupyter()
|
258
|
+
self.start_tunnel(config_file)
|
259
|
+
|
260
|
+
print("\n" + "="*50)
|
261
|
+
print("🎉 SUCCESS! Persistent URL created:")
|
262
|
+
print(" {}".format(self.jupyter_url))
|
263
|
+
print(" Tunnel ID: {}".format(self.tunnel_id))
|
264
|
+
print("="*50)
|
265
|
+
|
266
|
+
return True
|
267
|
+
|
268
|
+
except Exception as e:
|
269
|
+
print("❌ Error: {}".format(e))
|
270
|
+
import traceback
|
271
|
+
traceback.print_exc()
|
272
|
+
self.stop()
|
273
|
+
return False
|
274
|
+
|
275
|
+
def start_quick_tunnel(self):
|
276
|
+
"""Fallback to quick tunnel"""
|
277
|
+
print("🔧 Using quick tunnel (temporary URL)...")
|
278
|
+
|
279
|
+
# Start Jupyter first
|
280
|
+
self.start_jupyter()
|
281
|
+
|
282
|
+
# Start quick tunnel
|
283
|
+
cloudflared = self.get_cloudflared_path()
|
284
|
+
cmd = [cloudflared, "tunnel", "--url", "http://localhost:8888"]
|
285
|
+
|
286
|
+
self.tunnel_process = subprocess.Popen(
|
287
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
288
|
+
)
|
289
|
+
|
290
|
+
# Get URL from output
|
291
|
+
for _ in range(30):
|
292
|
+
line = self.tunnel_process.stdout.readline()
|
293
|
+
if "trycloudflare.com" in line:
|
294
|
+
import re
|
295
|
+
match = re.search(r'https://[a-zA-Z0-9-]+\.trycloudflare\.com', line)
|
296
|
+
if match:
|
297
|
+
self.jupyter_url = match.group(0)
|
298
|
+
print("✅ Quick tunnel: {}".format(self.jupyter_url))
|
299
|
+
return True
|
300
|
+
time.sleep(0.5)
|
301
|
+
|
302
|
+
return False
|
303
|
+
|
304
|
+
def stop(self):
|
305
|
+
"""Stop everything"""
|
306
|
+
if self.jupyter_process:
|
307
|
+
self.jupyter_process.terminate()
|
308
|
+
if self.tunnel_process:
|
309
|
+
self.tunnel_process.terminate()
|
310
|
+
|
311
|
+
# Optionally delete tunnel when stopping
|
312
|
+
if self.tunnel_id:
|
313
|
+
try:
|
314
|
+
url = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel/{}".format(
|
315
|
+
self.cf_account_id, self.tunnel_id
|
316
|
+
)
|
317
|
+
requests.delete(url, headers=self._get_headers())
|
318
|
+
print("🗑️ Tunnel deleted")
|
319
|
+
except Exception as e:
|
320
|
+
pass # Ignore cleanup errors
|
321
|
+
|
322
|
+
def run(self):
|
323
|
+
"""Run and keep alive"""
|
324
|
+
try:
|
325
|
+
if self.start():
|
326
|
+
print("\nPress Ctrl+C to stop...")
|
327
|
+
while True:
|
328
|
+
time.sleep(1)
|
329
|
+
except KeyboardInterrupt:
|
330
|
+
print("\n⏹️ Shutting down...")
|
331
|
+
self.stop()
|
332
|
+
|
333
|
+
|
334
|
+
def main():
|
335
|
+
import platform
|
336
|
+
import uuid
|
337
|
+
|
338
|
+
hostname = platform.node().replace('.', '-')[:20]
|
339
|
+
device_id = "{}-{}".format(hostname, str(uuid.uuid4())[:8])
|
340
|
+
|
341
|
+
print("Device ID: {}".format(device_id))
|
342
|
+
|
343
|
+
tunnel = PersistentTunnel(device_id=device_id)
|
344
|
+
tunnel.run()
|
345
|
+
|
346
|
+
|
347
|
+
if __name__ == "__main__":
|
348
|
+
main()
|
@@ -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=
|
6
|
+
unitlab/client.py,sha256=roVX8yq1x8LW1XEFuic4G-Cq1QEjsk2CySGkiJbxA78,25759
|
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=0ubhsUOJUpDKG0xo18e6mN1V4pxNQvNFjylC1J1QglA,11712
|
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.
|
14
|
-
unitlab-2.3.
|
15
|
-
unitlab-2.3.
|
16
|
-
unitlab-2.3.
|
17
|
-
unitlab-2.3.
|
18
|
-
unitlab-2.3.
|
18
|
+
unitlab-2.3.29.dist-info/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
|
19
|
+
unitlab-2.3.29.dist-info/METADATA,sha256=OG1XzALj4PXPvmcrJ4l15-DXdelWtgxm5y0Wo9k9uZ8,844
|
20
|
+
unitlab-2.3.29.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
21
|
+
unitlab-2.3.29.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
|
22
|
+
unitlab-2.3.29.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
|
23
|
+
unitlab-2.3.29.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|