1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::{Arc, Once};
3use std::time::Duration;
4
5static CRYPTO_PROVIDER_INIT: Once = Once::new();
6
7fn ensure_crypto_provider() {
12 CRYPTO_PROVIDER_INIT.call_once(|| {
13 let _ = rustls::crypto::ring::default_provider().install_default();
15 });
16}
17
18use crate::api::custom_geo::CustomGeoApi;
19use crate::api::inbounds::InboundsApi;
20use crate::api::server::ServerApi;
21use crate::api::settings::SettingsApi;
22use crate::api::xray::XrayApi;
23use crate::config::ClientConfig;
24use crate::error::Result;
25use crate::models::common::ApiResponse;
26use crate::Error;
27
28pub(crate) async fn read_api_response<T: serde::de::DeserializeOwned>(
37 resp: reqwest::Response,
38) -> Result<ApiResponse<T>> {
39 let status = resp.status();
40 let path = resp.url().path().to_string();
41 let bytes = resp.bytes().await?;
42 if status == reqwest::StatusCode::NOT_FOUND {
43 return Err(Error::EndpointNotFound(path));
44 }
45 if bytes.is_empty() {
46 return Err(Error::Api(format!("empty response body (HTTP {})", status)));
47 }
48 serde_json::from_slice::<ApiResponse<T>>(&bytes).map_err(|e| {
49 if status.is_success() {
50 Error::Json(e)
51 } else {
52 let snippet: String = String::from_utf8_lossy(&bytes).chars().take(200).collect();
55 Error::Api(format!(
56 "HTTP {} — non-JSON body: {}",
57 status,
58 snippet.trim()
59 ))
60 }
61 })
62}
63
64pub(crate) struct ClientInner {
65 pub http: reqwest::Client,
66 pub base_url: String,
67 pub authenticated: AtomicBool,
68}
69
70#[derive(Clone)]
71pub struct Client {
72 pub(crate) inner: Arc<ClientInner>,
73}
74
75impl Client {
76 pub fn new(config: ClientConfig) -> Self {
77 ensure_crypto_provider();
78 let mut builder = reqwest::Client::builder()
79 .cookie_store(true)
80 .danger_accept_invalid_certs(config.accept_invalid_certs)
81 .timeout(Duration::from_secs(config.timeout_secs));
82
83 if let Some(proxy_url) = &config.proxy {
84 let mut proxy = reqwest::Proxy::all(proxy_url.as_str())
88 .expect("proxy url validated at config build time");
89 if let (Some(u), Some(p)) = (&config.proxy_username, &config.proxy_password) {
90 proxy = proxy.basic_auth(u, p);
91 }
92 builder = builder.proxy(proxy);
93 }
94
95 let http = builder.build().expect("failed to build reqwest client");
96
97 Client {
98 inner: Arc::new(ClientInner {
99 http,
100 base_url: config.base_url(),
101 authenticated: AtomicBool::new(false),
102 }),
103 }
104 }
105
106 pub(crate) fn url(&self, path: &str) -> String {
107 format!("{}{}", self.inner.base_url, path)
108 }
109
110 pub(crate) fn require_auth(&self) -> Result<()> {
111 if self.inner.authenticated.load(Ordering::Relaxed) {
112 Ok(())
113 } else {
114 Err(Error::NotAuthenticated)
115 }
116 }
117
118 pub async fn login(&self, username: &str, password: &str) -> Result<()> {
119 self.login_inner(username, password, None).await
120 }
121
122 pub async fn login_2fa(&self, username: &str, password: &str, code: &str) -> Result<()> {
123 self.login_inner(username, password, Some(code)).await
124 }
125
126 async fn login_inner(
127 &self,
128 username: &str,
129 password: &str,
130 two_factor: Option<&str>,
131 ) -> Result<()> {
132 let mut params = vec![
133 ("username", username.to_string()),
134 ("password", password.to_string()),
135 ];
136 if let Some(code) = two_factor {
137 params.push(("twoFactorCode", code.to_string()));
138 }
139
140 let resp = self
141 .inner
142 .http
143 .post(self.url("login"))
144 .form(¶ms)
145 .send()
146 .await?
147 .json::<ApiResponse<serde_json::Value>>()
148 .await?;
149
150 if resp.success {
151 self.inner.authenticated.store(true, Ordering::Relaxed);
152 Ok(())
153 } else {
154 Err(Error::Auth(resp.msg))
155 }
156 }
157
158 pub async fn logout(&self) -> Result<()> {
159 let _ = self.inner.http.get(self.url("logout")).send().await?;
160 self.inner.authenticated.store(false, Ordering::Relaxed);
161 Ok(())
162 }
163
164 pub async fn is_two_factor_enabled(&self) -> Result<bool> {
165 let resp = self
166 .inner
167 .http
168 .post(self.url("getTwoFactorEnable"))
169 .send()
170 .await?
171 .json::<ApiResponse<bool>>()
172 .await?;
173 resp.into_result().map(|v| v.unwrap_or(false))
174 }
175
176 pub async fn backup_to_tgbot(&self) -> Result<()> {
177 self.require_auth()?;
178 self.inner
179 .http
180 .get(self.url("panel/api/backuptotgbot"))
181 .send()
182 .await?;
183 Ok(())
184 }
185
186 pub fn inbounds(&self) -> InboundsApi<'_> {
187 InboundsApi { client: self }
188 }
189
190 pub fn server(&self) -> ServerApi<'_> {
191 ServerApi { client: self }
192 }
193
194 pub fn settings(&self) -> SettingsApi<'_> {
195 SettingsApi { client: self }
196 }
197
198 pub fn xray(&self) -> XrayApi<'_> {
199 XrayApi { client: self }
200 }
201
202 pub fn custom_geo(&self) -> CustomGeoApi<'_> {
203 CustomGeoApi { client: self }
204 }
205
206 pub(crate) async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
207 self.require_auth()?;
208 let raw = self.inner.http.get(self.url(path)).send().await?;
209 let resp = read_api_response::<T>(raw).await?;
210 resp.into_result()
211 .and_then(|v| v.ok_or_else(|| Error::Api("empty response".into())))
212 }
213
214 pub(crate) async fn post<B, T>(&self, path: &str, body: &B) -> Result<T>
215 where
216 B: serde::Serialize,
217 T: serde::de::DeserializeOwned,
218 {
219 self.require_auth()?;
220 let raw = self
221 .inner
222 .http
223 .post(self.url(path))
224 .json(body)
225 .send()
226 .await?;
227 let resp = read_api_response::<T>(raw).await?;
228 resp.into_result()
229 .and_then(|v| v.ok_or_else(|| Error::Api("empty response".into())))
230 }
231
232 pub(crate) async fn post_empty<B>(&self, path: &str, body: &B) -> Result<()>
233 where
234 B: serde::Serialize,
235 {
236 self.require_auth()?;
237 let raw = self
238 .inner
239 .http
240 .post(self.url(path))
241 .json(body)
242 .send()
243 .await?;
244 let resp = read_api_response::<serde_json::Value>(raw).await?;
245 if resp.success {
246 Ok(())
247 } else {
248 Err(Error::Api(resp.msg))
249 }
250 }
251
252 pub(crate) async fn post_form_empty(&self, path: &str, params: &[(&str, &str)]) -> Result<()> {
253 self.require_auth()?;
254 let raw = self
255 .inner
256 .http
257 .post(self.url(path))
258 .form(params)
259 .send()
260 .await?;
261 let resp = read_api_response::<serde_json::Value>(raw).await?;
262 if resp.success {
263 Ok(())
264 } else {
265 Err(Error::Api(resp.msg))
266 }
267 }
268
269 pub(crate) async fn get_bytes(&self, path: &str) -> Result<Vec<u8>> {
270 self.require_auth()?;
271 let bytes = self
272 .inner
273 .http
274 .get(self.url(path))
275 .send()
276 .await?
277 .bytes()
278 .await?;
279 Ok(bytes.to_vec())
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use wiremock::matchers::{method, path};
287 use wiremock::{Mock, MockServer, ResponseTemplate};
288
289 async fn mock_client(server: &MockServer) -> Client {
290 let config = ClientConfig::builder()
291 .host("127.0.0.1")
292 .port(server.address().port())
293 .build()
294 .unwrap();
295 Client::new(config)
296 }
297
298 #[tokio::test]
299 async fn login_sets_authenticated() {
300 let server = MockServer::start().await;
301 Mock::given(method("POST"))
302 .and(path("/login"))
303 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
304 "success": true, "msg": "ok", "obj": null
305 })))
306 .mount(&server)
307 .await;
308
309 let client = mock_client(&server).await;
310 assert!(!client.inner.authenticated.load(Ordering::Relaxed));
311 client.login("admin", "pass").await.unwrap();
312 assert!(client.inner.authenticated.load(Ordering::Relaxed));
313 }
314
315 #[tokio::test]
316 async fn login_failure_returns_auth_error() {
317 let server = MockServer::start().await;
318 Mock::given(method("POST"))
319 .and(path("/login"))
320 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
321 "success": false, "msg": "wrong username or password", "obj": null
322 })))
323 .mount(&server)
324 .await;
325
326 let client = mock_client(&server).await;
327 let err = client.login("admin", "wrong").await.unwrap_err();
328 assert!(matches!(err, Error::Auth(_)));
329 }
330
331 #[tokio::test]
332 async fn http_404_returns_endpoint_not_found() {
333 let server = MockServer::start().await;
334 Mock::given(method("POST"))
335 .and(path("/login"))
336 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
337 "success": true, "msg": "", "obj": null
338 })))
339 .mount(&server)
340 .await;
341 Mock::given(method("GET"))
342 .and(path("/panel/api/missing"))
343 .respond_with(ResponseTemplate::new(404).set_body_string(""))
344 .mount(&server)
345 .await;
346
347 let client = mock_client(&server).await;
348 client.login("admin", "p").await.unwrap();
349
350 let err: Result<serde_json::Value> = client.get("panel/api/missing").await;
351 match err {
352 Err(Error::EndpointNotFound(p)) => assert!(p.contains("missing")),
353 other => panic!("expected EndpointNotFound, got {:?}", other.is_ok()),
354 }
355 }
356
357 #[tokio::test]
358 async fn http_500_with_html_surfaces_status_in_api_error() {
359 let server = MockServer::start().await;
360 Mock::given(method("POST"))
361 .and(path("/login"))
362 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
363 "success": true, "msg": "", "obj": null
364 })))
365 .mount(&server)
366 .await;
367 Mock::given(method("GET"))
368 .and(path("/panel/api/boom"))
369 .respond_with(
370 ResponseTemplate::new(500).set_body_string("<html>Internal Server Error</html>"),
371 )
372 .mount(&server)
373 .await;
374
375 let client = mock_client(&server).await;
376 client.login("admin", "p").await.unwrap();
377
378 let err: Result<serde_json::Value> = client.get("panel/api/boom").await;
379 let msg = err.unwrap_err().to_string();
380 assert!(msg.contains("HTTP 500"), "msg = {}", msg);
381 }
382
383 #[tokio::test]
384 async fn empty_body_returns_api_error_not_panic() {
385 let server = MockServer::start().await;
386 Mock::given(method("POST"))
387 .and(path("/login"))
388 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
389 "success": true, "msg": "", "obj": null
390 })))
391 .mount(&server)
392 .await;
393 Mock::given(method("GET"))
394 .and(path("/panel/api/empty"))
395 .respond_with(ResponseTemplate::new(200).set_body_string(""))
396 .mount(&server)
397 .await;
398
399 let client = mock_client(&server).await;
400 client.login("admin", "p").await.unwrap();
401
402 let err: Result<serde_json::Value> = client.get("panel/api/empty").await;
403 let msg = err.unwrap_err().to_string();
404 assert!(msg.contains("empty response body"), "msg = {}", msg);
405 }
406
407 #[tokio::test]
408 async fn require_auth_fails_when_not_logged_in() {
409 let server = MockServer::start().await;
410 let client = mock_client(&server).await;
411 assert!(matches!(
412 client.require_auth(),
413 Err(Error::NotAuthenticated)
414 ));
415 }
416}