Add auto-downloading cloudflared tunnel manager
linuztx committed
Nov 10, 2024 at 14:56 UTC
2b1aa0984023fcc1617d4c3f664f27a9045958f7
1 file changed
+128
python/helpers/cloudflare_tunnel.py
new
+128
@@ -0,0 +1,128 @@
1
+import os
2
+import platform
3
+import requests
4
+import subprocess
5
+import threading
6
+from python.helpers import files
7
+
8
+class CloudflareTunnel:
9
+ def __init__(self, port: int):
10
+ self.port = port
11
+ self.bin_dir = "bin" # Relative path
12
+ self.cloudflared_path = None
13
+ self.tunnel_process = None
14
+ self.tunnel_url = None
15
+ self._stop_event = threading.Event()
16
+
17
+ def download_cloudflared(self):
18
+ """Downloads the appropriate cloudflared binary for the current system"""
19
+ # Create bin directory if it doesn't exist using files helper
20
+ os.makedirs(files.get_abs_path(self.bin_dir), exist_ok=True)
21
+
22
+ # Determine OS and architecture
23
+ system = platform.system().lower()
24
+ arch = platform.machine().lower()
25
+
26
+ # Define executable name
27
+ executable_name = "cloudflared.exe" if system == "windows" else "cloudflared"
28
+ install_path = files.get_abs_path(self.bin_dir, executable_name)
29
+
30
+ # Return if already exists
31
+ if files.exists(self.bin_dir, executable_name):
32
+ self.cloudflared_path = install_path
33
+ return install_path
34
+
35
+ # Map platform/arch to download URLs
36
+ base_url = "https://github.com/cloudflare/cloudflared/releases/latest/download/"
37
+ download_file = None
38
+
39
+ if system == "linux":
40
+ download_file = "cloudflared-linux-amd64" if arch in ["x86_64", "amd64"] else "cloudflared-linux-arm"
41
+ elif system == "darwin":
42
+ download_file = "cloudflared-darwin-amd64" if arch in ["x86_64"] else "cloudflared-darwin-arm64"
43
+ elif system == "windows":
44
+ download_file = "cloudflared-windows-amd64.exe"
45
+
46
+ if not download_file:
47
+ raise RuntimeError(f"Unsupported platform: {system} {arch}")
48
+
49
+ # Download binary
50
+ download_url = f"{base_url}{download_file}"
51
+ download_path = files.get_abs_path(self.bin_dir, download_file)
52
+
53
+ print(f"\nDownloading cloudflared from: {download_url}")
54
+ response = requests.get(download_url, stream=True)
55
+ if response.status_code == 200:
56
+ with open(download_path, "wb") as f:
57
+ for chunk in response.iter_content(chunk_size=8192):
58
+ f.write(chunk)
59
+ print(f"Downloaded to {download_path}")
60
+ else:
61
+ raise RuntimeError(f"Failed to download cloudflared: {response.status_code}")
62
+
63
+ # Rename and set permissions
64
+ if os.path.exists(install_path):
65
+ os.remove(install_path)
66
+ os.rename(download_path, install_path)
67
+
68
+ if system != "windows":
69
+ os.chmod(install_path, 0o755)
70
+
71
+ self.cloudflared_path = install_path
72
+ return install_path
73
+
74
+ def _extract_tunnel_url(self, process):
75
+ """Extracts the tunnel URL from cloudflared output"""
76
+ while not self._stop_event.is_set():
77
+ line = process.stdout.readline()
78
+ if not line:
79
+ break
80
+
81
+ if isinstance(line, bytes):
82
+ line = line.decode('utf-8')
83
+
84
+ if "trycloudflare.com" in line and "https://" in line:
85
+ start = line.find("https://")
86
+ end = line.find("trycloudflare.com") + len("trycloudflare.com")
87
+ self.tunnel_url = line[start:end].strip()
88
+ print("\n=== Cloudflare Tunnel URL ===")
89
+ print(f"URL: {self.tunnel_url}")
90
+ print("============================\n")
91
+ return
92
+
93
+ def start(self):
94
+ """Starts the cloudflare tunnel"""
95
+ if not self.cloudflared_path:
96
+ self.download_cloudflared()
97
+
98
+ print("\nStarting Cloudflare tunnel...")
99
+ # Start tunnel process
100
+ self.tunnel_process = subprocess.Popen(
101
+ [
102
+ str(self.cloudflared_path),
103
+ "tunnel",
104
+ "--url",
105
+ f"http://localhost:{self.port}"
106
+ ],
107
+ stdout=subprocess.PIPE,
108
+ stderr=subprocess.STDOUT,
109
+ bufsize=1,
110
+ universal_newlines=True
111
+ )
112
+
113
+ # Extract tunnel URL in separate thread
114
+ threading.Thread(
115
+ target=self._extract_tunnel_url,
116
+ args=(self.tunnel_process,),
117
+ daemon=True
118
+ ).start()
119
+
120
+ def stop(self):
121
+ """Stops the cloudflare tunnel"""
122
+ self._stop_event.set()
123
+ if self.tunnel_process:
124
+ print("\nStopping Cloudflare tunnel...")
125
+ self.tunnel_process.terminate()
126
+ self.tunnel_process.wait()
127
+ self.tunnel_process = None
128
+ self.tunnel_url = None
\ No newline at end of file