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