use serde::{Deserialize, Serialize}; #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Trojan { pub host: String, pub port: u16, pub password: String, pub server_name: String, pub alpn: Vec, pub fingerprint: Option, } impl Trojan { pub fn identity(&self) -> (&str, u16, &str) { (&self.host, self.port, &self.password) } } #[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum EntryStatus { Supported, Unsupported, Invalid, } impl EntryStatus { pub fn as_str(self) -> &'static str { match self { Self::Supported => "supported", Self::Unsupported => "unsupported", Self::Invalid => "invalid", } } } #[derive(Clone, Serialize, Deserialize)] pub struct Entry { pub id: String, pub name: String, pub status: EntryStatus, pub explanation: Option, pub connection: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ConfigurationSummary { pub id: String, pub subscription_id: String, pub name: String, pub host: Option, pub port: Option, pub status: String, pub explanation: Option, pub selected: bool, pub ping_ms: Option, pub speed_bytes_per_second: Option, } #[derive(Clone, Serialize, Deserialize)] pub struct Subscription { pub id: String, pub name: String, pub url: String, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SubscriptionSummary { pub id: String, pub name: String, } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct Settings { pub ping_url: String, pub speed_url: String, pub maximum_download_mib: u64, pub maximum_duration_seconds: u64, } impl Default for Settings { fn default() -> Self { Self { ping_url: "https://speed.cloudflare.com/__down?bytes=0".into(), speed_url: "https://speed.cloudflare.com/__down?bytes=10485760".into(), maximum_download_mib: 10, maximum_duration_seconds: 10, } } } impl Settings { pub fn validate(&self) -> anyhow::Result<()> { for value in [&self.ping_url, &self.speed_url] { let url = url::Url::parse(value) .map_err(|_| anyhow::anyhow!("Use an absolute HTTPS diagnostic URL"))?; anyhow::ensure!( url.scheme() == "https" && url.host_str().is_some() && url.username().is_empty() && url.password().is_none() && url.fragment().is_none(), "Diagnostic URLs require HTTPS without credentials or fragments" ); } anyhow::ensure!( self.maximum_download_mib > 0 && self.maximum_download_mib.checked_mul(1_048_576).is_some(), "Download limit must be a positive representable MiB count" ); anyhow::ensure!( self.maximum_duration_seconds > 0, "Duration limit must be positive" ); anyhow::ensure!( std::time::Instant::now() .checked_add(std::time::Duration::from_secs( self.maximum_duration_seconds )) .is_some(), "Duration exceeds the supported clock range" ); Ok(()) } } #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Definitions { pub version: u32, #[serde(default)] pub settings: Settings, #[serde(default)] pub subscriptions: Vec, } impl Default for Definitions { fn default() -> Self { Self { version: 1, settings: Settings::default(), subscriptions: vec![], } } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Status { pub version: u32, pub requested_enabled: bool, pub selected_configuration: Option, pub generation: u64, pub routing: String, pub health: String, pub last_error: Option, } pub fn display_text(value: &str) -> String { value .chars() .filter(|c| !c.is_control()) .take(256) .collect() }