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> {
108 vec!["https://cdn.iris.to".to_string(), "https://hashtree.iris.to".to_string()]
109}
110
111fn default_write_servers() -> Vec<String> {
112 vec!["https://hashtree.iris.to".to_string()]
113}
114
115fn default_max_upload_mb() -> u64 {
116 5
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct SyncConfig {
121 #[serde(default = "default_sync_enabled")]
123 pub enabled: bool,
124 #[serde(default = "default_sync_own")]
126 pub sync_own: bool,
127 #[serde(default = "default_sync_followed")]
129 pub sync_followed: bool,
130 #[serde(default = "default_max_concurrent")]
132 pub max_concurrent: usize,
133 #[serde(default = "default_webrtc_timeout_ms")]
135 pub webrtc_timeout_ms: u64,
136 #[serde(default = "default_blossom_timeout_ms")]
138 pub blossom_timeout_ms: u64,
139}
140
141
142fn default_sync_enabled() -> bool {
143 true
144}
145
146fn default_sync_own() -> bool {
147 true
148}
149
150fn default_sync_followed() -> bool {
151 true
152}
153
154fn default_max_concurrent() -> usize {
155 3
156}
157
158fn default_webrtc_timeout_ms() -> u64 {
159 2000
160}
161
162fn default_blossom_timeout_ms() -> u64 {
163 10000
164}
165
166fn default_relays() -> Vec<String> {
167 vec![
168 "wss://relay.damus.io".to_string(),
169 "wss://relay.snort.social".to_string(),
170 "wss://nos.lol".to_string(),
171 "wss://temp.iris.to".to_string(),
172 ]
173}
174
175fn default_bind_address() -> String {
176 "127.0.0.1:8080".to_string()
177}
178
179fn default_enable_auth() -> bool {
180 true
181}
182
183fn default_stun_port() -> u16 {
184 3478 }
186
187fn default_enable_webrtc() -> bool {
188 true
189}
190
191fn default_data_dir() -> String {
192 hashtree_config::get_hashtree_dir()
193 .join("data")
194 .to_string_lossy()
195 .to_string()
196}
197
198fn default_max_size_gb() -> u64 {
199 10
200}
201
202impl Default for ServerConfig {
203 fn default() -> Self {
204 Self {
205 bind_address: default_bind_address(),
206 enable_auth: default_enable_auth(),
207 stun_port: default_stun_port(),
208 enable_webrtc: default_enable_webrtc(),
209 public_writes: default_public_writes(),
210 }
211 }
212}
213
214impl Default for StorageConfig {
215 fn default() -> Self {
216 Self {
217 data_dir: default_data_dir(),
218 max_size_gb: default_max_size_gb(),
219 s3: None,
220 }
221 }
222}
223
224impl Default for NostrConfig {
225 fn default() -> Self {
226 Self {
227 relays: default_relays(),
228 allowed_npubs: Vec::new(),
229 }
230 }
231}
232
233impl Default for BlossomConfig {
234 fn default() -> Self {
235 Self {
236 servers: Vec::new(),
237 read_servers: default_read_servers(),
238 write_servers: default_write_servers(),
239 max_upload_mb: default_max_upload_mb(),
240 }
241 }
242}
243
244impl Default for SyncConfig {
245 fn default() -> Self {
246 Self {
247 enabled: default_sync_enabled(),
248 sync_own: default_sync_own(),
249 sync_followed: default_sync_followed(),
250 max_concurrent: default_max_concurrent(),
251 webrtc_timeout_ms: default_webrtc_timeout_ms(),
252 blossom_timeout_ms: default_blossom_timeout_ms(),
253 }
254 }
255}
256
257impl Default for Config {
258 fn default() -> Self {
259 Self {
260 server: ServerConfig::default(),
261 storage: StorageConfig::default(),
262 nostr: NostrConfig::default(),
263 blossom: BlossomConfig::default(),
264 sync: SyncConfig::default(),
265 }
266 }
267}
268
269impl Config {
270 pub fn load() -> Result<Self> {
272 let config_path = get_config_path();
273
274 if config_path.exists() {
275 let content = fs::read_to_string(&config_path)
276 .context("Failed to read config file")?;
277 toml::from_str(&content).context("Failed to parse config file")
278 } else {
279 let config = Config::default();
280 config.save()?;
281 Ok(config)
282 }
283 }
284
285 pub fn save(&self) -> Result<()> {
287 let config_path = get_config_path();
288
289 if let Some(parent) = config_path.parent() {
291 fs::create_dir_all(parent)?;
292 }
293
294 let content = toml::to_string_pretty(self)?;
295 fs::write(&config_path, content)?;
296
297 Ok(())
298 }
299}
300
301pub use hashtree_config::{get_auth_cookie_path, get_config_path, get_hashtree_dir, get_keys_path};
303
304pub fn ensure_auth_cookie() -> Result<(String, String)> {
306 let cookie_path = get_auth_cookie_path();
307
308 if cookie_path.exists() {
309 read_auth_cookie()
310 } else {
311 generate_auth_cookie()
312 }
313}
314
315pub fn read_auth_cookie() -> Result<(String, String)> {
317 let cookie_path = get_auth_cookie_path();
318 let content = fs::read_to_string(&cookie_path)
319 .context("Failed to read auth cookie")?;
320
321 let parts: Vec<&str> = content.trim().split(':').collect();
322 if parts.len() != 2 {
323 anyhow::bail!("Invalid auth cookie format");
324 }
325
326 Ok((parts[0].to_string(), parts[1].to_string()))
327}
328
329pub fn ensure_keys() -> Result<(Keys, bool)> {
332 let keys_path = get_keys_path();
333
334 if keys_path.exists() {
335 let nsec_str = fs::read_to_string(&keys_path)
336 .context("Failed to read keys file")?;
337 let nsec_str = nsec_str.trim();
338 let secret_key = SecretKey::from_bech32(nsec_str)
339 .context("Invalid nsec format")?;
340 let keys = Keys::new(secret_key);
341 Ok((keys, false))
342 } else {
343 let keys = generate_keys()?;
344 Ok((keys, true))
345 }
346}
347
348pub fn read_keys() -> Result<Keys> {
350 let keys_path = get_keys_path();
351 let nsec_str = fs::read_to_string(&keys_path)
352 .context("Failed to read keys file")?;
353 let nsec_str = nsec_str.trim();
354 let secret_key = SecretKey::from_bech32(nsec_str)
355 .context("Invalid nsec format")?;
356 Ok(Keys::new(secret_key))
357}
358
359pub fn ensure_keys_string() -> Result<(String, bool)> {
362 let keys_path = get_keys_path();
363
364 if keys_path.exists() {
365 let nsec_str = fs::read_to_string(&keys_path)
366 .context("Failed to read keys file")?;
367 Ok((nsec_str.trim().to_string(), false))
368 } else {
369 let keys = generate_keys()?;
370 let nsec = keys.secret_key().to_bech32()
371 .context("Failed to encode nsec")?;
372 Ok((nsec, true))
373 }
374}
375
376pub fn generate_keys() -> Result<Keys> {
378 let keys_path = get_keys_path();
379
380 if let Some(parent) = keys_path.parent() {
382 fs::create_dir_all(parent)?;
383 }
384
385 let keys = Keys::generate();
387 let nsec = keys.secret_key().to_bech32()
388 .context("Failed to encode nsec")?;
389
390 fs::write(&keys_path, &nsec)?;
392
393 #[cfg(unix)]
395 {
396 use std::os::unix::fs::PermissionsExt;
397 let perms = fs::Permissions::from_mode(0o600);
398 fs::set_permissions(&keys_path, perms)?;
399 }
400
401 Ok(keys)
402}
403
404pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
406 keys.public_key().to_bytes()
407}
408
409pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
411 use nostr::PublicKey;
412 let pk = PublicKey::from_bech32(npub)
413 .context("Invalid npub format")?;
414 Ok(pk.to_bytes())
415}
416
417pub fn generate_auth_cookie() -> Result<(String, String)> {
419 use rand::Rng;
420
421 let cookie_path = get_auth_cookie_path();
422
423 if let Some(parent) = cookie_path.parent() {
425 fs::create_dir_all(parent)?;
426 }
427
428 let mut rng = rand::thread_rng();
430 let username = format!("htree_{}", rng.gen::<u32>());
431 let password: String = (0..32)
432 .map(|_| {
433 let idx = rng.gen_range(0..62);
434 match idx {
435 0..=25 => (b'a' + idx) as char,
436 26..=51 => (b'A' + (idx - 26)) as char,
437 _ => (b'0' + (idx - 52)) as char,
438 }
439 })
440 .collect();
441
442 let content = format!("{}:{}", username, password);
444 fs::write(&cookie_path, content)?;
445
446 #[cfg(unix)]
448 {
449 use std::os::unix::fs::PermissionsExt;
450 let perms = fs::Permissions::from_mode(0o600);
451 fs::set_permissions(&cookie_path, perms)?;
452 }
453
454 Ok((username, password))
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460 use tempfile::TempDir;
461
462 #[test]
463 fn test_config_default() {
464 let config = Config::default();
465 assert_eq!(config.server.bind_address, "127.0.0.1:8080");
466 assert_eq!(config.server.enable_auth, true);
467 assert_eq!(config.storage.max_size_gb, 10);
468 }
469
470 #[test]
471 fn test_auth_cookie_generation() -> Result<()> {
472 let temp_dir = TempDir::new()?;
473
474 std::env::set_var("HOME", temp_dir.path());
476
477 let (username, password) = generate_auth_cookie()?;
478
479 assert!(username.starts_with("htree_"));
480 assert_eq!(password.len(), 32);
481
482 let cookie_path = get_auth_cookie_path();
484 assert!(cookie_path.exists());
485
486 let (u2, p2) = read_auth_cookie()?;
488 assert_eq!(username, u2);
489 assert_eq!(password, p2);
490
491 Ok(())
492 }
493}