summaryrefslogtreecommitdiff
path: root/src/model.rs
diff options
context:
space:
mode:
authorAndrew Guschin <guschin@altlinux.org>2026-10-30 03:10:41 +0400
committerAndrew Guschin <guschin@altlinux.org>2026-10-30 03:10:41 +0400
commitd608ffeeda1a479c9dc0e5f6ad269ba07a510d7f (patch)
tree3824ec9da5ca0adac913cc47e3e20054c3dc843a /src/model.rs
parent40cf3276cdc74bf6f1795990ed600891a637d1fe (diff)
Implement Linux proxy controller, subscription state, and CLI/TUI
Diffstat (limited to 'src/model.rs')
-rw-r--r--src/model.rs147
1 files changed, 147 insertions, 0 deletions
diff --git a/src/model.rs b/src/model.rs
new file mode 100644
index 0000000..8966956
--- /dev/null
+++ b/src/model.rs
@@ -0,0 +1,147 @@
+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<String>,
+ pub fingerprint: Option<String>,
+}
+
+impl Trojan {
+ pub fn identity(&self) -> (&str, u16, &str) {
+ (&self.host, self.port, &self.password)
+ }
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+pub struct Entry {
+ pub id: String,
+ pub name: String,
+ pub status: String,
+ pub explanation: Option<String>,
+ pub connection: Option<Trojan>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct ConfigurationSummary {
+ pub id: String,
+ pub subscription_id: String,
+ pub name: String,
+ pub host: Option<String>,
+ pub port: Option<u16>,
+ pub status: String,
+ pub explanation: Option<String>,
+ pub selected: bool,
+ pub ping_ms: Option<u64>,
+ pub speed_bytes_per_second: Option<f64>,
+}
+
+#[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<Subscription>,
+}
+
+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<String>,
+ pub generation: u64,
+ pub routing: String,
+ pub health: String,
+ pub last_error: Option<String>,
+}
+
+pub fn display_text(value: &str) -> String {
+ value
+ .chars()
+ .filter(|c| !c.is_control())
+ .take(256)
+ .collect()
+}