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(¶ms)
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(¶ms)
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(¶ms)
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}