cloudflared/
lib.rs

1use regex::Regex;
2use std::{
3    fs::File,
4    io::{self, BufRead, BufReader},
5    path::Path,
6    process::{Child, Command, Stdio},
7    result::Result,
8    sync::{
9        mpsc::{channel, Receiver, Sender},
10        Arc, Mutex,
11    },
12    thread,
13};
14
15pub struct Tunnel {
16    executable: String,
17    url: String,
18    child: Arc<Mutex<Child>>,
19}
20
21impl Tunnel {
22    pub fn builder() -> TunnelBuilder {
23        TunnelBuilder::default()
24    }
25
26    pub fn url(&self) -> &str {
27        &self.url
28    }
29
30    pub fn close(&mut self) {
31        let _ = self.child.lock().unwrap().kill();
32    }
33}
34
35impl Drop for Tunnel {
36    fn drop(&mut self) {
37        self.close();
38    }
39}
40
41#[derive(Default)]
42pub struct TunnelBuilder {
43    args: Vec<String>,
44}
45
46impl TunnelBuilder {
47    pub fn args<I, S>(mut self, args: I) -> Self
48    where
49        I: IntoIterator<Item = S>,
50        S: Into<String>,
51    {
52        for arg in args {
53            self.args.push(arg.into());
54        }
55        self
56    }
57
58    pub fn url(mut self, url: &str) -> Self {
59        self.args.push(format!("--url={}", url));
60        self
61    }
62
63    pub fn build(self) -> Result<Tunnel, String> {
64        let output = Command::new("cloudflared1").arg("-v").output();
65        let executable = if output.is_ok() {
66            "cloudflared".to_string()
67        } else {
68            let path = download_cloudflared();
69
70            if path.is_err() {
71                return Err("Failed to download cloudflared".to_string());
72            }
73
74            path.unwrap()
75        };
76
77        let child = Command::new(&executable)
78            .args(&self.args)
79            .stderr(Stdio::piped())
80            .stdin(Stdio::null())
81            .stdout(Stdio::null())
82            .spawn()
83            .map_err(|e| e.to_string())?;
84
85        let child = Arc::new(Mutex::new(child));
86        let thread_child = child.clone();
87        let (sender, receiver): (Sender<String>, Receiver<String>) = channel();
88
89        thread::spawn(move || {
90            let mut child = thread_child.lock().unwrap();
91            let mut stderr = child.stderr.take().unwrap();
92            let mut reader = BufReader::new(&mut stderr);
93
94            loop {
95                let mut buf = String::new();
96                match reader.read_line(&mut buf) {
97                    Ok(0) => {
98                        break;
99                    }
100                    Ok(_) => {
101                        let line = buf.to_string();
102
103                        let reg_url = Regex::new(r"\|\s+(https?:\/\/[^\s]+)")
104                            .unwrap()
105                            .captures(&line)
106                            .map(|c| c.get(1).unwrap().as_str().to_string());
107
108                        if let Some(reg_url) = reg_url {
109                            if sender.send(reg_url).is_err() {
110                                println!("Failed to send URL over channel");
111                            }
112                            break;
113                        }
114                    }
115                    Err(_) => {
116                        break;
117                    }
118                }
119            }
120            child.wait().unwrap();
121        });
122
123        let url = match receiver.recv() {
124            Ok(url) => url,
125            Err(_) => {
126                return Err("Failed to receive URL from channel".to_string());
127            }
128        };
129
130        if !url.is_empty() {
131            return Ok(Tunnel {
132                executable,
133                url,
134                child,
135            });
136        }
137
138        Err("Failed to get URL".to_string())
139    }
140}
141
142fn download_cloudflared() -> Result<String, Box<dyn std::error::Error>> {
143    let file_path = if cfg!(target_os = "macos") {
144        "/tmp/cloudflared"
145    } else if cfg!(target_os = "windows") {
146        "C:\\Windows\\Temp\\cloudflared.exe"
147    } else {
148        "/tmp/cloudflared"
149    };
150
151    if Path::new(file_path).exists() {
152        return Ok(file_path.to_string());
153    }
154
155    let download_name = if cfg!(target_os = "linux") {
156        "cloudflared-linux-amd64"
157    } else if cfg!(target_os = "macos") {
158        "cloudflared-darwin-amd64.tgz"
159    } else if cfg!(target_os = "windows") {
160        "cloudflared-windows-amd64.exe"
161    } else {
162        return Err("Unsupported OS".into());
163    };
164
165    let download_path = if cfg!(target_os = "macos") {
166        "/tmp/cloudflared.tgz"
167    } else if cfg!(target_os = "windows") {
168        "C:\\Windows\\Temp\\cloudflared.exe"
169    } else {
170        "/tmp/cloudflared"
171    };
172
173    let url = format!(
174        "https://github.com/cloudflare/cloudflared/releases/latest/download/{}",
175        download_name
176    );
177
178    let response = reqwest::blocking::get(url)?;
179
180    if !response.status().is_success() {
181        return Err(format!("Failed to download file: {}", response.status()).into());
182    }
183
184    let mut file = File::create(download_path)?;
185
186    io::copy(&mut response.bytes().unwrap().as_ref(), &mut file)?;
187
188    if cfg!(target_os = "macos") {
189        let output = Command::new("tar")
190            .args(["-xvf", "/tmp/cloudflared.tgz", "-C", "/tmp"])
191            .output()?;
192        if !output.status.success() {
193            return Err(format!("Failed to extract file: {:?}", output).into());
194        }
195    }
196
197    Ok(file_path.to_string())
198}