1use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
2use reqwest::{Method, header};
3use serde::{Serialize, de::DeserializeOwned};
4use serde_json::Value;
5
6use super::models::{
7 AmneziaWgLogs, ClientIpRecord, DatabaseFile, EchKeyPair, Fail2banStatus, HistoryBucket,
8 LegacyCpuPoint, MetricPoint, Mldsa65KeyPair, Mlkem768KeyPair, NodeSummary, PanelLogRequest,
9 PanelUpdateInfo, PanelUpdateRun, PanelUpdateStatus, RealityScanRequest, RealityScanResult,
10 ServerStatus, SystemMetric, VlessEncryptionOptions, WebCertificateFiles, X25519KeyPair,
11 XrayConfig, XrayLogEntry, XrayLogRequest, XrayMetric, XrayMetricsState, XrayObservatoryEntry,
12};
13use crate::{Client, Error, Result, client::AuthenticationScope, response::ApiResponse};
14
15const ROOT: &str = "panel/api/server";
16
17#[derive(Clone, Copy, Debug)]
19pub struct ServerApi<'client> {
20 client: &'client Client,
21}
22
23impl<'client> ServerApi<'client> {
24 pub(crate) const fn new(client: &'client Client) -> Self {
25 Self { client }
26 }
27
28 pub async fn status(self) -> Result<ServerStatus> {
34 self.get_object("status").await
35 }
36
37 pub async fn legacy_cpu_history(self, bucket: HistoryBucket) -> Result<Vec<LegacyCpuPoint>> {
45 self.get_object(&format!("cpuHistory/{}", bucket.seconds()))
46 .await
47 }
48
49 pub async fn system_history(
55 self,
56 metric: SystemMetric,
57 bucket: HistoryBucket,
58 ) -> Result<Vec<MetricPoint>> {
59 self.get_object(&format!("history/{}/{}", metric.as_str(), bucket.seconds()))
60 .await
61 }
62
63 pub async fn xray_metrics_state(self) -> Result<XrayMetricsState> {
69 self.get_object("xrayMetricsState").await
70 }
71
72 pub async fn xray_metrics_history(
78 self,
79 metric: XrayMetric,
80 bucket: HistoryBucket,
81 ) -> Result<Vec<MetricPoint>> {
82 self.get_object(&format!(
83 "xrayMetricsHistory/{}/{}",
84 metric.as_str(),
85 bucket.seconds()
86 ))
87 .await
88 }
89
90 pub async fn xray_observatory(self) -> Result<Vec<XrayObservatoryEntry>> {
96 self.get_object("xrayObservatory").await
97 }
98
99 pub async fn xray_observatory_history(
107 self,
108 tag: &str,
109 bucket: HistoryBucket,
110 ) -> Result<Vec<MetricPoint>> {
111 self.get_object(&format!(
112 "xrayObservatoryHistory/{}/{}",
113 segment(tag),
114 bucket.seconds()
115 ))
116 .await
117 }
118
119 pub async fn xray_versions(self) -> Result<Vec<String>> {
125 self.get_object("getXrayVersion").await
126 }
127
128 pub async fn panel_update_info(self) -> Result<PanelUpdateInfo> {
134 self.get_object("getPanelUpdateInfo").await
135 }
136
137 pub async fn panel_update_status(self) -> Result<PanelUpdateStatus> {
143 self.get_object("getUpdateStatus").await
144 }
145
146 pub async fn xray_config(self) -> Result<XrayConfig> {
155 self.get_object("getConfigJson").await
156 }
157
158 pub async fn download_database(self) -> Result<DatabaseFile> {
164 self.download("getDb").await
165 }
166
167 pub async fn download_migration(self) -> Result<DatabaseFile> {
173 self.download("getMigration").await
174 }
175
176 pub async fn import_database(self, filename: &str, database_bytes: &[u8]) -> Result<()> {
185 let path = format!("{ROOT}/importDB");
186 self.client
187 .execute_multipart::<Value>(
188 Method::POST,
189 &path,
190 "db",
191 filename,
192 database_bytes,
193 AuthenticationScope::PanelApi,
194 )
195 .await?;
196 Ok(())
197 }
198
199 pub async fn generate_uuid(self) -> Result<String> {
205 #[derive(serde::Deserialize)]
206 struct UuidObject {
207 uuid: String,
208 }
209
210 Ok(self.get_object::<UuidObject>("getNewUUID").await?.uuid)
211 }
212
213 pub async fn web_certificate_files(self) -> Result<WebCertificateFiles> {
219 self.get_object("getWebCertFiles").await
220 }
221
222 pub async fn descendants(self) -> Result<Vec<NodeSummary>> {
228 self.get_object("descendants").await
229 }
230
231 pub async fn generate_x25519(self) -> Result<X25519KeyPair> {
237 self.get_object("getNewX25519Cert").await
238 }
239
240 pub async fn generate_mldsa65(self) -> Result<Mldsa65KeyPair> {
246 self.get_object("getNewmldsa65").await
247 }
248
249 pub async fn generate_mlkem768(self) -> Result<Mlkem768KeyPair> {
255 self.get_object("getNewmlkem768").await
256 }
257
258 pub async fn generate_vless_encryption(self) -> Result<VlessEncryptionOptions> {
264 self.get_object("getNewVlessEnc").await
265 }
266
267 pub async fn generate_ech(self, sni: &str) -> Result<EchKeyPair> {
273 #[derive(Serialize)]
274 struct Form<'a> {
275 sni: &'a str,
276 }
277
278 self.post_form_object("getNewEchCert", &Form { sni }).await
279 }
280
281 pub async fn certificate_file_hashes(self, certificate_file: &str) -> Result<Vec<String>> {
290 self.certificate_hashes(certificate_file, "").await
291 }
292
293 pub async fn certificate_content_hashes(
299 self,
300 certificate_content: &str,
301 ) -> Result<Vec<String>> {
302 self.certificate_hashes("", certificate_content).await
303 }
304
305 pub async fn remote_certificate_hashes(self, server: &str) -> Result<Vec<String>> {
311 #[derive(Serialize)]
312 struct Form<'a> {
313 server: &'a str,
314 }
315
316 self.post_form_object("getRemoteCertHash", &Form { server })
317 .await
318 }
319
320 pub async fn scan_reality_target(
326 self,
327 request: &RealityScanRequest,
328 ) -> Result<RealityScanResult> {
329 #[derive(Serialize)]
330 struct Form<'a> {
331 target: &'a str,
332 xver: i32,
333 }
334
335 self.post_form_object(
336 "scanRealityTarget",
337 &Form {
338 target: &request.target,
339 xver: request.xray_version,
340 },
341 )
342 .await
343 }
344
345 pub async fn scan_reality_targets(
351 self,
352 targets: &[impl AsRef<str>],
353 ) -> Result<Vec<RealityScanResult>> {
354 #[derive(Serialize)]
355 struct Form {
356 targets: String,
357 }
358
359 self.post_form_object(
360 "scanRealityTargets",
361 &Form {
362 targets: targets
363 .iter()
364 .map(AsRef::as_ref)
365 .collect::<Vec<_>>()
366 .join(","),
367 },
368 )
369 .await
370 }
371
372 pub async fn scan_default_reality_targets(self) -> Result<Vec<RealityScanResult>> {
381 self.scan_reality_targets(&[] as &[&str]).await
382 }
383
384 pub async fn client_ips(self) -> Result<Vec<ClientIpRecord>> {
390 self.get_object("clientIps").await
391 }
392
393 pub async fn merge_client_ips(self, records: &[ClientIpRecord]) -> Result<()> {
399 self.post_empty("clientIps", Some(records)).await
400 }
401
402 pub async fn fail2ban_status(self) -> Result<Fail2banStatus> {
408 self.get_object("fail2banStatus").await
409 }
410
411 pub async fn stop_xray(self) -> Result<()> {
417 self.post_empty::<()>("stopXrayService", None).await
418 }
419
420 pub async fn restart_xray(self) -> Result<()> {
426 self.post_empty::<()>("restartXrayService", None).await
427 }
428
429 pub async fn install_xray(self, version: &str) -> Result<()> {
437 self.post_empty::<()>(&format!("installXray/{}", segment(version)), None)
438 .await
439 }
440
441 pub async fn update_panel(self, dev_override: Option<bool>) -> Result<PanelUpdateRun> {
450 #[derive(Serialize)]
451 struct Form {
452 #[serde(skip_serializing_if = "Option::is_none")]
453 dev: Option<bool>,
454 }
455
456 self.post_form_object("updatePanel", &Form { dev: dev_override })
457 .await
458 }
459
460 pub async fn set_update_channel(self, dev: bool) -> Result<()> {
466 #[derive(Serialize)]
467 struct Form {
468 dev: bool,
469 }
470
471 self.post_form_empty("setUpdateChannel", &Form { dev })
472 .await
473 }
474
475 pub async fn update_geofiles(self) -> Result<()> {
481 self.post_empty::<()>("updateGeofile", None).await
482 }
483
484 pub async fn update_geofile(self, filename: &str) -> Result<()> {
493 self.post_empty::<()>(&format!("updateGeofile/{}", segment(filename)), None)
494 .await
495 }
496
497 pub async fn panel_logs(self, request: &PanelLogRequest) -> Result<Vec<String>> {
503 #[derive(Serialize)]
504 #[serde(rename_all = "camelCase")]
505 struct Form<'a> {
506 level: &'a str,
507 syslog: bool,
508 }
509
510 self.post_form_list(
511 &format!("logs/{}", request.count),
512 &Form {
513 level: request.level.as_str(),
514 syslog: request.syslog,
515 },
516 )
517 .await
518 }
519
520 pub async fn xray_logs(self, request: &XrayLogRequest) -> Result<Vec<XrayLogEntry>> {
526 #[derive(Serialize)]
527 #[serde(rename_all = "camelCase")]
528 struct Form<'a> {
529 filter: &'a str,
530 show_direct: bool,
531 show_blocked: bool,
532 show_proxy: bool,
533 }
534
535 self.post_form_list(
536 &format!("xraylogs/{}", request.count),
537 &Form {
538 filter: &request.filter,
539 show_direct: request.show_direct,
540 show_blocked: request.show_blocked,
541 show_proxy: request.show_proxy,
542 },
543 )
544 .await
545 }
546
547 pub async fn amneziawg_logs(self, count: u32, filter: &str) -> Result<AmneziaWgLogs> {
556 #[derive(Serialize)]
557 struct Form<'a> {
558 filter: &'a str,
559 }
560
561 self.post_form_object(&format!("amneziawglogs/{count}"), &Form { filter })
562 .await
563 }
564
565 async fn certificate_hashes(
566 self,
567 certificate_file: &str,
568 certificate_content: &str,
569 ) -> Result<Vec<String>> {
570 #[derive(Serialize)]
571 #[serde(rename_all = "camelCase")]
572 struct Form<'a> {
573 cert_file: &'a str,
574 cert_content: &'a str,
575 }
576
577 self.post_form_object(
578 "getCertHash",
579 &Form {
580 cert_file: certificate_file,
581 cert_content: certificate_content,
582 },
583 )
584 .await
585 }
586
587 async fn get_object<T>(self, suffix: &str) -> Result<T>
588 where
589 T: DeserializeOwned,
590 {
591 let path = format!("{ROOT}/{suffix}");
592 let envelope = self
593 .client
594 .execute::<T, ()>(Method::GET, &path, None, AuthenticationScope::PanelApi)
595 .await?;
596 self.required_object(Method::GET, &path, envelope)
597 }
598
599 async fn post_form_object<T, B>(self, suffix: &str, form: &B) -> Result<T>
600 where
601 T: DeserializeOwned,
602 B: Serialize + ?Sized,
603 {
604 let path = format!("{ROOT}/{suffix}");
605 let envelope = self
606 .client
607 .execute_form(Method::POST, &path, form, AuthenticationScope::PanelApi)
608 .await?;
609 self.required_object(Method::POST, &path, envelope)
610 }
611
612 async fn post_form_list<T: DeserializeOwned, B: Serialize + ?Sized>(
616 self,
617 suffix: &str,
618 form: &B,
619 ) -> Result<Vec<T>> {
620 let path = format!("{ROOT}/{suffix}");
621 let envelope = self
622 .client
623 .execute_form::<Vec<T>, _>(Method::POST, &path, form, AuthenticationScope::PanelApi)
624 .await?;
625 Ok(envelope.obj.unwrap_or_default())
626 }
627
628 async fn post_form_empty<B>(self, suffix: &str, form: &B) -> Result<()>
631 where
632 B: Serialize + ?Sized,
633 {
634 let path = format!("{ROOT}/{suffix}");
635 self.client
636 .execute_form::<Value, _>(Method::POST, &path, form, AuthenticationScope::PanelApi)
637 .await?;
638 Ok(())
639 }
640
641 async fn post_empty<B>(self, suffix: &str, body: Option<&B>) -> Result<()>
642 where
643 B: Serialize + ?Sized,
644 {
645 let path = format!("{ROOT}/{suffix}");
646 self.client
647 .execute::<Value, B>(Method::POST, &path, body, AuthenticationScope::PanelApi)
648 .await?;
649 Ok(())
650 }
651
652 async fn download(self, suffix: &str) -> Result<DatabaseFile> {
653 let path = format!("{ROOT}/{suffix}");
654 let (headers, bytes) = self
655 .client
656 .execute_bytes(Method::GET, &path, AuthenticationScope::PanelApi)
657 .await?;
658 let content_type = headers
659 .get(header::CONTENT_TYPE)
660 .and_then(|value| value.to_str().ok())
661 .map(str::to_owned);
662 if content_type
663 .as_deref()
664 .is_some_and(|value| value.starts_with("application/json"))
665 {
666 let url = self.client.endpoint(&path)?;
667 let envelope: ApiResponse<Value> =
668 serde_json::from_slice(&bytes).map_err(|source| Error::Decode {
669 method: Method::GET,
670 url: Box::new(url.clone()),
671 source,
672 })?;
673 if !envelope.success {
674 return Err(Error::Api {
675 method: Method::GET,
676 url: Box::new(url),
677 message: if envelope.msg.is_empty() {
678 "database download failed".to_owned()
679 } else {
680 envelope.msg
681 },
682 });
683 }
684 }
685 Ok(DatabaseFile {
686 filename: attachment_filename(&headers),
687 content_type,
688 bytes,
689 })
690 }
691
692 fn required_object<T>(self, method: Method, path: &str, envelope: ApiResponse<T>) -> Result<T> {
693 let url = self.client.endpoint(path)?;
694 envelope.obj.ok_or_else(|| Error::MissingObject {
695 method,
696 url: Box::new(url),
697 })
698 }
699}
700
701fn segment(value: &str) -> String {
702 utf8_percent_encode(value, NON_ALPHANUMERIC).to_string()
703}
704
705fn attachment_filename(headers: &header::HeaderMap) -> Option<String> {
706 let disposition = headers.get(header::CONTENT_DISPOSITION)?.to_str().ok()?;
707 disposition.split(';').skip(1).find_map(|parameter| {
708 let (name, value) = parameter.trim().split_once('=')?;
709 name.eq_ignore_ascii_case("filename")
710 .then(|| value.trim().trim_matches('"').to_owned())
711 })
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717
718 #[test]
719 fn path_segments_encode_reserved_and_unicode_characters() {
720 assert_eq!(segment("v1/latest"), "v1%2Flatest");
721 assert_eq!(segment("узел 1"), "%D1%83%D0%B7%D0%B5%D0%BB%201");
722 }
723
724 #[test]
725 fn extracts_quoted_attachment_filename() {
726 let mut headers = header::HeaderMap::new();
727 headers.insert(
728 header::CONTENT_DISPOSITION,
729 header::HeaderValue::from_static("attachment; filename=\"x-ui.db\""),
730 );
731 assert_eq!(attachment_filename(&headers).as_deref(), Some("x-ui.db"));
732 }
733}