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 body::Body,
23 extract::DefaultBodyLimit,
24 http::Request,
25 middleware,
26 response::Response,
27 routing::{get, post, put},
28 Router,
29};
30use futures::{future::poll_fn, pin_mut, FutureExt};
31use hyper::body::Incoming;
32use hyper_util::{
33 rt::{TokioExecutor, TokioIo, TokioTimer},
34 server::conn::auto::Builder as HyperBuilder,
35 service::TowerToHyperService,
36};
37use socket2::{SockRef, TcpKeepalive};
38use std::collections::{HashMap, HashSet};
39use std::convert::Infallible;
40use std::future;
41use std::io;
42use std::net::SocketAddr;
43use std::sync::{Arc, OnceLock, RwLock};
44use std::time::Duration;
45use tokio::sync::watch;
46use tower::{Service, ServiceExt as _};
47use tower_http::cors::CorsLayer;
48use tracing::{debug, error, trace};
49
50pub use auth::{new_lookup_cache, AppState, AuthCredentials, CachedTreeRootEntry};
51
52static VIRTUAL_TREE_HOSTS: OnceLock<RwLock<HashMap<String, String>>> = OnceLock::new();
53const DEFAULT_OPTIMISTIC_UPLOAD_QUEUE_BYTES: usize = 512 * 1024 * 1024;
54
55#[cfg(not(test))]
56const HTTP1_HEADER_READ_TIMEOUT: Duration = Duration::from_secs(30);
57#[cfg(test)]
58const HTTP1_HEADER_READ_TIMEOUT: Duration = Duration::from_millis(200);
59const HTTP2_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(30);
60const HTTP2_KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(10);
61const TCP_KEEPALIVE_TIME: Duration = Duration::from_secs(60);
62const TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(15);
63
64fn virtual_tree_hosts() -> &'static RwLock<HashMap<String, String>> {
65 VIRTUAL_TREE_HOSTS.get_or_init(|| RwLock::new(HashMap::new()))
66}
67
68fn normalize_virtual_tree_host(host: &str) -> Option<String> {
69 let trimmed = host.trim().trim_end_matches('.').to_ascii_lowercase();
70 if trimmed.is_empty() {
71 return None;
72 }
73
74 if let Some(stripped) = trimmed
75 .strip_prefix('[')
76 .and_then(|value| value.split_once(']'))
77 {
78 let host_only = stripped.0.trim();
79 if host_only.is_empty() {
80 return None;
81 }
82 return Some(host_only.to_string());
83 }
84
85 if let Some((host_only, port)) = trimmed.rsplit_once(':') {
86 if !host_only.is_empty() && !port.is_empty() && port.chars().all(|ch| ch.is_ascii_digit()) {
87 return Some(host_only.to_string());
88 }
89 }
90
91 Some(trimmed)
92}
93
94pub fn register_virtual_tree_host(host: &str, internal_root: &str) {
95 let Some(normalized_host) = normalize_virtual_tree_host(host) else {
96 return;
97 };
98
99 let normalized_root = internal_root.trim().trim_end_matches('/');
100 if normalized_root.is_empty() {
101 return;
102 }
103
104 if let Ok(mut hosts) = virtual_tree_hosts().write() {
105 hosts.insert(normalized_host, normalized_root.to_string());
106 }
107}
108
109pub fn resolve_virtual_tree_host(host: &str) -> Option<String> {
110 let normalized_host = normalize_virtual_tree_host(host)?;
111 virtual_tree_hosts()
112 .read()
113 .ok()
114 .and_then(|hosts| hosts.get(&normalized_host).cloned())
115}
116
117#[cfg(test)]
118pub fn clear_virtual_tree_hosts_for_test() {
119 if let Ok(mut hosts) = virtual_tree_hosts().write() {
120 hosts.clear();
121 }
122}
123
124pub struct HashtreeServer {
125 state: AppState,
126 addr: String,
127 extra_routes: Option<Router<AppState>>,
128 cors: Option<CorsLayer>,
129}
130
131impl HashtreeServer {
132 pub fn new(store: Arc<HashtreeStore>, addr: String) -> Self {
133 Self {
134 state: AppState {
135 store,
136 auth: None,
137 daemon_started_at: current_unix_secs(),
138 peer_mode: crate::config::ServerMode::Normal,
139 hash_get_enabled: true,
140 http_webrtc_fetch: true,
141 webrtc_peers: None,
142 fips_transport: None,
143 fetch_from_fips_peers: true,
144 ws_relay: Arc::new(auth::WsRelayState::new()),
145 max_upload_bytes: 5 * 1024 * 1024, public_writes: true, public_plaintext_reads: true,
148 require_random_untrusted_ingest: true,
149 optimistic_blossom_uploads: false,
150 optimistic_upload_queue_bytes: DEFAULT_OPTIMISTIC_UPLOAD_QUEUE_BYTES,
151 optimistic_upload_queue: Arc::new(tokio::sync::Semaphore::new(
152 DEFAULT_OPTIMISTIC_UPLOAD_QUEUE_BYTES,
153 )),
154 allowed_pubkeys: HashSet::new(), upstream_blossom: Vec::new(),
156 social_graph: None,
157 social_graph_store: None,
158 social_graph_root: None,
159 socialgraph_snapshot_public: false,
160 nostr_relay: None,
161 nostr_relay_urls: Vec::new(),
162 tree_root_cache: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
163 inflight_blob_fetches: Arc::new(tokio::sync::Mutex::new(
164 std::collections::HashMap::new(),
165 )),
166 inflight_blob_reads: Arc::new(tokio::sync::Mutex::new(
167 std::collections::HashMap::new(),
168 )),
169 blob_cache: Arc::new(crate::blob_cache::BlobCache::from_env()),
170 directory_listing_cache: Arc::new(std::sync::Mutex::new(new_lookup_cache())),
171 resolved_path_cache: Arc::new(std::sync::Mutex::new(new_lookup_cache())),
172 thumbnail_path_cache: Arc::new(std::sync::Mutex::new(new_lookup_cache())),
173 cid_size_cache: Arc::new(std::sync::Mutex::new(new_lookup_cache())),
174 },
175 addr,
176 extra_routes: None,
177 cors: None,
178 }
179 }
180
181 pub fn with_max_upload_bytes(mut self, bytes: usize) -> Self {
183 self.state.max_upload_bytes = bytes;
184 self
185 }
186
187 pub fn with_public_writes(mut self, public: bool) -> Self {
190 self.state.public_writes = public;
191 self
192 }
193
194 pub fn with_public_plaintext_reads(mut self, public: bool) -> Self {
196 self.state.public_plaintext_reads = public;
197 self
198 }
199
200 pub fn with_require_random_untrusted_ingest(mut self, require: bool) -> Self {
201 self.state.require_random_untrusted_ingest = require;
202 self
203 }
204
205 pub fn with_optimistic_blossom_uploads(mut self, enabled: bool) -> Self {
206 self.state.optimistic_blossom_uploads = enabled;
207 self
208 }
209
210 pub fn with_server_mode(mut self, mode: crate::config::ServerMode) -> Self {
211 self.state.peer_mode = mode;
212 self
213 }
214
215 pub fn with_hash_get_enabled(mut self, enabled: bool) -> Self {
216 self.state.hash_get_enabled = enabled;
217 self
218 }
219
220 pub fn with_http_webrtc_fetch(mut self, enabled: bool) -> Self {
221 self.state.http_webrtc_fetch = enabled;
222 self
223 }
224
225 pub fn with_fetch_from_fips_peers(mut self, enabled: bool) -> Self {
226 self.state.fetch_from_fips_peers = enabled;
227 self
228 }
229
230 pub fn with_fips_transport(
231 mut self,
232 transport: Arc<crate::fips_transport::DaemonFipsTransport>,
233 ) -> Self {
234 self.state.fips_transport = Some(transport);
235 self
236 }
237
238 pub fn with_webrtc_peers(mut self, webrtc_state: Arc<WebRTCState>) -> Self {
240 self.state.webrtc_peers = Some(webrtc_state);
241 self
242 }
243
244 pub fn with_auth(mut self, username: String, password: String) -> Self {
245 self.state.auth = Some(AuthCredentials { username, password });
246 self
247 }
248
249 pub fn with_allowed_pubkeys(mut self, pubkeys: HashSet<String>) -> Self {
251 self.state.allowed_pubkeys = pubkeys;
252 self
253 }
254
255 pub fn with_upstream_blossom(mut self, servers: Vec<String>) -> Self {
257 self.state.upstream_blossom = servers;
258 self
259 }
260
261 pub fn with_social_graph(mut self, sg: Arc<socialgraph::SocialGraphAccessControl>) -> Self {
263 self.state.social_graph = Some(sg);
264 self
265 }
266
267 pub fn with_socialgraph_snapshot(
269 mut self,
270 store: Arc<dyn socialgraph::SocialGraphBackend>,
271 root: [u8; 32],
272 public: bool,
273 ) -> Self {
274 self.state.social_graph_store = Some(store);
275 self.state.social_graph_root = Some(root);
276 self.state.socialgraph_snapshot_public = public;
277 self
278 }
279
280 pub fn with_nostr_relay(mut self, relay: Arc<NostrRelay>) -> Self {
282 self.state.nostr_relay = Some(relay);
283 self
284 }
285
286 pub fn with_nostr_relay_urls(mut self, relays: Vec<String>) -> Self {
288 self.state.nostr_relay_urls = relays;
289 self
290 }
291
292 pub fn with_extra_routes(mut self, routes: Router<AppState>) -> Self {
294 self.extra_routes = Some(routes);
295 self
296 }
297
298 pub fn with_cors(mut self, cors: CorsLayer) -> Self {
300 self.cors = Some(cors);
301 self
302 }
303
304 pub async fn run(self) -> Result<()> {
305 let listener = tokio::net::TcpListener::bind(&self.addr).await?;
306 let _ = self.run_with_listener(listener).await?;
307 Ok(())
308 }
309
310 pub async fn run_with_listener(self, listener: tokio::net::TcpListener) -> Result<u16> {
311 self.run_with_listener_until(listener, future::pending::<()>())
312 .await
313 }
314
315 pub async fn run_with_listener_until<F>(
316 self,
317 listener: tokio::net::TcpListener,
318 shutdown: F,
319 ) -> Result<u16>
320 where
321 F: std::future::Future<Output = ()> + Send + 'static,
322 {
323 let local_addr = listener.local_addr()?;
324
325 let state = self.state.clone();
329 let public_routes = Router::new()
330 .route("/", get(handlers::serve_root_or_virtual_host))
331 .route("/ws", get(ws_relay::ws_data))
332 .route("/ws/", get(ws_relay::ws_data))
333 .route(
334 "/htree/test",
335 get(handlers::htree_test).head(handlers::htree_test),
336 )
337 .route("/htree/nhash1:nhash", get(handlers::htree_nhash))
339 .route("/htree/nhash1:nhash/", get(handlers::htree_nhash))
340 .route("/htree/nhash1:nhash/*path", get(handlers::htree_nhash_path))
341 .route("/htree/npub1:npub/:treename", get(handlers::htree_npub))
343 .route("/htree/npub1:npub/:treename/", get(handlers::htree_npub))
344 .route(
345 "/htree/npub1:npub/:treename/*path",
346 get(handlers::htree_npub_path),
347 )
348 .route("/n/:pubkey/:treename", get(handlers::resolve_and_serve))
350 .route("/npub1:rest", get(handlers::serve_npub))
352 .route("/npub1:rest/*path", get(handlers::serve_npub))
353 .route(
355 "/:id",
356 get(handlers::serve_content_or_blob)
357 .head(blossom::head_blob)
358 .delete(blossom::delete_blob)
359 .options(blossom::cors_preflight),
360 )
361 .route(
362 "/upload",
363 put(blossom::upload_blob).options(blossom::cors_preflight),
364 )
365 .route(
366 "/upload/batch",
367 post(blossom::upload_blob_batch).options(blossom::cors_preflight),
368 )
369 .route(
370 "/upload/check",
371 post(blossom::upload_check).options(blossom::cors_preflight),
372 )
373 .route(
374 "/list/:pubkey",
375 get(blossom::list_blobs).options(blossom::cors_preflight),
376 )
377 .route("/health", get(handlers::health_check))
379 .route("/api/pins", get(handlers::list_pins))
380 .route("/api/stats", get(handlers::storage_stats))
381 .route("/api/peers", get(handlers::webrtc_peers))
382 .route("/api/status", get(handlers::daemon_status))
383 .route("/api/p2p/signal", post(handlers::p2p_signal))
384 .route("/api/socialgraph", get(handlers::socialgraph_stats))
385 .route(
386 "/api/socialgraph/snapshot",
387 get(handlers::socialgraph_snapshot),
388 )
389 .route(
390 "/api/socialgraph/distance/:pubkey",
391 get(handlers::follow_distance),
392 )
393 .route(
395 "/api/resolve/:pubkey/:treename",
396 get(handlers::resolve_to_hash),
397 )
398 .route(
399 "/api/nostr/resolve/:pubkey/:treename",
400 get(handlers::resolve_to_hash),
401 )
402 .route("/api/nostr/profile/:pubkey", get(handlers::nostr_profile))
403 .route("/api/cache-tree-root", post(handlers::cache_tree_root))
404 .route(
405 "/api/clear-tree-root-cache",
406 post(handlers::clear_tree_root_cache),
407 )
408 .route("/api/trees/:pubkey", get(handlers::list_trees))
409 .fallback(get(handlers::serve_virtual_host_fallback))
410 .with_state(state.clone());
411
412 let protected_routes = Router::new()
414 .route("/upload", post(handlers::upload_file))
415 .route("/api/pin/:cid", post(handlers::pin_cid))
416 .route("/api/unpin/:cid", post(handlers::unpin_cid))
417 .route("/api/gc", post(handlers::garbage_collect))
418 .layer(middleware::from_fn_with_state(
419 state.clone(),
420 auth::auth_middleware,
421 ))
422 .with_state(state.clone());
423
424 let mut app = public_routes
425 .merge(protected_routes)
426 .layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)) .layer(middleware::from_fn(status_metrics::record_http_status));
428
429 if let Some(extra) = self.extra_routes {
430 app = app.merge(extra.with_state(state));
431 }
432
433 if let Some(cors) = self.cors {
434 app = app.layer(cors);
435 }
436
437 let make_service = app.into_make_service_with_connect_info::<std::net::SocketAddr>();
438 serve_with_connection_limits(listener, make_service, shutdown).await?;
439
440 Ok(local_addr.port())
441 }
442
443 pub fn addr(&self) -> &str {
444 &self.addr
445 }
446}
447
448async fn serve_with_connection_limits<M, S, F>(
449 listener: tokio::net::TcpListener,
450 mut make_service: M,
451 shutdown: F,
452) -> io::Result<()>
453where
454 M: Service<SocketAddr, Error = Infallible, Response = S> + Send + 'static,
455 M::Future: Send,
456 S: Service<Request<Body>, Response = Response, Error = Infallible> + Clone + Send + 'static,
457 S::Future: Send,
458 F: std::future::Future<Output = ()> + Send + 'static,
459{
460 let (signal_tx, signal_rx) = watch::channel(());
461 let signal_tx = Arc::new(signal_tx);
462 tokio::spawn(async move {
463 shutdown.await;
464 trace!("received graceful shutdown signal; stopping daemon listener");
465 drop(signal_rx);
466 });
467
468 let (close_tx, close_rx) = watch::channel(());
469
470 loop {
471 let (tcp_stream, remote_addr) = tokio::select! {
472 accepted = accept_tcp(&listener) => {
473 match accepted {
474 Some(connection) => connection,
475 None => continue,
476 }
477 }
478 _ = signal_tx.closed() => {
479 trace!("shutdown signal received; no longer accepting daemon connections");
480 break;
481 }
482 };
483
484 configure_tcp_stream(&tcp_stream);
485 let tcp_stream = TokioIo::new(tcp_stream);
486
487 poll_fn(|cx| make_service.poll_ready(cx))
488 .await
489 .unwrap_or_else(|err| match err {});
490
491 let tower_service = make_service
492 .call(remote_addr)
493 .await
494 .unwrap_or_else(|err| match err {})
495 .map_request(|req: Request<Incoming>| req.map(Body::new));
496 let hyper_service = TowerToHyperService::new(tower_service);
497
498 let signal_tx = Arc::clone(&signal_tx);
499 let close_rx = close_rx.clone();
500
501 tokio::spawn(async move {
502 let mut builder = HyperBuilder::new(TokioExecutor::new());
503 builder
504 .http1()
505 .timer(TokioTimer::new())
506 .header_read_timeout(HTTP1_HEADER_READ_TIMEOUT);
507 builder
508 .http2()
509 .timer(TokioTimer::new())
510 .keep_alive_interval(Some(HTTP2_KEEPALIVE_INTERVAL))
511 .keep_alive_timeout(HTTP2_KEEPALIVE_TIMEOUT);
512
513 let conn = builder.serve_connection_with_upgrades(tcp_stream, hyper_service);
514 pin_mut!(conn);
515
516 let signal_closed = signal_tx.closed().fuse();
517 pin_mut!(signal_closed);
518
519 loop {
520 tokio::select! {
521 result = conn.as_mut() => {
522 if let Err(err) = result {
523 trace!("daemon connection closed with error: {err:#}");
524 }
525 break;
526 }
527 _ = &mut signal_closed => {
528 trace!("shutdown signal received by connection task");
529 conn.as_mut().graceful_shutdown();
530 }
531 }
532 }
533
534 drop(close_rx);
535 });
536 }
537
538 drop(close_rx);
539 drop(listener);
540 close_tx.closed().await;
541
542 Ok(())
543}
544
545fn configure_tcp_stream(tcp_stream: &tokio::net::TcpStream) {
546 if let Err(err) = tcp_stream.set_nodelay(true) {
547 debug!("failed to set TCP_NODELAY on daemon connection: {err:#}");
548 }
549
550 let socket = SockRef::from(tcp_stream);
551 if let Err(err) = socket.set_tcp_keepalive(
552 &TcpKeepalive::new()
553 .with_time(TCP_KEEPALIVE_TIME)
554 .with_interval(TCP_KEEPALIVE_INTERVAL),
555 ) {
556 debug!("failed to set TCP keepalive on daemon connection: {err:#}");
557 }
558}
559
560async fn accept_tcp(
561 listener: &tokio::net::TcpListener,
562) -> Option<(tokio::net::TcpStream, SocketAddr)> {
563 match listener.accept().await {
564 Ok(connection) => Some(connection),
565 Err(err) => {
566 if is_connection_error(&err) {
567 return None;
568 }
569 error!("daemon accept error: {err}");
570 tokio::time::sleep(Duration::from_secs(1)).await;
571 None
572 }
573 }
574}
575
576fn is_connection_error(err: &io::Error) -> bool {
577 matches!(
578 err.kind(),
579 io::ErrorKind::ConnectionRefused
580 | io::ErrorKind::ConnectionAborted
581 | io::ErrorKind::ConnectionReset
582 )
583}
584
585fn current_unix_secs() -> u64 {
586 std::time::SystemTime::now()
587 .duration_since(std::time::UNIX_EPOCH)
588 .unwrap_or(std::time::Duration::ZERO)
589 .as_secs()
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595 use crate::nostr_relay::{NostrRelay, NostrRelayConfig};
596 use crate::storage::HashtreeStore;
597 use hashtree_core::{from_hex, nhash_encode, DirEntry, HashTree, HashTreeConfig, LinkType};
598 use nostr::{EventBuilder, Keys, Kind, Timestamp};
599 use serde_json::json;
600 use tempfile::TempDir;
601
602 #[tokio::test]
603 async fn test_server_serve_file() -> Result<()> {
604 let temp_dir = TempDir::new()?;
605 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
606
607 let test_file = temp_dir.path().join("test.txt");
609 std::fs::write(&test_file, b"Hello, Hashtree!")?;
610
611 let cid = store.upload_file(&test_file)?;
612 let hash = from_hex(&cid)?;
613
614 let content = store.get_file(&hash)?;
616 assert!(content.is_some());
617 assert_eq!(content.unwrap(), b"Hello, Hashtree!");
618
619 Ok(())
620 }
621
622 #[tokio::test]
623 async fn test_server_list_pins() -> Result<()> {
624 let temp_dir = TempDir::new()?;
625 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
626
627 let test_file = temp_dir.path().join("test.txt");
628 std::fs::write(&test_file, b"Test")?;
629
630 let cid = store.upload_file(&test_file)?;
631 let hash = from_hex(&cid)?;
632
633 let pins = store.list_pins_raw()?;
634 assert_eq!(pins.len(), 1);
635 assert_eq!(pins[0], hash);
636
637 Ok(())
638 }
639
640 async fn spawn_test_server(
641 store: Arc<HashtreeStore>,
642 ) -> Result<(u16, tokio::task::JoinHandle<Result<()>>)> {
643 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
644 let port = listener.local_addr()?.port();
645 let server = HashtreeServer::new(store, "127.0.0.1:0".to_string());
646 let handle =
647 tokio::spawn(async move { server.run_with_listener(listener).await.map(|_| ()) });
648 Ok((port, handle))
649 }
650
651 async fn spawn_test_server_with_nostr_relay(
652 store: Arc<HashtreeStore>,
653 relay: Arc<NostrRelay>,
654 ) -> Result<(u16, tokio::task::JoinHandle<Result<()>>)> {
655 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
656 let port = listener.local_addr()?.port();
657 let server = HashtreeServer::new(store, "127.0.0.1:0".to_string()).with_nostr_relay(relay);
658 let handle =
659 tokio::spawn(async move { server.run_with_listener(listener).await.map(|_| ()) });
660 Ok((port, handle))
661 }
662
663 #[tokio::test]
664 async fn virtual_tree_hosts_serve_root_assets_and_spa_fallbacks() -> Result<()> {
665 clear_virtual_tree_hosts_for_test();
666
667 let temp_dir = TempDir::new()?;
668 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
669 let tree = HashTree::new(HashTreeConfig::new(store.store_arc()).public());
670
671 let (index_cid, _) = tree
672 .put(b"<!doctype html><title>Virtual host ok</title>")
673 .await?;
674 let (favicon_cid, _) = tree.put(b"ico").await?;
675 let (main_js_cid, _) = tree.put(b"console.log('ok');").await?;
676 let assets_dir = tree
677 .put_directory(vec![
678 DirEntry::from_cid("main.js", &main_js_cid).with_link_type(LinkType::File)
679 ])
680 .await?;
681 let root_cid = tree
682 .put_directory(vec![
683 DirEntry::from_cid("index.html", &index_cid).with_link_type(LinkType::File),
684 DirEntry::from_cid("favicon.ico", &favicon_cid).with_link_type(LinkType::File),
685 DirEntry::from_cid("assets", &assets_dir).with_link_type(LinkType::Dir),
686 ])
687 .await?;
688 let nhash = nhash_encode(&root_cid.hash)?;
689 let host = "tree-test.htree.localhost";
690 register_virtual_tree_host(host, &format!("/htree/{nhash}"));
691
692 let (port, handle) = spawn_test_server(store).await?;
693 let base_url = format!("http://127.0.0.1:{port}");
694 let host_header = format!("{host}:{port}");
695 let client = reqwest::Client::new();
696
697 let root_response = client
698 .get(format!("{base_url}/"))
699 .header("Host", &host_header)
700 .header("Accept", "text/html")
701 .send()
702 .await?;
703 assert_eq!(root_response.status(), reqwest::StatusCode::OK);
704 assert_eq!(
705 root_response.bytes().await?.as_ref(),
706 b"<!doctype html><title>Virtual host ok</title>"
707 );
708
709 let favicon_response = client
710 .get(format!("{base_url}/favicon.ico"))
711 .header("Host", &host_header)
712 .send()
713 .await?;
714 assert_eq!(favicon_response.status(), reqwest::StatusCode::OK);
715 assert_eq!(favicon_response.bytes().await?.as_ref(), b"ico");
716
717 let js_response = client
718 .get(format!("{base_url}/assets/main.js"))
719 .header("Host", &host_header)
720 .send()
721 .await?;
722 assert_eq!(js_response.status(), reqwest::StatusCode::OK);
723 assert_eq!(js_response.bytes().await?.as_ref(), b"console.log('ok');");
724
725 let profile_response = client
726 .get(format!("{base_url}/users/npub1example"))
727 .header("Host", &host_header)
728 .header("Accept", "text/html")
729 .send()
730 .await?;
731 assert_eq!(profile_response.status(), reqwest::StatusCode::OK);
732 assert_eq!(
733 profile_response.bytes().await?.as_ref(),
734 b"<!doctype html><title>Virtual host ok</title>"
735 );
736
737 handle.abort();
738 clear_virtual_tree_hosts_for_test();
739
740 Ok(())
741 }
742
743 #[tokio::test]
744 async fn nostr_profile_route_returns_latest_metadata_event() -> Result<()> {
745 let temp_dir = TempDir::new()?;
746 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
747 let graph_store = {
748 let _guard = crate::socialgraph::test_lock();
749 crate::socialgraph::open_social_graph_store_with_mapsize(
750 &temp_dir.path().join("relay-db"),
751 Some(128 * 1024 * 1024),
752 )?
753 };
754 let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store;
755 let relay = Arc::new(NostrRelay::new(
756 backend,
757 temp_dir.path().to_path_buf(),
758 HashSet::new(),
759 None,
760 NostrRelayConfig {
761 spambox_db_max_bytes: 0,
762 ..Default::default()
763 },
764 )?);
765
766 let author = Keys::generate();
767 let older = EventBuilder::new(
768 Kind::Metadata,
769 json!({ "name": "older", "about": "before" }).to_string(),
770 )
771 .custom_created_at(Timestamp::from_secs(10))
772 .sign_with_keys(&author)?;
773 let newer = EventBuilder::new(
774 Kind::Metadata,
775 json!({ "name": "newer", "about": "after" }).to_string(),
776 )
777 .custom_created_at(Timestamp::from_secs(20))
778 .sign_with_keys(&author)?;
779
780 relay.ingest_trusted_event(older).await?;
781 relay.ingest_trusted_event(newer.clone()).await?;
782
783 let (port, handle) = spawn_test_server_with_nostr_relay(store, relay).await?;
784 let response = reqwest::get(format!(
785 "http://127.0.0.1:{port}/api/nostr/profile/{}",
786 author.public_key().to_hex()
787 ))
788 .await?;
789
790 assert_eq!(response.status(), reqwest::StatusCode::OK);
791 let payload: serde_json::Value = response.json().await?;
792 assert_eq!(payload["profile"]["name"].as_str(), Some("newer"),);
793 assert_eq!(payload["profile"]["about"].as_str(), Some("after"));
794 assert_eq!(payload["created_at"].as_u64(), Some(20));
795 let expected_event_id = newer.id.to_hex();
796 assert_eq!(
797 payload["event_id"].as_str(),
798 Some(expected_event_id.as_str())
799 );
800
801 handle.abort();
802 Ok(())
803 }
804}