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