1mod auth;
2mod blob_read;
3pub mod blossom;
4mod handlers;
5mod ingest_filter;
6mod mime;
7mod nostr_query;
8mod peer_status;
9mod request_paths;
10mod status_metrics;
11#[cfg(feature = "p2p")]
12pub mod stun;
13mod ui;
14pub mod ws_relay;
15
16use crate::nostr_relay::NostrRelay;
17use crate::socialgraph;
18use crate::storage::HashtreeStore;
19use crate::webrtc::WebRTCState;
20use anyhow::Result;
21use axum::{
22 extract::DefaultBodyLimit,
23 middleware,
24 routing::{get, post, put},
25 Router,
26};
27use std::collections::{HashMap, HashSet};
28use std::future;
29use std::sync::{Arc, OnceLock, RwLock};
30use tower_http::cors::CorsLayer;
31
32pub use auth::{new_lookup_cache, AppState, AuthCredentials, CachedTreeRootEntry};
33
34static VIRTUAL_TREE_HOSTS: OnceLock<RwLock<HashMap<String, String>>> = OnceLock::new();
35const DEFAULT_OPTIMISTIC_UPLOAD_QUEUE_BYTES: usize = 512 * 1024 * 1024;
36
37fn virtual_tree_hosts() -> &'static RwLock<HashMap<String, String>> {
38 VIRTUAL_TREE_HOSTS.get_or_init(|| RwLock::new(HashMap::new()))
39}
40
41fn normalize_virtual_tree_host(host: &str) -> Option<String> {
42 let trimmed = host.trim().trim_end_matches('.').to_ascii_lowercase();
43 if trimmed.is_empty() {
44 return None;
45 }
46
47 if let Some(stripped) = trimmed
48 .strip_prefix('[')
49 .and_then(|value| value.split_once(']'))
50 {
51 let host_only = stripped.0.trim();
52 if host_only.is_empty() {
53 return None;
54 }
55 return Some(host_only.to_string());
56 }
57
58 if let Some((host_only, port)) = trimmed.rsplit_once(':') {
59 if !host_only.is_empty() && !port.is_empty() && port.chars().all(|ch| ch.is_ascii_digit()) {
60 return Some(host_only.to_string());
61 }
62 }
63
64 Some(trimmed)
65}
66
67pub fn register_virtual_tree_host(host: &str, internal_root: &str) {
68 let Some(normalized_host) = normalize_virtual_tree_host(host) else {
69 return;
70 };
71
72 let normalized_root = internal_root.trim().trim_end_matches('/');
73 if normalized_root.is_empty() {
74 return;
75 }
76
77 if let Ok(mut hosts) = virtual_tree_hosts().write() {
78 hosts.insert(normalized_host, normalized_root.to_string());
79 }
80}
81
82pub fn resolve_virtual_tree_host(host: &str) -> Option<String> {
83 let normalized_host = normalize_virtual_tree_host(host)?;
84 virtual_tree_hosts()
85 .read()
86 .ok()
87 .and_then(|hosts| hosts.get(&normalized_host).cloned())
88}
89
90#[cfg(test)]
91pub fn clear_virtual_tree_hosts_for_test() {
92 if let Ok(mut hosts) = virtual_tree_hosts().write() {
93 hosts.clear();
94 }
95}
96
97pub struct HashtreeServer {
98 state: AppState,
99 addr: String,
100 extra_routes: Option<Router<AppState>>,
101 cors: Option<CorsLayer>,
102}
103
104impl HashtreeServer {
105 pub fn new(store: Arc<HashtreeStore>, addr: String) -> Self {
106 Self {
107 state: AppState {
108 store,
109 auth: None,
110 daemon_started_at: current_unix_secs(),
111 peer_mode: crate::config::ServerMode::Normal,
112 hash_get_enabled: true,
113 http_webrtc_fetch: true,
114 webrtc_peers: None,
115 fips_transport: None,
116 fetch_from_fips_peers: true,
117 ws_relay: Arc::new(auth::WsRelayState::new()),
118 max_upload_bytes: 5 * 1024 * 1024, public_writes: true, require_random_untrusted_ingest: true,
121 optimistic_blossom_uploads: false,
122 optimistic_upload_queue_bytes: DEFAULT_OPTIMISTIC_UPLOAD_QUEUE_BYTES,
123 optimistic_upload_queue: Arc::new(tokio::sync::Semaphore::new(
124 DEFAULT_OPTIMISTIC_UPLOAD_QUEUE_BYTES,
125 )),
126 allowed_pubkeys: HashSet::new(), upstream_blossom: Vec::new(),
128 social_graph: None,
129 social_graph_store: None,
130 social_graph_root: None,
131 socialgraph_snapshot_public: false,
132 nostr_relay: None,
133 nostr_relay_urls: Vec::new(),
134 tree_root_cache: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
135 inflight_blob_fetches: Arc::new(tokio::sync::Mutex::new(
136 std::collections::HashMap::new(),
137 )),
138 inflight_blob_reads: Arc::new(tokio::sync::Mutex::new(
139 std::collections::HashMap::new(),
140 )),
141 blob_cache: Arc::new(crate::blob_cache::BlobCache::from_env()),
142 directory_listing_cache: Arc::new(std::sync::Mutex::new(new_lookup_cache())),
143 resolved_path_cache: Arc::new(std::sync::Mutex::new(new_lookup_cache())),
144 thumbnail_path_cache: Arc::new(std::sync::Mutex::new(new_lookup_cache())),
145 cid_size_cache: Arc::new(std::sync::Mutex::new(new_lookup_cache())),
146 },
147 addr,
148 extra_routes: None,
149 cors: None,
150 }
151 }
152
153 pub fn with_max_upload_bytes(mut self, bytes: usize) -> Self {
155 self.state.max_upload_bytes = bytes;
156 self
157 }
158
159 pub fn with_public_writes(mut self, public: bool) -> Self {
162 self.state.public_writes = public;
163 self
164 }
165
166 pub fn with_require_random_untrusted_ingest(mut self, require: bool) -> Self {
167 self.state.require_random_untrusted_ingest = require;
168 self
169 }
170
171 pub fn with_optimistic_blossom_uploads(mut self, enabled: bool) -> Self {
172 self.state.optimistic_blossom_uploads = enabled;
173 self
174 }
175
176 pub fn with_server_mode(mut self, mode: crate::config::ServerMode) -> Self {
177 self.state.peer_mode = mode;
178 self
179 }
180
181 pub fn with_hash_get_enabled(mut self, enabled: bool) -> Self {
182 self.state.hash_get_enabled = enabled;
183 self
184 }
185
186 pub fn with_http_webrtc_fetch(mut self, enabled: bool) -> Self {
187 self.state.http_webrtc_fetch = enabled;
188 self
189 }
190
191 pub fn with_fetch_from_fips_peers(mut self, enabled: bool) -> Self {
192 self.state.fetch_from_fips_peers = enabled;
193 self
194 }
195
196 pub fn with_fips_transport(
197 mut self,
198 transport: Arc<crate::fips_transport::DaemonFipsTransport>,
199 ) -> Self {
200 self.state.fips_transport = Some(transport);
201 self
202 }
203
204 pub fn with_webrtc_peers(mut self, webrtc_state: Arc<WebRTCState>) -> Self {
206 self.state.webrtc_peers = Some(webrtc_state);
207 self
208 }
209
210 pub fn with_auth(mut self, username: String, password: String) -> Self {
211 self.state.auth = Some(AuthCredentials { username, password });
212 self
213 }
214
215 pub fn with_allowed_pubkeys(mut self, pubkeys: HashSet<String>) -> Self {
217 self.state.allowed_pubkeys = pubkeys;
218 self
219 }
220
221 pub fn with_upstream_blossom(mut self, servers: Vec<String>) -> Self {
223 self.state.upstream_blossom = servers;
224 self
225 }
226
227 pub fn with_social_graph(mut self, sg: Arc<socialgraph::SocialGraphAccessControl>) -> Self {
229 self.state.social_graph = Some(sg);
230 self
231 }
232
233 pub fn with_socialgraph_snapshot(
235 mut self,
236 store: Arc<dyn socialgraph::SocialGraphBackend>,
237 root: [u8; 32],
238 public: bool,
239 ) -> Self {
240 self.state.social_graph_store = Some(store);
241 self.state.social_graph_root = Some(root);
242 self.state.socialgraph_snapshot_public = public;
243 self
244 }
245
246 pub fn with_nostr_relay(mut self, relay: Arc<NostrRelay>) -> Self {
248 self.state.nostr_relay = Some(relay);
249 self
250 }
251
252 pub fn with_nostr_relay_urls(mut self, relays: Vec<String>) -> Self {
254 self.state.nostr_relay_urls = relays;
255 self
256 }
257
258 pub fn with_extra_routes(mut self, routes: Router<AppState>) -> Self {
260 self.extra_routes = Some(routes);
261 self
262 }
263
264 pub fn with_cors(mut self, cors: CorsLayer) -> Self {
266 self.cors = Some(cors);
267 self
268 }
269
270 pub async fn run(self) -> Result<()> {
271 let listener = tokio::net::TcpListener::bind(&self.addr).await?;
272 let _ = self.run_with_listener(listener).await?;
273 Ok(())
274 }
275
276 pub async fn run_with_listener(self, listener: tokio::net::TcpListener) -> Result<u16> {
277 self.run_with_listener_until(listener, future::pending::<()>())
278 .await
279 }
280
281 pub async fn run_with_listener_until<F>(
282 self,
283 listener: tokio::net::TcpListener,
284 shutdown: F,
285 ) -> Result<u16>
286 where
287 F: std::future::Future<Output = ()> + Send + 'static,
288 {
289 let local_addr = listener.local_addr()?;
290
291 let state = self.state.clone();
295 let public_routes = Router::new()
296 .route("/", get(handlers::serve_root_or_virtual_host))
297 .route("/ws", get(ws_relay::ws_data))
298 .route("/ws/", get(ws_relay::ws_data))
299 .route(
300 "/htree/test",
301 get(handlers::htree_test).head(handlers::htree_test),
302 )
303 .route("/htree/nhash1:nhash", get(handlers::htree_nhash))
305 .route("/htree/nhash1:nhash/", get(handlers::htree_nhash))
306 .route("/htree/nhash1:nhash/*path", get(handlers::htree_nhash_path))
307 .route("/htree/npub1:npub/:treename", get(handlers::htree_npub))
309 .route("/htree/npub1:npub/:treename/", get(handlers::htree_npub))
310 .route(
311 "/htree/npub1:npub/:treename/*path",
312 get(handlers::htree_npub_path),
313 )
314 .route("/n/:pubkey/:treename", get(handlers::resolve_and_serve))
316 .route("/npub1:rest", get(handlers::serve_npub))
318 .route(
320 "/:id",
321 get(handlers::serve_content_or_blob)
322 .head(blossom::head_blob)
323 .delete(blossom::delete_blob)
324 .options(blossom::cors_preflight),
325 )
326 .route(
327 "/upload",
328 put(blossom::upload_blob).options(blossom::cors_preflight),
329 )
330 .route(
331 "/list/:pubkey",
332 get(blossom::list_blobs).options(blossom::cors_preflight),
333 )
334 .route("/health", get(handlers::health_check))
336 .route("/api/pins", get(handlers::list_pins))
337 .route("/api/stats", get(handlers::storage_stats))
338 .route("/api/peers", get(handlers::webrtc_peers))
339 .route("/api/status", get(handlers::daemon_status))
340 .route("/api/p2p/signal", post(handlers::p2p_signal))
341 .route("/api/socialgraph", get(handlers::socialgraph_stats))
342 .route(
343 "/api/socialgraph/snapshot",
344 get(handlers::socialgraph_snapshot),
345 )
346 .route(
347 "/api/socialgraph/distance/:pubkey",
348 get(handlers::follow_distance),
349 )
350 .route(
352 "/api/resolve/:pubkey/:treename",
353 get(handlers::resolve_to_hash),
354 )
355 .route(
356 "/api/nostr/resolve/:pubkey/:treename",
357 get(handlers::resolve_to_hash),
358 )
359 .route("/api/nostr/profile/:pubkey", get(handlers::nostr_profile))
360 .route("/api/cache-tree-root", post(handlers::cache_tree_root))
361 .route(
362 "/api/clear-tree-root-cache",
363 post(handlers::clear_tree_root_cache),
364 )
365 .route("/api/trees/:pubkey", get(handlers::list_trees))
366 .fallback(get(handlers::serve_virtual_host_fallback))
367 .with_state(state.clone());
368
369 let protected_routes = Router::new()
371 .route("/upload", post(handlers::upload_file))
372 .route("/api/pin/:cid", post(handlers::pin_cid))
373 .route("/api/unpin/:cid", post(handlers::unpin_cid))
374 .route("/api/gc", post(handlers::garbage_collect))
375 .layer(middleware::from_fn_with_state(
376 state.clone(),
377 auth::auth_middleware,
378 ))
379 .with_state(state.clone());
380
381 let mut app = public_routes
382 .merge(protected_routes)
383 .layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)) .layer(middleware::from_fn(status_metrics::record_http_status));
385
386 if let Some(extra) = self.extra_routes {
387 app = app.merge(extra.with_state(state));
388 }
389
390 if let Some(cors) = self.cors {
391 app = app.layer(cors);
392 }
393
394 axum::serve(
395 listener,
396 app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
397 )
398 .with_graceful_shutdown(shutdown)
399 .await?;
400
401 Ok(local_addr.port())
402 }
403
404 pub fn addr(&self) -> &str {
405 &self.addr
406 }
407}
408
409fn current_unix_secs() -> u64 {
410 std::time::SystemTime::now()
411 .duration_since(std::time::UNIX_EPOCH)
412 .unwrap_or(std::time::Duration::ZERO)
413 .as_secs()
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::nostr_relay::{NostrRelay, NostrRelayConfig};
420 use crate::storage::HashtreeStore;
421 use hashtree_core::{from_hex, nhash_encode, DirEntry, HashTree, HashTreeConfig, LinkType};
422 use nostr::{EventBuilder, Keys, Kind, Timestamp};
423 use serde_json::json;
424 use tempfile::TempDir;
425
426 #[tokio::test]
427 async fn test_server_serve_file() -> Result<()> {
428 let temp_dir = TempDir::new()?;
429 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
430
431 let test_file = temp_dir.path().join("test.txt");
433 std::fs::write(&test_file, b"Hello, Hashtree!")?;
434
435 let cid = store.upload_file(&test_file)?;
436 let hash = from_hex(&cid)?;
437
438 let content = store.get_file(&hash)?;
440 assert!(content.is_some());
441 assert_eq!(content.unwrap(), b"Hello, Hashtree!");
442
443 Ok(())
444 }
445
446 #[tokio::test]
447 async fn test_server_list_pins() -> Result<()> {
448 let temp_dir = TempDir::new()?;
449 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
450
451 let test_file = temp_dir.path().join("test.txt");
452 std::fs::write(&test_file, b"Test")?;
453
454 let cid = store.upload_file(&test_file)?;
455 let hash = from_hex(&cid)?;
456
457 let pins = store.list_pins_raw()?;
458 assert_eq!(pins.len(), 1);
459 assert_eq!(pins[0], hash);
460
461 Ok(())
462 }
463
464 async fn spawn_test_server(
465 store: Arc<HashtreeStore>,
466 ) -> Result<(u16, tokio::task::JoinHandle<Result<()>>)> {
467 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
468 let port = listener.local_addr()?.port();
469 let server = HashtreeServer::new(store, "127.0.0.1:0".to_string());
470 let handle =
471 tokio::spawn(async move { server.run_with_listener(listener).await.map(|_| ()) });
472 Ok((port, handle))
473 }
474
475 async fn spawn_test_server_with_nostr_relay(
476 store: Arc<HashtreeStore>,
477 relay: Arc<NostrRelay>,
478 ) -> Result<(u16, tokio::task::JoinHandle<Result<()>>)> {
479 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
480 let port = listener.local_addr()?.port();
481 let server = HashtreeServer::new(store, "127.0.0.1:0".to_string()).with_nostr_relay(relay);
482 let handle =
483 tokio::spawn(async move { server.run_with_listener(listener).await.map(|_| ()) });
484 Ok((port, handle))
485 }
486
487 #[tokio::test]
488 async fn virtual_tree_hosts_serve_root_assets_and_spa_fallbacks() -> Result<()> {
489 clear_virtual_tree_hosts_for_test();
490
491 let temp_dir = TempDir::new()?;
492 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
493 let tree = HashTree::new(HashTreeConfig::new(store.store_arc()).public());
494
495 let (index_cid, _) = tree
496 .put(b"<!doctype html><title>Virtual host ok</title>")
497 .await?;
498 let (favicon_cid, _) = tree.put(b"ico").await?;
499 let (main_js_cid, _) = tree.put(b"console.log('ok');").await?;
500 let assets_dir = tree
501 .put_directory(vec![
502 DirEntry::from_cid("main.js", &main_js_cid).with_link_type(LinkType::File)
503 ])
504 .await?;
505 let root_cid = tree
506 .put_directory(vec![
507 DirEntry::from_cid("index.html", &index_cid).with_link_type(LinkType::File),
508 DirEntry::from_cid("favicon.ico", &favicon_cid).with_link_type(LinkType::File),
509 DirEntry::from_cid("assets", &assets_dir).with_link_type(LinkType::Dir),
510 ])
511 .await?;
512 let nhash = nhash_encode(&root_cid.hash)?;
513 let host = "tree-test.htree.localhost";
514 register_virtual_tree_host(host, &format!("/htree/{nhash}"));
515
516 let (port, handle) = spawn_test_server(store).await?;
517 let base_url = format!("http://127.0.0.1:{port}");
518 let host_header = format!("{host}:{port}");
519 let client = reqwest::Client::new();
520
521 let root_response = client
522 .get(format!("{base_url}/"))
523 .header("Host", &host_header)
524 .header("Accept", "text/html")
525 .send()
526 .await?;
527 assert_eq!(root_response.status(), reqwest::StatusCode::OK);
528 assert_eq!(
529 root_response.bytes().await?.as_ref(),
530 b"<!doctype html><title>Virtual host ok</title>"
531 );
532
533 let favicon_response = client
534 .get(format!("{base_url}/favicon.ico"))
535 .header("Host", &host_header)
536 .send()
537 .await?;
538 assert_eq!(favicon_response.status(), reqwest::StatusCode::OK);
539 assert_eq!(favicon_response.bytes().await?.as_ref(), b"ico");
540
541 let js_response = client
542 .get(format!("{base_url}/assets/main.js"))
543 .header("Host", &host_header)
544 .send()
545 .await?;
546 assert_eq!(js_response.status(), reqwest::StatusCode::OK);
547 assert_eq!(js_response.bytes().await?.as_ref(), b"console.log('ok');");
548
549 let profile_response = client
550 .get(format!("{base_url}/users/npub1example"))
551 .header("Host", &host_header)
552 .header("Accept", "text/html")
553 .send()
554 .await?;
555 assert_eq!(profile_response.status(), reqwest::StatusCode::OK);
556 assert_eq!(
557 profile_response.bytes().await?.as_ref(),
558 b"<!doctype html><title>Virtual host ok</title>"
559 );
560
561 handle.abort();
562 clear_virtual_tree_hosts_for_test();
563
564 Ok(())
565 }
566
567 #[tokio::test]
568 async fn nostr_profile_route_returns_latest_metadata_event() -> Result<()> {
569 let temp_dir = TempDir::new()?;
570 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
571 let graph_store = {
572 let _guard = crate::socialgraph::test_lock();
573 crate::socialgraph::open_social_graph_store_with_mapsize(
574 &temp_dir.path().join("relay-db"),
575 Some(128 * 1024 * 1024),
576 )?
577 };
578 let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store;
579 let relay = Arc::new(NostrRelay::new(
580 backend,
581 temp_dir.path().to_path_buf(),
582 HashSet::new(),
583 None,
584 NostrRelayConfig {
585 spambox_db_max_bytes: 0,
586 ..Default::default()
587 },
588 )?);
589
590 let author = Keys::generate();
591 let older = EventBuilder::new(
592 Kind::Metadata,
593 json!({ "name": "older", "about": "before" }).to_string(),
594 [],
595 )
596 .custom_created_at(Timestamp::from_secs(10))
597 .to_event(&author)?;
598 let newer = EventBuilder::new(
599 Kind::Metadata,
600 json!({ "name": "newer", "about": "after" }).to_string(),
601 [],
602 )
603 .custom_created_at(Timestamp::from_secs(20))
604 .to_event(&author)?;
605
606 relay.ingest_trusted_event(older).await?;
607 relay.ingest_trusted_event(newer.clone()).await?;
608
609 let (port, handle) = spawn_test_server_with_nostr_relay(store, relay).await?;
610 let response = reqwest::get(format!(
611 "http://127.0.0.1:{port}/api/nostr/profile/{}",
612 author.public_key().to_hex()
613 ))
614 .await?;
615
616 assert_eq!(response.status(), reqwest::StatusCode::OK);
617 let payload: serde_json::Value = response.json().await?;
618 assert_eq!(payload["profile"]["name"].as_str(), Some("newer"),);
619 assert_eq!(payload["profile"]["about"].as_str(), Some("after"));
620 assert_eq!(payload["created_at"].as_u64(), Some(20));
621 let expected_event_id = newer.id.to_hex();
622 assert_eq!(
623 payload["event_id"].as_str(),
624 Some(expected_event_id.as_str())
625 );
626
627 handle.abort();
628 Ok(())
629 }
630}