Skip to main content

threexui_rs/api/
server.rs

1use crate::models::server::{
2    CpuHistoryPoint, EchCert, Mldsa65Keys, Mlkem768Keys, ServerStatus, UuidResponse,
3    VlessEncResult, X25519Cert,
4};
5use crate::{Client, Result};
6
7pub struct ServerApi<'a> {
8    pub(crate) client: &'a Client,
9}
10
11impl<'a> ServerApi<'a> {
12    pub async fn status(&self) -> Result<ServerStatus> {
13        self.client.get("panel/api/server/status").await
14    }
15
16    pub async fn cpu_history(&self, bucket: u32) -> Result<Vec<CpuHistoryPoint>> {
17        self.client
18            .get(&format!("panel/api/server/cpuHistory/{}", bucket))
19            .await
20    }
21
22    pub async fn xray_versions(&self) -> Result<Vec<String>> {
23        self.client.get("panel/api/server/getXrayVersion").await
24    }
25
26    pub async fn config_json(&self) -> Result<serde_json::Value> {
27        self.client.get("panel/api/server/getConfigJson").await
28    }
29
30    pub async fn download_db(&self) -> Result<Vec<u8>> {
31        self.client.get_bytes("panel/api/server/getDb").await
32    }
33
34    pub async fn new_uuid(&self) -> Result<UuidResponse> {
35        self.client.get("panel/api/server/getNewUUID").await
36    }
37
38    pub async fn new_x25519_cert(&self) -> Result<X25519Cert> {
39        self.client.get("panel/api/server/getNewX25519Cert").await
40    }
41
42    pub async fn new_mldsa65(&self) -> Result<Mldsa65Keys> {
43        self.client.get("panel/api/server/getNewmldsa65").await
44    }
45
46    pub async fn new_mlkem768(&self) -> Result<Mlkem768Keys> {
47        self.client.get("panel/api/server/getNewmlkem768").await
48    }
49
50    pub async fn new_vless_enc(&self) -> Result<VlessEncResult> {
51        self.client.get("panel/api/server/getNewVlessEnc").await
52    }
53
54    pub async fn stop_xray(&self) -> Result<()> {
55        self.client
56            .post_empty("panel/api/server/stopXrayService", &serde_json::json!({}))
57            .await
58    }
59
60    pub async fn restart_xray(&self) -> Result<()> {
61        self.client
62            .post_empty(
63                "panel/api/server/restartXrayService",
64                &serde_json::json!({}),
65            )
66            .await
67    }
68
69    pub async fn install_xray(&self, version: &str) -> Result<()> {
70        self.client
71            .post_empty(
72                &format!("panel/api/server/installXray/{}", version),
73                &serde_json::json!({}),
74            )
75            .await
76    }
77
78    pub async fn update_geofile(&self, filename: Option<&str>) -> Result<()> {
79        let path = match filename {
80            Some(name) => format!("panel/api/server/updateGeofile/{}", name),
81            None => "panel/api/server/updateGeofile".to_string(),
82        };
83        self.client.post_empty(&path, &serde_json::json!({})).await
84    }
85
86    pub async fn logs(&self, count: u32, level: &str, syslog: &str) -> Result<Vec<String>> {
87        let params = [("level", level), ("syslog", syslog)];
88        let path = format!("panel/api/server/logs/{}", count);
89        self.client.require_auth()?;
90        let raw = self
91            .client
92            .inner
93            .http
94            .post(self.client.url(&path))
95            .form(&params)
96            .send()
97            .await?;
98        let resp = crate::client::read_api_response::<Vec<String>>(raw).await?;
99        resp.into_result().map(|v| v.unwrap_or_default())
100    }
101
102    pub async fn xray_logs(
103        &self,
104        count: u32,
105        filter: &str,
106        show_direct: bool,
107        show_blocked: bool,
108        show_proxy: bool,
109    ) -> Result<Vec<String>> {
110        let params = [
111            ("filter", filter.to_string()),
112            ("showDirect", show_direct.to_string()),
113            ("showBlocked", show_blocked.to_string()),
114            ("showProxy", show_proxy.to_string()),
115        ];
116        let path = format!("panel/api/server/xraylogs/{}", count);
117        self.client.require_auth()?;
118        let raw = self
119            .client
120            .inner
121            .http
122            .post(self.client.url(&path))
123            .form(&params)
124            .send()
125            .await?;
126        let resp = crate::client::read_api_response::<Vec<String>>(raw).await?;
127        resp.into_result().map(|v| v.unwrap_or_default())
128    }
129
130    pub async fn import_db(&self, data: Vec<u8>) -> Result<()> {
131        self.client.require_auth()?;
132        let part = reqwest::multipart::Part::bytes(data).file_name("x-ui.db");
133        let form = reqwest::multipart::Form::new().part("db", part);
134        let raw = self
135            .client
136            .inner
137            .http
138            .post(self.client.url("panel/api/server/importDB"))
139            .multipart(form)
140            .send()
141            .await?;
142        let resp = crate::client::read_api_response::<serde_json::Value>(raw).await?;
143        if resp.success {
144            Ok(())
145        } else {
146            Err(crate::Error::Api(resp.msg))
147        }
148    }
149
150    pub async fn new_ech_cert(&self, sni: &str) -> Result<EchCert> {
151        let params = [("sni", sni)];
152        self.client.require_auth()?;
153        let raw = self
154            .client
155            .inner
156            .http
157            .post(self.client.url("panel/api/server/getNewEchCert"))
158            .form(&params)
159            .send()
160            .await?;
161        let resp = crate::client::read_api_response::<EchCert>(raw).await?;
162        resp.into_result()
163            .and_then(|v| v.ok_or_else(|| crate::Error::Api("empty response".into())))
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::config::ClientConfig;
171    use wiremock::matchers::{method, path};
172    use wiremock::{Mock, MockServer, ResponseTemplate};
173
174    async fn auth_client(server: &MockServer) -> Client {
175        Mock::given(method("POST"))
176            .and(path("/login"))
177            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
178                "success": true, "msg": "", "obj": null
179            })))
180            .mount(server)
181            .await;
182        let config = ClientConfig::builder()
183            .host("127.0.0.1")
184            .port(server.address().port())
185            .build()
186            .unwrap();
187        let client = Client::new(config);
188        client.login("admin", "pass").await.unwrap();
189        client
190    }
191
192    #[tokio::test]
193    async fn status_returns_server_status() {
194        let server = MockServer::start().await;
195        Mock::given(method("GET"))
196            .and(path("/panel/api/server/status"))
197            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
198                "success": true, "msg": "", "obj": {
199                    "cpu":5.0,"cpuCores":4,"logicalPro":8,"cpuSpeedMhz":3200.0,
200                    "mem":{"current":1024,"total":8192},
201                    "swap":{"current":0,"total":0},
202                    "disk":{"current":10240,"total":102400},
203                    "xray":{"state":"running","errorMsg":"","version":"1.8.0"},
204                    "uptime":7200,"loads":[0.1,0.2,0.3],
205                    "tcpCount":5,"udpCount":2,
206                    "netIO":{"up":512,"down":1024},
207                    "netTraffic":{"sent":51200,"recv":102400},
208                    "publicIP":{"ipv4":"1.2.3.4","ipv6":"::1"},
209                    "appStats":{"threads":8,"mem":32768,"uptime":7200}
210                }
211            })))
212            .mount(&server)
213            .await;
214
215        let client = auth_client(&server).await;
216        let status = client.server().status().await.unwrap();
217        assert_eq!(status.cpu, 5.0);
218        assert_eq!(status.xray.state, "running");
219    }
220
221    #[tokio::test]
222    async fn new_uuid_returns_uuid() {
223        let server = MockServer::start().await;
224        Mock::given(method("GET"))
225            .and(path("/panel/api/server/getNewUUID"))
226            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
227                "success": true, "msg": "", "obj": {"uuid": "abc-123"}
228            })))
229            .mount(&server)
230            .await;
231
232        let client = auth_client(&server).await;
233        let resp = client.server().new_uuid().await.unwrap();
234        assert_eq!(resp.uuid, "abc-123");
235    }
236
237    #[tokio::test]
238    async fn restart_xray_succeeds() {
239        let server = MockServer::start().await;
240        Mock::given(method("POST"))
241            .and(path("/panel/api/server/restartXrayService"))
242            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
243                "success": true, "msg": "restarted", "obj": null
244            })))
245            .mount(&server)
246            .await;
247
248        let client = auth_client(&server).await;
249        client.server().restart_xray().await.unwrap();
250    }
251}