unitlab 2.3.33__py3-none-any.whl → 2.3.35__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unitlab/client.py +78 -64
- unitlab/main.py +17 -32
- unitlab/persistent_tunnel.py +196 -51
- unitlab/utils.py +2 -0
- {unitlab-2.3.33.dist-info → unitlab-2.3.35.dist-info}/METADATA +12 -3
- unitlab-2.3.35.dist-info/RECORD +13 -0
- {unitlab-2.3.33.dist-info → unitlab-2.3.35.dist-info}/WHEEL +1 -1
- unitlab/api_tunnel.py +0 -238
- unitlab/auto_tunnel.py +0 -174
- unitlab/binary_manager.py +0 -154
- unitlab/cloudflare_api_tunnel.py +0 -379
- unitlab/cloudflare_api_tunnel_backup.py +0 -653
- unitlab/dynamic_tunnel.py +0 -272
- unitlab/easy_tunnel.py +0 -210
- unitlab/simple_tunnel.py +0 -205
- unitlab/tunnel_config.py +0 -204
- unitlab/tunnel_service_token.py +0 -104
- unitlab-2.3.33.dist-info/RECORD +0 -23
- {unitlab-2.3.33.dist-info → unitlab-2.3.35.dist-info}/entry_points.txt +0 -0
- {unitlab-2.3.33.dist-info → unitlab-2.3.35.dist-info/licenses}/LICENSE.md +0 -0
- {unitlab-2.3.33.dist-info → unitlab-2.3.35.dist-info}/top_level.txt +0 -0
unitlab/dynamic_tunnel.py
DELETED
@@ -1,272 +0,0 @@
|
|
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
DELETED
@@ -1,210 +0,0 @@
|
|
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()
|
unitlab/simple_tunnel.py
DELETED
@@ -1,205 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
Simple Cloudflare Tunnel for Jupyter - MVP Version
|
4
|
-
Uses hardcoded tunnel credentials
|
5
|
-
"""
|
6
|
-
|
7
|
-
import subprocess
|
8
|
-
from pathlib import Path
|
9
|
-
import time
|
10
|
-
|
11
|
-
|
12
|
-
class SimpleTunnel:
|
13
|
-
def __init__(self, device_id=None):
|
14
|
-
"""
|
15
|
-
Initialize SimpleTunnel with hardcoded everything
|
16
|
-
device_id: Unique device identifier for subdomain generation
|
17
|
-
"""
|
18
|
-
# Everything hardcoded for simplicity
|
19
|
-
self.tunnel_uuid = "c6caf64a-7499-4aa5-8702-0d1870388114"
|
20
|
-
self.domain = "1scan.uz"
|
21
|
-
|
22
|
-
# Generate unique subdomain from device_id
|
23
|
-
if device_id:
|
24
|
-
# Clean device_id: remove special chars, lowercase, limit length
|
25
|
-
clean_id = device_id.replace('-', '').replace('_', '').replace('.', '').lower()[:30]
|
26
|
-
self.subdomain = clean_id
|
27
|
-
else:
|
28
|
-
# Fallback to a random subdomain if no device_id
|
29
|
-
import uuid
|
30
|
-
self.subdomain = str(uuid.uuid4())[:8]
|
31
|
-
|
32
|
-
# The unique URL for this device
|
33
|
-
self.jupyter_url = "https://{}.{}".format(self.subdomain, self.domain)
|
34
|
-
|
35
|
-
self.jupyter_process = None
|
36
|
-
self.tunnel_process = None
|
37
|
-
|
38
|
-
def start_jupyter(self, port=8888):
|
39
|
-
"""Start Jupyter notebook server"""
|
40
|
-
print("🚀 Starting Jupyter on port {}...".format(port))
|
41
|
-
|
42
|
-
cmd = [
|
43
|
-
"jupyter", "notebook",
|
44
|
-
"--port", '8888',
|
45
|
-
"--no-browser",
|
46
|
-
"--ip", "0.0.0.0",
|
47
|
-
"--ServerApp.token=''",
|
48
|
-
"--ServerApp.password=''",
|
49
|
-
"--ServerApp.allow_origin='*'"
|
50
|
-
]
|
51
|
-
|
52
|
-
self.jupyter_process = subprocess.Popen(
|
53
|
-
cmd,
|
54
|
-
stdout=subprocess.PIPE,
|
55
|
-
stderr=subprocess.PIPE
|
56
|
-
)
|
57
|
-
|
58
|
-
# Wait for Jupyter to start
|
59
|
-
time.sleep(3)
|
60
|
-
|
61
|
-
print("✅ Jupyter started on port {}".format(port))
|
62
|
-
return True
|
63
|
-
|
64
|
-
def get_cloudflared_path(self):
|
65
|
-
"""Get cloudflared binary, download if needed"""
|
66
|
-
import os
|
67
|
-
import platform
|
68
|
-
|
69
|
-
# Check if cloudflared exists in system
|
70
|
-
try:
|
71
|
-
import shutil
|
72
|
-
if shutil.which("cloudflared"):
|
73
|
-
print("✅ Using system cloudflared")
|
74
|
-
return "cloudflared"
|
75
|
-
except:
|
76
|
-
pass
|
77
|
-
|
78
|
-
# Check local binary
|
79
|
-
local_bin = os.path.expanduser("~/.local/bin/cloudflared")
|
80
|
-
if os.path.exists(local_bin):
|
81
|
-
print("✅ Using existing cloudflared from ~/.local/bin")
|
82
|
-
return local_bin
|
83
|
-
|
84
|
-
# Download it
|
85
|
-
print("📦 Downloading cloudflared (this may take a moment)...")
|
86
|
-
system = platform.system().lower()
|
87
|
-
if system == "linux":
|
88
|
-
arch = "amd64" if "x86" in platform.machine() else "arm64"
|
89
|
-
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
|
90
|
-
|
91
|
-
os.makedirs(os.path.expanduser("~/.local/bin"), exist_ok=True)
|
92
|
-
result = subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
|
93
|
-
if result.returncode == 0:
|
94
|
-
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
95
|
-
print("✅ cloudflared downloaded successfully")
|
96
|
-
return local_bin
|
97
|
-
else:
|
98
|
-
print("❌ Failed to download cloudflared")
|
99
|
-
raise Exception("Could not download cloudflared")
|
100
|
-
|
101
|
-
raise Exception("Could not find or download cloudflared")
|
102
|
-
|
103
|
-
def start_tunnel(self, local_port=8888):
|
104
|
-
"""Start cloudflared tunnel - simple and direct"""
|
105
|
-
print("🔧 Starting Cloudflare tunnel...")
|
106
|
-
|
107
|
-
# Get cloudflared (downloads if needed)
|
108
|
-
try:
|
109
|
-
cloudflared = self.get_cloudflared_path()
|
110
|
-
except Exception as e:
|
111
|
-
print("⚠️ Error getting cloudflared: {}".format(e))
|
112
|
-
# Fallback - try to use system cloudflared anyway
|
113
|
-
cloudflared = "cloudflared"
|
114
|
-
|
115
|
-
# Simple command - just run the tunnel
|
116
|
-
cmd = [
|
117
|
-
cloudflared,
|
118
|
-
"tunnel",
|
119
|
-
"run",
|
120
|
-
"--url", "http://127.0.0.1:{}".format(local_port),
|
121
|
-
self.tunnel_uuid
|
122
|
-
]
|
123
|
-
|
124
|
-
self.tunnel_process = subprocess.Popen(
|
125
|
-
cmd,
|
126
|
-
stdout=subprocess.PIPE,
|
127
|
-
stderr=subprocess.PIPE
|
128
|
-
)
|
129
|
-
|
130
|
-
# Wait for tunnel to establish
|
131
|
-
time.sleep(3)
|
132
|
-
|
133
|
-
print("✅ Tunnel running at {}".format(self.jupyter_url))
|
134
|
-
|
135
|
-
def start(self):
|
136
|
-
"""Start tunnel and Jupyter (non-blocking)"""
|
137
|
-
try:
|
138
|
-
print("="*50)
|
139
|
-
print("🌐 Simple Cloudflare Tunnel for Jupyter - MVP")
|
140
|
-
print("="*50)
|
141
|
-
|
142
|
-
# 1. Cloudflared should be installed on system
|
143
|
-
|
144
|
-
# 2. Start Jupyter
|
145
|
-
if not self.start_jupyter():
|
146
|
-
raise Exception("Failed to start Jupyter")
|
147
|
-
|
148
|
-
# 3. Start tunnel
|
149
|
-
self.start_tunnel()
|
150
|
-
|
151
|
-
# 4. Print access info
|
152
|
-
print("\n" + "="*50)
|
153
|
-
print("🎉 SUCCESS! Your Jupyter is now accessible at:")
|
154
|
-
print(" {}".format(self.jupyter_url))
|
155
|
-
print(" Device subdomain: {}".format(self.subdomain))
|
156
|
-
print("="*50)
|
157
|
-
|
158
|
-
return True
|
159
|
-
|
160
|
-
except Exception as e:
|
161
|
-
print("❌ Error: {}".format(e))
|
162
|
-
self.stop()
|
163
|
-
return False
|
164
|
-
|
165
|
-
def stop(self):
|
166
|
-
"""Stop tunnel and Jupyter"""
|
167
|
-
if self.jupyter_process:
|
168
|
-
self.jupyter_process.terminate()
|
169
|
-
self.jupyter_process = None
|
170
|
-
if self.tunnel_process:
|
171
|
-
self.tunnel_process.terminate()
|
172
|
-
self.tunnel_process = None
|
173
|
-
|
174
|
-
def run(self):
|
175
|
-
"""Main entry point for standalone use - sets up everything and blocks"""
|
176
|
-
try:
|
177
|
-
if self.start():
|
178
|
-
# Keep running
|
179
|
-
while True:
|
180
|
-
time.sleep(1)
|
181
|
-
except KeyboardInterrupt:
|
182
|
-
print("\n⏹️ Shutting down...")
|
183
|
-
self.stop()
|
184
|
-
print("👋 Goodbye!")
|
185
|
-
|
186
|
-
|
187
|
-
def main():
|
188
|
-
"""Example usage with device ID"""
|
189
|
-
|
190
|
-
# Generate a unique device ID (in real usage, this comes from main.py)
|
191
|
-
import platform
|
192
|
-
import uuid
|
193
|
-
hostname = platform.node().replace('.', '-').replace(' ', '-')[:20]
|
194
|
-
random_suffix = str(uuid.uuid4())[:8]
|
195
|
-
device_id = "{}-{}".format(hostname, random_suffix)
|
196
|
-
|
197
|
-
print("Device ID: {}".format(device_id))
|
198
|
-
|
199
|
-
# Create tunnel with device ID for unique subdomain
|
200
|
-
tunnel = SimpleTunnel(device_id=device_id)
|
201
|
-
tunnel.run()
|
202
|
-
|
203
|
-
|
204
|
-
if __name__ == "__main__":
|
205
|
-
main()
|