unitlab 2.3.33__py3-none-any.whl → 2.3.34__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 +79 -64
- unitlab/main.py +17 -32
- unitlab/persistent_tunnel.py +141 -50
- unitlab/utils.py +2 -0
- {unitlab-2.3.33.dist-info → unitlab-2.3.34.dist-info}/METADATA +12 -3
- unitlab-2.3.34.dist-info/RECORD +13 -0
- {unitlab-2.3.33.dist-info → unitlab-2.3.34.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.34.dist-info}/entry_points.txt +0 -0
- {unitlab-2.3.33.dist-info → unitlab-2.3.34.dist-info/licenses}/LICENSE.md +0 -0
- {unitlab-2.3.33.dist-info → unitlab-2.3.34.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: unitlab
|
3
|
-
Version: 2.3.
|
3
|
+
Version: 2.3.34
|
4
4
|
Home-page: https://github.com/teamunitlab/unitlab-sdk
|
5
5
|
Author: Unitlab Inc.
|
6
6
|
Author-email: team@unitlab.ai
|
@@ -25,4 +25,13 @@ Requires-Dist: psutil
|
|
25
25
|
Requires-Dist: pyyaml
|
26
26
|
Requires-Dist: jupyter
|
27
27
|
Requires-Dist: python-dotenv
|
28
|
-
|
28
|
+
Requires-Dist: uvicorn
|
29
|
+
Requires-Dist: fastapi
|
30
|
+
Dynamic: author
|
31
|
+
Dynamic: author-email
|
32
|
+
Dynamic: classifier
|
33
|
+
Dynamic: home-page
|
34
|
+
Dynamic: keywords
|
35
|
+
Dynamic: license
|
36
|
+
Dynamic: license-file
|
37
|
+
Dynamic: requires-dist
|
@@ -0,0 +1,13 @@
|
|
1
|
+
unitlab/__init__.py,sha256=Wtk5kQ_MTlxtd3mxJIn2qHVK5URrVcasMMPjD3BtrVM,214
|
2
|
+
unitlab/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
|
3
|
+
unitlab/client.py,sha256=LMZ7HFjRxzPV2IrCXlP2GETlY0vWvAxP0RrjSXOn_Jk,26015
|
4
|
+
unitlab/exceptions.py,sha256=68Tr6LreEzjQ3Vns8HAaWdtewtkNUJOvPazbf6NSnXU,950
|
5
|
+
unitlab/main.py,sha256=EbQNO-Z5drNQjDXJp_sIs5a3WgPoqxaXxpdFGzMWm6k,4416
|
6
|
+
unitlab/persistent_tunnel.py,sha256=XDJo2PPq4EjEtI4vT68LIGUUq7WV4m0bnXYKPfY51cY,21180
|
7
|
+
unitlab/utils.py,sha256=9gPRu-d6pbhSoVdll1GXe4eoz_uFYOSbYArFDQdlUZs,1922
|
8
|
+
unitlab-2.3.34.dist-info/licenses/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
|
9
|
+
unitlab-2.3.34.dist-info/METADATA,sha256=ctnyV_ZT0vuOzGMoH0N09v0nalsR79WQCGdi_D-ncw0,1046
|
10
|
+
unitlab-2.3.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
+
unitlab-2.3.34.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
|
12
|
+
unitlab-2.3.34.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
|
13
|
+
unitlab-2.3.34.dist-info/RECORD,,
|
unitlab/api_tunnel.py
DELETED
@@ -1,238 +0,0 @@
|
|
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
DELETED
@@ -1,174 +0,0 @@
|
|
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/binary_manager.py
DELETED
@@ -1,154 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import platform
|
3
|
-
import hashlib
|
4
|
-
import urllib.request
|
5
|
-
from pathlib import Path
|
6
|
-
import stat
|
7
|
-
import json
|
8
|
-
|
9
|
-
class CloudflaredBinaryManager:
|
10
|
-
"""
|
11
|
-
Manages cloudflared binary automatically
|
12
|
-
- Downloads on first use
|
13
|
-
- Caches for future use
|
14
|
-
- Verifies integrity
|
15
|
-
- Zero user configuration
|
16
|
-
"""
|
17
|
-
|
18
|
-
# Binary URLs and checksums
|
19
|
-
BINARIES = {
|
20
|
-
'linux-amd64': {
|
21
|
-
'url': 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64',
|
22
|
-
'checksum': 'sha256:...', # Add real checksums
|
23
|
-
'filename': 'cloudflared'
|
24
|
-
},
|
25
|
-
'linux-arm64': {
|
26
|
-
'url': 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64',
|
27
|
-
'checksum': 'sha256:...',
|
28
|
-
'filename': 'cloudflared'
|
29
|
-
},
|
30
|
-
'darwin-amd64': {
|
31
|
-
'url': 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz',
|
32
|
-
'checksum': 'sha256:...',
|
33
|
-
'filename': 'cloudflared',
|
34
|
-
'compressed': True
|
35
|
-
},
|
36
|
-
'windows-amd64': {
|
37
|
-
'url': 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe',
|
38
|
-
'checksum': 'sha256:...',
|
39
|
-
'filename': 'cloudflared.exe'
|
40
|
-
}
|
41
|
-
}
|
42
|
-
|
43
|
-
def __init__(self):
|
44
|
-
# User's home directory - works on all platforms
|
45
|
-
self.cache_dir = Path.home() / '.unitlab' / 'bin'
|
46
|
-
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
47
|
-
|
48
|
-
# Detect platform once
|
49
|
-
self.platform_key = self._detect_platform()
|
50
|
-
|
51
|
-
def _detect_platform(self):
|
52
|
-
"""Detect OS and architecture"""
|
53
|
-
system = platform.system().lower()
|
54
|
-
machine = platform.machine().lower()
|
55
|
-
|
56
|
-
if system == 'linux':
|
57
|
-
if machine in ['x86_64', 'amd64']:
|
58
|
-
return 'linux-amd64'
|
59
|
-
elif machine in ['aarch64', 'arm64']:
|
60
|
-
return 'linux-arm64'
|
61
|
-
|
62
|
-
elif system == 'darwin': # macOS
|
63
|
-
# Check if ARM (M1/M2) or Intel
|
64
|
-
if machine == 'arm64':
|
65
|
-
return 'darwin-arm64'
|
66
|
-
return 'darwin-amd64'
|
67
|
-
|
68
|
-
elif system == 'windows':
|
69
|
-
return 'windows-amd64'
|
70
|
-
|
71
|
-
raise RuntimeError(f"Unsupported platform: {system} {machine}")
|
72
|
-
|
73
|
-
def get_binary_path(self):
|
74
|
-
"""Get path to cloudflared binary, downloading if needed"""
|
75
|
-
|
76
|
-
binary_info = self.BINARIES[self.platform_key]
|
77
|
-
binary_path = self.cache_dir / binary_info['filename']
|
78
|
-
|
79
|
-
# Check if already downloaded
|
80
|
-
if binary_path.exists():
|
81
|
-
print("✓ Using cached cloudflared")
|
82
|
-
return str(binary_path)
|
83
|
-
|
84
|
-
# Download for first time
|
85
|
-
print("🔄 First time setup - downloading cloudflared...")
|
86
|
-
self._download_binary(binary_info, binary_path)
|
87
|
-
|
88
|
-
return str(binary_path)
|
89
|
-
|
90
|
-
def _download_binary(self, info, target_path):
|
91
|
-
"""Download and verify binary"""
|
92
|
-
|
93
|
-
# Create SSL context to handle certificate issues
|
94
|
-
import ssl
|
95
|
-
ssl_context = ssl.create_default_context()
|
96
|
-
ssl_context.check_hostname = False
|
97
|
-
ssl_context.verify_mode = ssl.CERT_NONE
|
98
|
-
|
99
|
-
# Download with progress bar
|
100
|
-
def download_progress(block_num, block_size, total_size):
|
101
|
-
downloaded = block_num * block_size
|
102
|
-
if total_size > 0:
|
103
|
-
percent = min(downloaded * 100 / total_size, 100)
|
104
|
-
print(f"Downloading: {percent:.0f}%", end='\r')
|
105
|
-
else:
|
106
|
-
print(f"Downloading: {downloaded} bytes", end='\r')
|
107
|
-
|
108
|
-
temp_file = target_path.with_suffix('.tmp')
|
109
|
-
|
110
|
-
try:
|
111
|
-
# Download file with SSL context
|
112
|
-
req = urllib.request.Request(info['url'], headers={'User-Agent': 'Mozilla/5.0'})
|
113
|
-
with urllib.request.urlopen(req, context=ssl_context) as response:
|
114
|
-
total_size = int(response.headers.get('Content-Length', 0))
|
115
|
-
|
116
|
-
with open(temp_file, 'wb') as f:
|
117
|
-
downloaded = 0
|
118
|
-
block_size = 8192
|
119
|
-
while True:
|
120
|
-
chunk = response.read(block_size)
|
121
|
-
if not chunk:
|
122
|
-
break
|
123
|
-
f.write(chunk)
|
124
|
-
downloaded += len(chunk)
|
125
|
-
if total_size > 0:
|
126
|
-
percent = min(downloaded * 100 / total_size, 100)
|
127
|
-
print(f"Downloading: {percent:.0f}%", end='\r')
|
128
|
-
print("\n✓ Download complete")
|
129
|
-
|
130
|
-
# Handle compressed files (macOS .tgz)
|
131
|
-
if info.get('compressed'):
|
132
|
-
import tarfile
|
133
|
-
with tarfile.open(temp_file, 'r:gz') as tar:
|
134
|
-
# Extract just the cloudflared binary
|
135
|
-
tar.extract('cloudflared', self.cache_dir)
|
136
|
-
temp_file.unlink()
|
137
|
-
else:
|
138
|
-
# Move to final location
|
139
|
-
temp_file.rename(target_path)
|
140
|
-
|
141
|
-
# Make executable on Unix systems
|
142
|
-
if platform.system() != 'Windows':
|
143
|
-
target_path.chmod(target_path.stat().st_mode | stat.S_IEXEC)
|
144
|
-
|
145
|
-
print("✓ Cloudflared ready!")
|
146
|
-
|
147
|
-
except Exception as e:
|
148
|
-
print(f"❌ Download failed: {e}")
|
149
|
-
if temp_file.exists():
|
150
|
-
temp_file.unlink()
|
151
|
-
raise
|
152
|
-
|
153
|
-
|
154
|
-
|