singbox_driver/api/
controller_configuration.rs1use crate::api::http::SingBoxControllerAuthorization;
4use serde_json::Value;
5use std::error::Error;
6use std::fmt::{self, Display, Formatter};
7use std::net::SocketAddr;
8
9#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum SingBoxControllerConfigurationError {
12 InvalidJson,
14 ClashApiDisabled,
16 InvalidExternalController,
18 ExternalControllerNotLoopback,
20 ExternalControllerHasEphemeralPort,
22 InvalidSecret,
24}
25
26impl Display for SingBoxControllerConfigurationError {
27 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
28 match self {
29 Self::InvalidJson => formatter.write_str("sing-box profile is not valid JSON"),
30 Self::ClashApiDisabled => {
31 formatter.write_str("sing-box profile does not enable experimental.clash_api")
32 }
33 Self::InvalidExternalController => {
34 formatter.write_str("sing-box external_controller is not a socket address")
35 }
36 Self::ExternalControllerNotLoopback => {
37 formatter.write_str("sing-box external_controller must bind to loopback")
38 }
39 Self::ExternalControllerHasEphemeralPort => {
40 formatter.write_str("sing-box external_controller must use a fixed, non-zero port")
41 }
42 Self::InvalidSecret => {
43 formatter.write_str("sing-box Clash API secret must be a string")
44 }
45 }
46 }
47}
48
49impl Error for SingBoxControllerConfigurationError {}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct SingBoxControllerConfiguration {
54 address: SocketAddr,
55 authorization: SingBoxControllerAuthorization,
56}
57
58impl SingBoxControllerConfiguration {
59 pub fn from_profile_json(
64 raw_bytes: &[u8],
65 ) -> Result<Self, SingBoxControllerConfigurationError> {
66 let document: Value = serde_json::from_slice(raw_bytes)
67 .map_err(|_| SingBoxControllerConfigurationError::InvalidJson)?;
68 let clash_api = document
69 .get("experimental")
70 .and_then(Value::as_object)
71 .and_then(|experimental| experimental.get("clash_api"))
72 .and_then(Value::as_object)
73 .ok_or(SingBoxControllerConfigurationError::ClashApiDisabled)?;
74 let external_controller = clash_api
75 .get("external_controller")
76 .and_then(Value::as_str)
77 .filter(|address| !address.trim().is_empty())
78 .ok_or(SingBoxControllerConfigurationError::ClashApiDisabled)?;
79 let address = external_controller
80 .parse::<SocketAddr>()
81 .map_err(|_| SingBoxControllerConfigurationError::InvalidExternalController)?;
82 if !address.ip().is_loopback() {
83 return Err(SingBoxControllerConfigurationError::ExternalControllerNotLoopback);
84 }
85 if address.port() == 0 {
86 return Err(SingBoxControllerConfigurationError::ExternalControllerHasEphemeralPort);
87 }
88 let authorization = match clash_api.get("secret") {
89 None | Some(Value::Null) => SingBoxControllerAuthorization::NoSecret,
90 Some(Value::String(secret)) if secret.is_empty() => {
91 SingBoxControllerAuthorization::NoSecret
92 }
93 Some(Value::String(secret)) => {
94 SingBoxControllerAuthorization::BearerToken(secret.clone())
95 }
96 Some(_) => return Err(SingBoxControllerConfigurationError::InvalidSecret),
97 };
98 Ok(Self {
99 address,
100 authorization,
101 })
102 }
103
104 pub fn address(&self) -> SocketAddr {
106 self.address
107 }
108
109 pub fn endpoint(&self) -> String {
111 format!("http://{}", self.address)
112 }
113
114 pub fn authorization(&self) -> &SingBoxControllerAuthorization {
116 &self.authorization
117 }
118}
119
120#[cfg(test)]
121#[path = "controller_configuration_tests.rs"]
122mod tests;