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 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 the simple hardcoded tunnel approach
285
- logger.info("Using simple hardcoded Cloudflare tunnel")
286
- self.tunnel_manager = SimpleTunnel(device_id=self.device_id)
287
- # SimpleTunnel generates the URL internally based on device_id
288
- self.jupyter_url = self.tunnel_manager.jupyter_url
289
- self.ssh_url = self.jupyter_url # Same URL for both services
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 handles both Jupyter and tunnel startup internally
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" # SimpleTunnel uses fixed port
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unitlab
3
- Version: 2.3.28
3
+ Version: 2.3.29
4
4
  Home-page: https://github.com/teamunitlab/unitlab-sdk
5
5
  Author: Unitlab Inc.
6
6
  Author-email: team@unitlab.ai
@@ -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=TAv9ePzs8gGAgqGXkiGCxD-cI5dEGKWnyGKAU6UiR0M,24635
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.28.dist-info/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
14
- unitlab-2.3.28.dist-info/METADATA,sha256=Ig_y0Z7LQzzQkeE2NIKX6lq46EuKP1VVd1TqURBqfDI,844
15
- unitlab-2.3.28.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
16
- unitlab-2.3.28.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
17
- unitlab-2.3.28.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
18
- unitlab-2.3.28.dist-info/RECORD,,
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,,