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 nsec_str = fs::read_to_string(&keys_path)
337 .context("Failed to read keys file")?;
338 let nsec_str = nsec_str.trim();
339 let secret_key = SecretKey::from_bech32(nsec_str)
340 .context("Invalid nsec format")?;
341 let keys = Keys::new(secret_key);
342 Ok((keys, false))
343 } else {
344 let keys = generate_keys()?;
345 Ok((keys, true))
346 }
347}
348
349pub fn read_keys() -> Result<Keys> {
351 let keys_path = get_keys_path();
352 let nsec_str = fs::read_to_string(&keys_path)
353 .context("Failed to read keys file")?;
354 let nsec_str = nsec_str.trim();
355 let secret_key = SecretKey::from_bech32(nsec_str)
356 .context("Invalid nsec format")?;
357 Ok(Keys::new(secret_key))
358}
359
360pub fn ensure_keys_string() -> Result<(String, bool)> {
363 let keys_path = get_keys_path();
364
365 if keys_path.exists() {
366 let nsec_str = fs::read_to_string(&keys_path)
367 .context("Failed to read keys file")?;
368 Ok((nsec_str.trim().to_string(), false))
369 } else {
370 let keys = generate_keys()?;
371 let nsec = keys.secret_key().to_bech32()
372 .context("Failed to encode nsec")?;
373 Ok((nsec, true))
374 }
375}
376
377pub fn generate_keys() -> Result<Keys> {
379 let keys_path = get_keys_path();
380
381 if let Some(parent) = keys_path.parent() {
383 fs::create_dir_all(parent)?;
384 }
385
386 let keys = Keys::generate();
388 let nsec = keys.secret_key().to_bech32()
389 .context("Failed to encode nsec")?;
390
391 fs::write(&keys_path, &nsec)?;
393
394 #[cfg(unix)]
396 {
397 use std::os::unix::fs::PermissionsExt;
398 let perms = fs::Permissions::from_mode(0o600);
399 fs::set_permissions(&keys_path, perms)?;
400 }
401
402 Ok(keys)
403}
404
405pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
407 keys.public_key().to_bytes()
408}
409
410pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
412 use nostr::PublicKey;
413 let pk = PublicKey::from_bech32(npub)
414 .context("Invalid npub format")?;
415 Ok(pk.to_bytes())
416}
417
418pub fn generate_auth_cookie() -> Result<(String, String)> {
420 use rand::Rng;
421
422 let cookie_path = get_auth_cookie_path();
423
424 if let Some(parent) = cookie_path.parent() {
426 fs::create_dir_all(parent)?;
427 }
428
429 let mut rng = rand::thread_rng();
431 let username = format!("htree_{}", rng.gen::<u32>());
432 let password: String = (0..32)
433 .map(|_| {
434 let idx = rng.gen_range(0..62);
435 match idx {
436 0..=25 => (b'a' + idx) as char,
437 26..=51 => (b'A' + (idx - 26)) as char,
438 _ => (b'0' + (idx - 52)) as char,
439 }
440 })
441 .collect();
442
443 let content = format!("{}:{}", username, password);
445 fs::write(&cookie_path, content)?;
446
447 #[cfg(unix)]
449 {
450 use std::os::unix::fs::PermissionsExt;
451 let perms = fs::Permissions::from_mode(0o600);
452 fs::set_permissions(&cookie_path, perms)?;
453 }
454
455 Ok((username, password))
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461 use tempfile::TempDir;
462
463 #[test]
464 fn test_config_default() {
465 let config = Config::default();
466 assert_eq!(config.server.bind_address, "127.0.0.1:8080");
467 assert_eq!(config.server.enable_auth, true);
468 assert_eq!(config.storage.max_size_gb, 10);
469 }
470
471 #[test]
472 fn test_auth_cookie_generation() -> Result<()> {
473 let temp_dir = TempDir::new()?;
474
475 std::env::set_var("HOME", temp_dir.path());
477
478 let (username, password) = generate_auth_cookie()?;
479
480 assert!(username.starts_with("htree_"));
481 assert_eq!(password.len(), 32);
482
483 let cookie_path = get_auth_cookie_path();
485 assert!(cookie_path.exists());
486
487 let (u2, p2) = read_auth_cookie()?;
489 assert_eq!(username, u2);
490 assert_eq!(password, p2);
491
492 Ok(())
493 }
494}