1use anyhow::{Context, Result};
2use nostr::nips::nip19::{FromBech32, ToBech32};
3use nostr::{Keys, SecretKey};
4use serde::{Deserialize, Serialize};
5use std::fs;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Config {
9 #[serde(default)]
10 pub server: ServerConfig,
11 #[serde(default)]
12 pub storage: StorageConfig,
13 #[serde(default)]
14 pub nostr: NostrConfig,
15 #[serde(default)]
16 pub blossom: BlossomConfig,
17 #[serde(default)]
18 pub sync: SyncConfig,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ServerConfig {
23 #[serde(default = "default_bind_address")]
24 pub bind_address: String,
25 #[serde(default = "default_enable_auth")]
26 pub enable_auth: bool,
27 #[serde(default = "default_stun_port")]
29 pub stun_port: u16,
30 #[serde(default = "default_enable_webrtc")]
32 pub enable_webrtc: bool,
33 #[serde(default = "default_public_writes")]
36 pub public_writes: bool,
37}
38
39fn default_public_writes() -> bool {
40 true
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct StorageConfig {
45 #[serde(default = "default_data_dir")]
46 pub data_dir: String,
47 #[serde(default = "default_max_size_gb")]
48 pub max_size_gb: u64,
49 #[serde(default)]
51 pub s3: Option<S3Config>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct S3Config {
57 pub endpoint: String,
59 pub bucket: String,
61 #[serde(default)]
63 pub prefix: Option<String>,
64 #[serde(default = "default_s3_region")]
66 pub region: String,
67 #[serde(default)]
69 pub access_key: Option<String>,
70 #[serde(default)]
72 pub secret_key: Option<String>,
73 #[serde(default)]
75 pub public_url: Option<String>,
76}
77
78fn default_s3_region() -> String {
79 "auto".to_string()
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct NostrConfig {
84 #[serde(default = "default_relays")]
85 pub relays: Vec<String>,
86 #[serde(default)]
88 pub allowed_npubs: Vec<String>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct BlossomConfig {
93 #[serde(default)]
95 pub servers: Vec<String>,
96 #[serde(default = "default_read_servers")]
98 pub read_servers: Vec<String>,
99 #[serde(default = "default_write_servers")]
101 pub write_servers: Vec<String>,
102 #[serde(default = "default_max_upload_mb")]
104 pub max_upload_mb: u64,
105}
106
107fn default_read_servers() -> Vec<String> {
109 vec!["https://cdn.iris.to".to_string(), "https://hashtree.iris.to".to_string()]
110}
111
112fn default_write_servers() -> Vec<String> {
113 vec!["https://upload.iris.to".to_string()]
114}
115
116fn default_max_upload_mb() -> u64 {
117 5
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct SyncConfig {
122 #[serde(default = "default_sync_enabled")]
124 pub enabled: bool,
125 #[serde(default = "default_sync_own")]
127 pub sync_own: bool,
128 #[serde(default = "default_sync_followed")]
130 pub sync_followed: bool,
131 #[serde(default = "default_max_concurrent")]
133 pub max_concurrent: usize,
134 #[serde(default = "default_webrtc_timeout_ms")]
136 pub webrtc_timeout_ms: u64,
137 #[serde(default = "default_blossom_timeout_ms")]
139 pub blossom_timeout_ms: u64,
140}
141
142
143fn default_sync_enabled() -> bool {
144 true
145}
146
147fn default_sync_own() -> bool {
148 true
149}
150
151fn default_sync_followed() -> bool {
152 true
153}
154
155fn default_max_concurrent() -> usize {
156 3
157}
158
159fn default_webrtc_timeout_ms() -> u64 {
160 2000
161}
162
163fn default_blossom_timeout_ms() -> u64 {
164 10000
165}
166
167fn default_relays() -> Vec<String> {
168 vec![
169 "wss://relay.damus.io".to_string(),
170 "wss://relay.snort.social".to_string(),
171 "wss://nos.lol".to_string(),
172 "wss://temp.iris.to".to_string(),
173 ]
174}
175
176fn default_bind_address() -> String {
177 "127.0.0.1:8080".to_string()
178}
179
180fn default_enable_auth() -> bool {
181 true
182}
183
184fn default_stun_port() -> u16 {
185 3478 }
187
188fn default_enable_webrtc() -> bool {
189 true
190}
191
192fn default_data_dir() -> String {
193 hashtree_config::get_hashtree_dir()
194 .join("data")
195 .to_string_lossy()
196 .to_string()
197}
198
199fn default_max_size_gb() -> u64 {
200 10
201}
202
203impl Default for ServerConfig {
204 fn default() -> Self {
205 Self {
206 bind_address: default_bind_address(),
207 enable_auth: default_enable_auth(),
208 stun_port: default_stun_port(),
209 enable_webrtc: default_enable_webrtc(),
210 public_writes: default_public_writes(),
211 }
212 }
213}
214
215impl Default for StorageConfig {
216 fn default() -> Self {
217 Self {
218 data_dir: default_data_dir(),
219 max_size_gb: default_max_size_gb(),
220 s3: None,
221 }
222 }
223}
224
225impl Default for NostrConfig {
226 fn default() -> Self {
227 Self {
228 relays: default_relays(),
229 allowed_npubs: Vec::new(),
230 }
231 }
232}
233
234impl Default for BlossomConfig {
235 fn default() -> Self {
236 Self {
237 servers: Vec::new(),
238 read_servers: default_read_servers(),
239 write_servers: default_write_servers(),
240 max_upload_mb: default_max_upload_mb(),
241 }
242 }
243}
244
245impl Default for SyncConfig {
246 fn default() -> Self {
247 Self {
248 enabled: default_sync_enabled(),
249 sync_own: default_sync_own(),
250 sync_followed: default_sync_followed(),
251 max_concurrent: default_max_concurrent(),
252 webrtc_timeout_ms: default_webrtc_timeout_ms(),
253 blossom_timeout_ms: default_blossom_timeout_ms(),
254 }
255 }
256}
257
258impl Default for Config {
259 fn default() -> Self {
260 Self {
261 server: ServerConfig::default(),
262 storage: StorageConfig::default(),
263 nostr: NostrConfig::default(),
264 blossom: BlossomConfig::default(),
265 sync: SyncConfig::default(),
266 }
267 }
268}
269
270impl Config {
271 pub fn load() -> Result<Self> {
273 let config_path = get_config_path();
274
275 if config_path.exists() {
276 let content = fs::read_to_string(&config_path)
277 .context("Failed to read config file")?;
278 toml::from_str(&content).context("Failed to parse config file")
279 } else {
280 let config = Config::default();
281 config.save()?;
282 Ok(config)
283 }
284 }
285
286 pub fn save(&self) -> Result<()> {
288 let config_path = get_config_path();
289
290 if let Some(parent) = config_path.parent() {
292 fs::create_dir_all(parent)?;
293 }
294
295 let content = toml::to_string_pretty(self)?;
296 fs::write(&config_path, content)?;
297
298 Ok(())
299 }
300}
301
302pub use hashtree_config::{get_auth_cookie_path, get_config_path, get_hashtree_dir, get_keys_path};
304
305pub fn ensure_auth_cookie() -> Result<(String, String)> {
307 let cookie_path = get_auth_cookie_path();
308
309 if cookie_path.exists() {
310 read_auth_cookie()
311 } else {
312 generate_auth_cookie()
313 }
314}
315
316pub fn read_auth_cookie() -> Result<(String, String)> {
318 let cookie_path = get_auth_cookie_path();
319 let content = fs::read_to_string(&cookie_path)
320 .context("Failed to read auth cookie")?;
321
322 let parts: Vec<&str> = content.trim().split(':').collect();
323 if parts.len() != 2 {
324 anyhow::bail!("Invalid auth cookie format");
325 }
326
327 Ok((parts[0].to_string(), parts[1].to_string()))
328}
329
330pub fn ensure_keys() -> Result<(Keys, bool)> {
333 let keys_path = get_keys_path();
334
335 if keys_path.exists() {
336 let content = fs::read_to_string(&keys_path)
337 .context("Failed to read keys file")?;
338 let entries = hashtree_config::parse_keys_file(&content);
339 let nsec_str = entries.into_iter().next()
340 .map(|e| e.secret)
341 .context("Keys file is empty")?;
342 let secret_key = SecretKey::from_bech32(&nsec_str)
343 .context("Invalid nsec format")?;
344 let keys = Keys::new(secret_key);
345 Ok((keys, false))
346 } else {
347 let keys = generate_keys()?;
348 Ok((keys, true))
349 }
350}
351
352pub fn read_keys() -> Result<Keys> {
354 let keys_path = get_keys_path();
355 let content = fs::read_to_string(&keys_path)
356 .context("Failed to read keys file")?;
357 let entries = hashtree_config::parse_keys_file(&content);
358 let nsec_str = entries.into_iter().next()
359 .map(|e| e.secret)
360 .context("Keys file is empty")?;
361 let secret_key = SecretKey::from_bech32(&nsec_str)
362 .context("Invalid nsec format")?;
363 Ok(Keys::new(secret_key))
364}
365
366pub fn ensure_keys_string() -> Result<(String, bool)> {
369 let keys_path = get_keys_path();
370
371 if keys_path.exists() {
372 let content = fs::read_to_string(&keys_path)
373 .context("Failed to read keys file")?;
374 let entries = hashtree_config::parse_keys_file(&content);
375 let nsec_str = entries.into_iter().next()
376 .map(|e| e.secret)
377 .context("Keys file is empty")?;
378 Ok((nsec_str, false))
379 } else {
380 let keys = generate_keys()?;
381 let nsec = keys.secret_key().to_bech32()
382 .context("Failed to encode nsec")?;
383 Ok((nsec, true))
384 }
385}
386
387pub fn generate_keys() -> Result<Keys> {
389 let keys_path = get_keys_path();
390
391 if let Some(parent) = keys_path.parent() {
393 fs::create_dir_all(parent)?;
394 }
395
396 let keys = Keys::generate();
398 let nsec = keys.secret_key().to_bech32()
399 .context("Failed to encode nsec")?;
400
401 fs::write(&keys_path, &nsec)?;
403
404 #[cfg(unix)]
406 {
407 use std::os::unix::fs::PermissionsExt;
408 let perms = fs::Permissions::from_mode(0o600);
409 fs::set_permissions(&keys_path, perms)?;
410 }
411
412 Ok(keys)
413}
414
415pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
417 keys.public_key().to_bytes()
418}
419
420pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
422 use nostr::PublicKey;
423 let pk = PublicKey::from_bech32(npub)
424 .context("Invalid npub format")?;
425 Ok(pk.to_bytes())
426}
427
428pub fn generate_auth_cookie() -> Result<(String, String)> {
430 use rand::Rng;
431
432 let cookie_path = get_auth_cookie_path();
433
434 if let Some(parent) = cookie_path.parent() {
436 fs::create_dir_all(parent)?;
437 }
438
439 let mut rng = rand::thread_rng();
441 let username = format!("htree_{}", rng.gen::<u32>());
442 let password: String = (0..32)
443 .map(|_| {
444 let idx = rng.gen_range(0..62);
445 match idx {
446 0..=25 => (b'a' + idx) as char,
447 26..=51 => (b'A' + (idx - 26)) as char,
448 _ => (b'0' + (idx - 52)) as char,
449 }
450 })
451 .collect();
452
453 let content = format!("{}:{}", username, password);
455 fs::write(&cookie_path, content)?;
456
457 #[cfg(unix)]
459 {
460 use std::os::unix::fs::PermissionsExt;
461 let perms = fs::Permissions::from_mode(0o600);
462 fs::set_permissions(&cookie_path, perms)?;
463 }
464
465 Ok((username, password))
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471 use tempfile::TempDir;
472
473 #[test]
474 fn test_config_default() {
475 let config = Config::default();
476 assert_eq!(config.server.bind_address, "127.0.0.1:8080");
477 assert_eq!(config.server.enable_auth, true);
478 assert_eq!(config.storage.max_size_gb, 10);
479 }
480
481 #[test]
482 fn test_auth_cookie_generation() -> Result<()> {
483 let temp_dir = TempDir::new()?;
484
485 std::env::set_var("HOME", temp_dir.path());
487
488 let (username, password) = generate_auth_cookie()?;
489
490 assert!(username.starts_with("htree_"));
491 assert_eq!(password.len(), 32);
492
493 let cookie_path = get_auth_cookie_path();
495 assert!(cookie_path.exists());
496
497 let (u2, p2) = read_auth_cookie()?;
499 assert_eq!(username, u2);
500 assert_eq!(password, p2);
501
502 Ok(())
503 }
504}