1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5use std::sync::OnceLock;
6
7static CUSTOM_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
9
10pub fn set_config_dir(path: PathBuf) {
12 CUSTOM_CONFIG_DIR.set(path).ok();
13}
14
15#[derive(Debug, Serialize, Deserialize, Clone)]
17pub struct BridgeConfig {
18 pub hostname: String,
19 pub tunnel_id: String,
20 pub tunnel_secret: String,
21 #[serde(default)]
23 pub account_id: String,
24 pub client_id: String,
25 pub client_secret: String,
26 pub domain: String,
27 pub subdomain: String,
28 #[serde(default)]
30 pub auth_token: String,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub cert_fingerprint: Option<String>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub service_token_issued_at: Option<i64>,
38 #[serde(default, skip_serializing_if = "String::is_empty")]
41 pub api_token: String,
42}
43
44impl BridgeConfig {
45 pub fn config_path() -> PathBuf {
47 Self::config_dir().join("config.json")
48 }
49
50 pub fn config_dir() -> PathBuf {
52 let config_dir_path = if let Some(custom_dir) = CUSTOM_CONFIG_DIR.get() {
54 custom_dir.clone()
55 } else {
56 let config_dir = directories::ProjectDirs::from("com", "aptove", "bridge")
57 .expect("Failed to determine config directory");
58 config_dir.config_dir().to_path_buf()
59 };
60
61 fs::create_dir_all(&config_dir_path).ok();
62
63 config_dir_path
64 }
65
66 pub fn save(&self) -> Result<()> {
68 let config_path = Self::config_path();
69 let json = serde_json::to_string_pretty(self)
70 .context("Failed to serialize configuration")?;
71
72 fs::write(&config_path, &json)
73 .context(format!("Failed to write configuration to {:?}", config_path))?;
74
75 #[cfg(unix)]
77 {
78 use std::os::unix::fs::PermissionsExt;
79 let mut perms = fs::metadata(&config_path)?.permissions();
80 perms.set_mode(0o600); fs::set_permissions(&config_path, perms)?;
82 }
83
84 Ok(())
85 }
86
87 pub fn generate_auth_token() -> String {
89 use base64::{engine::general_purpose, Engine as _};
90 let random_bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
91 general_purpose::URL_SAFE_NO_PAD.encode(random_bytes)
92 }
93
94 pub fn ensure_auth_token(&mut self) {
96 if self.auth_token.is_empty() {
97 self.auth_token = Self::generate_auth_token();
98 }
99 }
100
101 const SERVICE_TOKEN_LIFETIME_SECS: i64 = 365 * 24 * 3600;
103 const SERVICE_TOKEN_ROTATE_THRESHOLD_SECS: i64 = 30 * 24 * 3600;
105
106 pub fn service_token_needs_rotation(&self) -> bool {
108 let issued_at = match self.service_token_issued_at {
109 Some(ts) => ts,
110 None => return !self.client_id.is_empty(),
112 };
113 let now = std::time::SystemTime::now()
114 .duration_since(std::time::UNIX_EPOCH)
115 .map(|d| d.as_secs() as i64)
116 .unwrap_or(0);
117 let age = now - issued_at;
118 age >= Self::SERVICE_TOKEN_LIFETIME_SECS - Self::SERVICE_TOKEN_ROTATE_THRESHOLD_SECS
119 }
120
121 pub fn stamp_service_token_issued(&mut self) {
123 let now = std::time::SystemTime::now()
124 .duration_since(std::time::UNIX_EPOCH)
125 .map(|d| d.as_secs() as i64)
126 .unwrap_or(0);
127 self.service_token_issued_at = Some(now);
128 }
129
130 pub fn load() -> Result<Self> {
132 let config_path = Self::config_path();
133 let json = fs::read_to_string(&config_path)
134 .context(format!("Failed to read configuration from {:?}", config_path))?;
135
136 let config: Self = serde_json::from_str(&json)
137 .context("Failed to parse configuration file")?;
138
139 Ok(config)
140 }
141
142 pub fn to_connection_json(&self) -> Result<String> {
144 use serde_json::{Map, Value};
145
146 let mut map = Map::new();
147 map.insert("url".to_string(), Value::String(self.hostname.clone()));
148 map.insert("protocol".to_string(), Value::String("acp".to_string()));
149 map.insert("version".to_string(), Value::String("1.0".to_string()));
150
151 if !self.client_id.is_empty() {
152 map.insert("clientId".to_string(), Value::String(self.client_id.clone()));
153 }
154
155 if !self.client_secret.is_empty() {
156 map.insert("clientSecret".to_string(), Value::String(self.client_secret.clone()));
157 }
158
159 if !self.auth_token.is_empty() {
161 map.insert("authToken".to_string(), Value::String(self.auth_token.clone()));
162 }
163
164 if let Some(ref fingerprint) = self.cert_fingerprint {
166 map.insert("certFingerprint".to_string(), Value::String(fingerprint.clone()));
167 }
168
169 serde_json::to_string(&Value::Object(map))
170 .context("Failed to serialize connection info")
171 }
172}