clashctl_core/
api.rs

1use std::io::{BufRead, BufReader, Read};
2use std::marker::PhantomData;
3use std::time::Duration;
4
5use log::{debug, trace};
6
7use serde::de::DeserializeOwned;
8use serde_json::{from_str, json};
9use ureq::{Agent, Request};
10use url::Url;
11
12use crate::model::{Config, Connections, Delay, Log, Proxies, Proxy, Rules, Traffic, Version};
13use crate::{Error, Result};
14
15trait Convert<T: DeserializeOwned> {
16    fn convert(self) -> Result<T>;
17}
18
19impl<T: DeserializeOwned> Convert<T> for String {
20    fn convert(self) -> Result<T> {
21        from_str(&self).map_err(Error::BadResponseFormat)
22    }
23}
24
25#[derive(Debug, Clone)]
26pub struct ClashBuilder {
27    url: Url,
28    secret: Option<String>,
29    timeout: Option<Duration>,
30}
31
32impl ClashBuilder {
33    pub fn new<S: Into<String>>(url: S) -> Result<Self> {
34        let mut url_str = url.into();
35        // Handle trailling slash
36        if !url_str.ends_with('/') {
37            url_str += "/";
38        };
39        let url = Url::parse(&url_str).map_err(|_| Error::UrlParseError)?;
40        Ok(Self {
41            url,
42            secret: None,
43            timeout: None,
44        })
45    }
46
47    pub fn secret(mut self, secret: Option<String>) -> Self {
48        self.secret = secret;
49        self
50    }
51
52    pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
53        self.timeout = timeout;
54        self
55    }
56
57    pub fn build(self) -> Clash {
58        let mut clash = Clash::new(self.url);
59        clash.secret = self.secret;
60        clash.timeout = self.timeout;
61        clash
62    }
63}
64
65/// # Clash API
66///
67/// Use struct `Clash` for interacting with Clash RESTful API.
68/// For more information, check <https://github.com/Dreamacro/clash/wiki/external-controller-API-reference###Proxies>,
69/// or maybe just read source code of clash
70#[derive(Debug, Clone)]
71pub struct Clash {
72    url: Url,
73    secret: Option<String>,
74    timeout: Option<Duration>,
75    agent: Agent,
76}
77
78impl Clash {
79    pub fn builder<S: Into<String>>(url: S) -> Result<ClashBuilder> {
80        ClashBuilder::new(url)
81    }
82
83    pub fn new(url: Url) -> Self {
84        debug!("Url of clash RESTful API: {}", url);
85        Self {
86            url,
87            secret: None,
88            timeout: None,
89            agent: Agent::new(),
90        }
91    }
92
93    fn build_request(&self, endpoint: &str, method: &str) -> Result<Request> {
94        let url = self.url.join(endpoint).map_err(|_| Error::UrlParseError)?;
95        let mut req = self.agent.request_url(method, &url);
96
97        if let Some(timeout) = self.timeout {
98            req = req.timeout(timeout)
99        }
100
101        if let Some(ref secret) = self.secret {
102            req = req.set("Authorization", &format!("Bearer {}", secret))
103        }
104
105        Ok(req)
106    }
107
108    fn build_request_without_timeout(&self, endpoint: &str, method: &str) -> Result<Request> {
109        let url = self.url.join(endpoint).map_err(|_| Error::UrlParseError)?;
110        let mut req = self.agent.request_url(method, &url);
111
112        if let Some(ref secret) = self.secret {
113            req = req.set("Authorization", &format!("Bearer {}", secret))
114        }
115
116        Ok(req)
117    }
118
119    /// Send a oneshot request to the specific endpoint with method, with body
120    pub fn oneshot_req_with_body(
121        &self,
122        endpoint: &str,
123        method: &str,
124        body: Option<String>,
125    ) -> Result<String> {
126        trace!("Body: {:#?}", body);
127        let resp = if let Some(body) = body {
128            self.build_request(endpoint, method)?.send_string(&body)?
129        } else {
130            self.build_request(endpoint, method)?.call()?
131        };
132
133        if resp.status() >= 400 {
134            return Err(Error::FailedResponse(resp.status()));
135        }
136
137        let text = resp.into_string().map_err(|_| Error::BadResponseEncoding)?;
138        trace!("Received response: {}", text);
139
140        Ok(text)
141    }
142
143    /// Send a oneshot request to the specific endpoint with method, without body
144    pub fn oneshot_req(&self, endpoint: &str, method: &str) -> Result<String> {
145        self.oneshot_req_with_body(endpoint, method, None)
146    }
147
148    /// Send a longhaul request to the specific endpoint with method,
149    /// Underlying is an http stream with chunked-encoding.
150    ///
151    /// Use [`LongHaul::next_item`], [`LongHaul::next_raw`] or [`Iterator::next`] to retreive data
152    ///
153    /// # Examplel
154    ///
155    /// ```rust
156    /// # use clashctl_core::{ Clash, model::Traffic }; use std::env;
157    /// # fn main() {
158    /// # let clash = Clash::builder(env::var("PROXY_ADDR").unwrap()).unwrap().build();
159    /// let traffics = clash.longhaul_req::<Traffic>("traffic", "GET").expect("connect failed");
160    ///
161    /// // LongHaul implements `Iterator` so you can use iterator combinators
162    /// for traffic in traffics.take(3) {
163    ///     println!("{:#?}", traffic)
164    /// }
165    /// # }
166    /// ```
167    pub fn longhaul_req<T: DeserializeOwned>(
168        &self,
169        endpoint: &str,
170        method: &str,
171    ) -> Result<LongHaul<T>> {
172        let resp = self
173            .build_request_without_timeout(endpoint, method)?
174            .call()?;
175
176        if resp.status() >= 400 {
177            return Err(Error::FailedResponse(resp.status()));
178        }
179
180        Ok(LongHaul::new(Box::new(resp.into_reader())))
181    }
182
183    /// Helper function for method `GET`
184    pub fn get<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T> {
185        self.oneshot_req(endpoint, "GET").and_then(Convert::convert)
186    }
187
188    /// Helper function for method `DELETE`
189    pub fn delete(&self, endpoint: &str) -> Result<()> {
190        self.oneshot_req(endpoint, "DELETE").map(|_| ())
191    }
192
193    /// Helper function for method `PUT`
194    pub fn put<T: DeserializeOwned>(&self, endpoint: &str, body: Option<String>) -> Result<T> {
195        self.oneshot_req_with_body(endpoint, "PUT", body)
196            .and_then(Convert::convert)
197    }
198
199    /// Get clash version
200    pub fn get_version(&self) -> Result<Version> {
201        self.get("version")
202    }
203
204    /// Get base configs
205    pub fn get_configs(&self) -> Result<Config> {
206        self.get("configs")
207    }
208
209    /// Reloading base configs.
210    ///
211    /// - `force`: will change ports etc.,
212    /// - `path`: the absolute path to config file
213    ///
214    /// This will **NOT** affect `external-controller` & `secret`
215    pub fn reload_configs(&self, force: bool, path: &str) -> Result<()> {
216        let body = json!({ "path": path }).to_string();
217        debug!("{}", body);
218        self.put::<String>(if force { "configs?force" } else { "configs" }, Some(body))
219            .map(|_| ())
220    }
221
222    /// Get proxies information
223    pub fn get_proxies(&self) -> Result<Proxies> {
224        self.get("proxies")
225    }
226
227    /// Get rules information
228    pub fn get_rules(&self) -> Result<Rules> {
229        self.get("rules")
230    }
231
232    /// Get specific proxy information
233    pub fn get_proxy(&self, proxy: &str) -> Result<Proxy> {
234        self.get(&format!("proxies/{}", proxy))
235    }
236
237    /// Get connections information
238    pub fn get_connections(&self) -> Result<Connections> {
239        self.get("connections")
240    }
241
242    /// Close all connections
243    pub fn close_connections(&self) -> Result<()> {
244        self.delete("connections")
245    }
246
247    /// Close specific connection
248    pub fn close_one_connection(&self, id: &str) -> Result<()> {
249        self.delete(&format!("connections/{}", id))
250    }
251
252    /// Get real-time traffic data
253    ///
254    /// **Note**: This is a longhaul request, which will last forever until interrupted or disconnected.
255    ///
256    /// See [`longhaul_req`] for more information
257    ///
258    /// [`longhaul_req`]: Clash::longhaul_req
259    pub fn get_traffic(&self) -> Result<LongHaul<Traffic>> {
260        self.longhaul_req("traffic", "GET")
261    }
262
263    /// Get real-time logs
264    ///
265    /// **Note**: This is a longhaul request, which will last forever until interrupted or disconnected.
266    ///
267    /// See [`longhaul_req`] for more information
268    ///
269    /// [`longhaul_req`]: Clash::longhaul_req
270    pub fn get_log(&self) -> Result<LongHaul<Log>> {
271        self.longhaul_req("logs", "GET")
272    }
273
274    /// Get specific proxy delay test information
275    pub fn get_proxy_delay(&self, proxy: &str, test_url: &str, timeout: u64) -> Result<Delay> {
276        use urlencoding::encode as e;
277        let (proxy, test_url) = (e(proxy), e(test_url));
278        self.get(&format!(
279            "proxies/{}/delay?url={}&timeout={}",
280            proxy, test_url, timeout
281        ))
282    }
283
284    /// Select specific proxy
285    pub fn set_proxygroup_selected(&self, group: &str, proxy: &str) -> Result<()> {
286        let body = format!("{{\"name\":\"{}\"}}", proxy);
287        self.oneshot_req_with_body(&format!("proxies/{}", group), "PUT", Some(body))?;
288        Ok(())
289    }
290}
291
292pub struct LongHaul<T: DeserializeOwned> {
293    reader: BufReader<Box<dyn Read + Send>>,
294    ty: PhantomData<T>,
295}
296
297impl<T: DeserializeOwned> LongHaul<T> {
298    pub fn new(reader: Box<dyn Read + Send>) -> Self {
299        Self {
300            reader: BufReader::new(reader),
301            ty: PhantomData,
302        }
303    }
304
305    pub fn next_item(&mut self) -> Option<Result<T>> {
306        Some(self.next_raw()?.and_then(Convert::convert))
307    }
308
309    pub fn next_raw(&mut self) -> Option<Result<String>> {
310        let mut buf = String::with_capacity(30);
311        match self.reader.read_line(&mut buf) {
312            Ok(0) => None,
313            Ok(_) => Some(Ok(buf)),
314            Err(e) => Some(Err(Error::Other(format!("{:}", e)))),
315        }
316    }
317}
318
319impl<T: DeserializeOwned> Iterator for LongHaul<T> {
320    type Item = Result<T>;
321    fn next(&mut self) -> Option<Self::Item> {
322        self.next_item()
323    }
324}