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 hashtree_core::Cid;
32use hyper::body::Incoming;
33use hyper_util::{
34 rt::{TokioExecutor, TokioIo, TokioTimer},
35 server::conn::auto::Builder as HyperBuilder,
36 service::TowerToHyperService,
37};
38use socket2::{SockRef, TcpKeepalive};
39use std::collections::{HashMap, HashSet};
40use std::convert::Infallible;
41use std::future;
42use std::io;
43use std::net::SocketAddr;
44use std::sync::{Arc, OnceLock, RwLock};
45use std::time::{Duration, Instant};
46use tokio::sync::watch;
47use tower::{Service, ServiceExt as _};
48use tower_http::cors::CorsLayer;
49use tracing::{debug, error, trace};
50
51pub use auth::{
52 new_lookup_cache, new_upstream_http_client, AppState, AuthCredentials, CachedTreeRootEntry,
53};
54
55static VIRTUAL_TREE_HOSTS: OnceLock<RwLock<HashMap<String, String>>> = OnceLock::new();
56const DEFAULT_OPTIMISTIC_UPLOAD_QUEUE_BYTES: usize = 256 * 1024 * 1024;
57const DEFAULT_BLOSSOM_UPLOAD_REPLICA_QUEUE_BYTES: usize = 256 * 1024 * 1024;
58const INTERNAL_JSON_BODY_LIMIT_BYTES: usize = 64 * 1024;
59
60#[cfg(not(test))]
61const HTTP1_HEADER_READ_TIMEOUT: Duration = Duration::from_secs(30);
62#[cfg(test)]
63const HTTP1_HEADER_READ_TIMEOUT: Duration = Duration::from_millis(200);
64const HTTP2_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(30);
65const HTTP2_KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(10);
66const TCP_KEEPALIVE_TIME: Duration = Duration::from_secs(60);
67const TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(15);
68
69pub fn bounded_upload_queue_bytes(bytes: u64) -> usize {
70 usize::try_from(bytes)
71 .unwrap_or(usize::MAX)
72 .clamp(1, tokio::sync::Semaphore::MAX_PERMITS)
73}
74
75fn virtual_tree_hosts() -> &'static RwLock<HashMap<String, String>> {
76 VIRTUAL_TREE_HOSTS.get_or_init(|| RwLock::new(HashMap::new()))
77}
78
79fn normalize_virtual_tree_host(host: &str) -> Option<String> {
80 let trimmed = host.trim().trim_end_matches('.').to_ascii_lowercase();
81 if trimmed.is_empty() {
82 return None;
83 }
84
85 if let Some(stripped) = trimmed
86 .strip_prefix('[')
87 .and_then(|value| value.split_once(']'))
88 {
89 let host_only = stripped.0.trim();
90 if host_only.is_empty() {
91 return None;
92 }
93 return Some(host_only.to_string());
94 }
95
96 if let Some((host_only, port)) = trimmed.rsplit_once(':') {
97 if !host_only.is_empty() && !port.is_empty() && port.chars().all(|ch| ch.is_ascii_digit()) {
98 return Some(host_only.to_string());
99 }
100 }
101
102 Some(trimmed)
103}
104
105pub fn register_virtual_tree_host(host: &str, internal_root: &str) {
106 let Some(normalized_host) = normalize_virtual_tree_host(host) else {
107 return;
108 };
109
110 let normalized_root = internal_root.trim().trim_end_matches('/');
111 if normalized_root.is_empty() {
112 return;
113 }
114
115 if let Ok(mut hosts) = virtual_tree_hosts().write() {
116 hosts.insert(normalized_host, normalized_root.to_string());
117 }
118}
119
120pub fn resolve_virtual_tree_host(host: &str) -> Option<String> {
121 let normalized_host = normalize_virtual_tree_host(host)?;
122 let configured = virtual_tree_hosts()
123 .read()
124 .ok()
125 .and_then(|hosts| hosts.get(&normalized_host).cloned());
126 configured.or_else(|| resolve_iris_localhost_tree_root(&normalized_host))
127}
128
129fn resolve_iris_localhost_tree_root(host: &str) -> Option<String> {
130 let labels: Vec<&str> = host.strip_suffix(".iris.localhost")?.split('.').collect();
131 match labels.as_slice() {
132 [nhash] if nhash.starts_with("nhash1") => Some(format!("/htree/{nhash}")),
133 [site, npub] if !site.is_empty() && npub.starts_with("npub1") => {
134 Some(format!("/htree/{npub}/{site}"))
135 }
136 _ => None,
137 }
138}
139
140#[cfg(test)]
141pub fn clear_virtual_tree_hosts_for_test() {
142 if let Ok(mut hosts) = virtual_tree_hosts().write() {
143 hosts.clear();
144 }
145}
146
147pub struct HashtreeServer {
148 state: AppState,
149 addr: String,
150 extra_routes: Option<Router<AppState>>,
151 cors: Option<CorsLayer>,
152}
153
154impl HashtreeServer {
155 pub fn new(store: Arc<HashtreeStore>, addr: String) -> Self {
156 Self {
157 state: AppState {
158 store,
159 auth: None,
160 daemon_started_at: current_unix_secs(),
161 peer_mode: crate::config::ServerMode::Normal,
162 hash_get_enabled: true,
163 http_webrtc_fetch: true,
164 webrtc_peers: None,
165 fips_endpoint: None,
166 fips_blob_resolver: None,
167 fetch_from_fips_peers: true,
168 ws_relay: Arc::new(auth::WsRelayState::new()),
169 max_upload_bytes: 5 * 1024 * 1024, public_writes: true, public_plaintext_reads: false,
172 require_random_untrusted_ingest: true,
173 optimistic_blossom_uploads: false,
174 optimistic_upload_queue_bytes: DEFAULT_OPTIMISTIC_UPLOAD_QUEUE_BYTES,
175 optimistic_upload_queue: Arc::new(tokio::sync::Semaphore::new(
176 DEFAULT_OPTIMISTIC_UPLOAD_QUEUE_BYTES,
177 )),
178 allowed_pubkeys: HashSet::new(), upstream_blossom: Vec::new(),
180 upstream_http_client: new_upstream_http_client(),
181 upstream_blossom_miss_cache: Arc::new(std::sync::Mutex::new(new_lookup_cache())),
182 upstream_blossom_fetch_metrics: Arc::new(
183 auth::UpstreamBlossomFetchMetrics::default(),
184 ),
185 blossom_upload_replicas: Vec::new(),
186 blossom_upload_replica_queue_bytes: DEFAULT_BLOSSOM_UPLOAD_REPLICA_QUEUE_BYTES,
187 blossom_upload_replica_queue: Arc::new(tokio::sync::Semaphore::new(
188 DEFAULT_BLOSSOM_UPLOAD_REPLICA_QUEUE_BYTES,
189 )),
190 blossom_upload_replica_keys: None,
191 blossom_upload_replica_scheduler: Arc::new(
192 blossom::BlossomUploadReplicaScheduler::new(),
193 ),
194 social_graph: None,
195 social_graph_store: None,
196 social_graph_root: None,
197 socialgraph_snapshot_public: false,
198 nostr_relay: None,
199 nostr_provider: None,
200 nostr_relay_urls: Vec::new(),
201 tree_root_cache: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
202 inflight_blob_fetches: Arc::new(tokio::sync::Mutex::new(
203 std::collections::HashMap::new(),
204 )),
205 inflight_blob_reads: Arc::new(tokio::sync::Mutex::new(
206 std::collections::HashMap::new(),
207 )),
208 blob_cache: Arc::new(crate::blob_cache::BlobCache::from_env()),
209 directory_listing_cache: Arc::new(std::sync::Mutex::new(new_lookup_cache())),
210 resolved_path_cache: Arc::new(std::sync::Mutex::new(new_lookup_cache())),
211 thumbnail_path_cache: Arc::new(std::sync::Mutex::new(new_lookup_cache())),
212 cid_size_cache: Arc::new(std::sync::Mutex::new(new_lookup_cache())),
213 },
214 addr,
215 extra_routes: None,
216 cors: None,
217 }
218 }
219
220 pub fn with_max_upload_bytes(mut self, bytes: usize) -> Self {
222 self.state.max_upload_bytes = bytes;
223 self
224 }
225
226 pub fn with_public_writes(mut self, public: bool) -> Self {
229 self.state.public_writes = public;
230 self
231 }
232
233 pub fn with_public_plaintext_reads(mut self, public: bool) -> Self {
235 self.state.public_plaintext_reads = public;
236 self
237 }
238
239 pub fn with_require_random_untrusted_ingest(mut self, require: bool) -> Self {
240 self.state.require_random_untrusted_ingest = require;
241 self
242 }
243
244 pub fn with_optimistic_blossom_uploads(mut self, enabled: bool) -> Self {
245 self.state.optimistic_blossom_uploads = enabled;
246 self
247 }
248
249 pub fn with_server_mode(mut self, mode: crate::config::ServerMode) -> Self {
250 self.state.peer_mode = mode;
251 self
252 }
253
254 pub fn with_hash_get_enabled(mut self, enabled: bool) -> Self {
255 self.state.hash_get_enabled = enabled;
256 self
257 }
258
259 pub fn with_http_webrtc_fetch(mut self, enabled: bool) -> Self {
260 self.state.http_webrtc_fetch = enabled;
261 self
262 }
263
264 pub fn with_fetch_from_fips_peers(mut self, enabled: bool) -> Self {
265 self.state.fetch_from_fips_peers = enabled;
266 self
267 }
268
269 pub fn with_fips_endpoint(
270 mut self,
271 endpoint: Arc<hashtree_fips_transport::FipsEndpoint>,
272 ) -> Self {
273 self.state.fips_endpoint = Some(endpoint);
274 self
275 }
276
277 pub fn with_fips_blob_resolver(
278 mut self,
279 resolver: Arc<crate::fips_transport::DaemonBlobResolver>,
280 ) -> Self {
281 self.state.fips_blob_resolver = Some(resolver);
282 self
283 }
284
285 pub fn with_webrtc_peers(mut self, webrtc_state: Arc<WebRTCState>) -> Self {
287 self.state.webrtc_peers = Some(webrtc_state);
288 self
289 }
290
291 pub fn with_auth(mut self, username: String, password: String) -> Self {
292 self.state.auth = Some(AuthCredentials { username, password });
293 self
294 }
295
296 pub fn with_allowed_pubkeys(mut self, pubkeys: HashSet<String>) -> Self {
298 self.state.allowed_pubkeys = pubkeys;
299 self
300 }
301
302 pub fn with_upstream_blossom(mut self, servers: Vec<String>) -> Self {
304 self.state.upstream_blossom = servers;
305 self
306 }
307
308 pub fn with_blossom_upload_replicas(
310 mut self,
311 servers: Vec<String>,
312 queue_bytes: usize,
313 keys: nostr::Keys,
314 ) -> Self {
315 let queue_bytes = queue_bytes.clamp(1, tokio::sync::Semaphore::MAX_PERMITS);
316 let mut servers: Vec<String> = servers
317 .into_iter()
318 .map(|server| server.trim().trim_end_matches('/').to_string())
319 .filter(|server| !server.is_empty())
320 .collect();
321 servers.sort();
322 servers.dedup();
323 let replica_keys = (!servers.is_empty()).then(|| Arc::new(keys));
324 self.state.blossom_upload_replicas = servers;
325 self.state.blossom_upload_replica_queue_bytes = queue_bytes;
326 self.state.blossom_upload_replica_queue =
327 Arc::new(tokio::sync::Semaphore::new(queue_bytes));
328 self.state.blossom_upload_replica_keys = replica_keys;
329 self
330 }
331
332 pub fn with_social_graph(mut self, sg: Arc<socialgraph::SocialGraphAccessControl>) -> Self {
334 self.state.social_graph = Some(sg);
335 self
336 }
337
338 pub fn with_socialgraph_snapshot(
340 mut self,
341 store: Arc<dyn socialgraph::SocialGraphBackend>,
342 root: [u8; 32],
343 public: bool,
344 ) -> Self {
345 self.state.social_graph_store = Some(store);
346 self.state.social_graph_root = Some(root);
347 self.state.socialgraph_snapshot_public = public;
348 self
349 }
350
351 pub fn with_nostr_relay(mut self, relay: Arc<NostrRelay>) -> Self {
353 self.state.nostr_relay = Some(relay);
354 self
355 }
356
357 pub fn with_nostr_provider(mut self, provider: Arc<dyn nostr_pubsub::PubsubProvider>) -> Self {
358 self.state.nostr_provider = Some(provider);
359 self
360 }
361
362 pub fn with_nostr_relay_urls(mut self, relays: Vec<String>) -> Self {
364 self.state.nostr_relay_urls = relays;
365 self
366 }
367
368 pub fn with_cached_tree_roots(self, roots: Vec<(String, Cid)>) -> Self {
370 if let Ok(mut cache) = self.state.tree_root_cache.lock() {
371 let now = Instant::now();
372 for (key, cid) in roots {
373 cache.insert(
374 key,
375 CachedTreeRootEntry {
376 cid,
377 source: "embedded-bootstrap",
378 root_event: None,
379 event: None,
380 cached_at: now,
381 },
382 );
383 }
384 }
385 self
386 }
387
388 pub fn with_extra_routes(mut self, routes: Router<AppState>) -> Self {
390 self.extra_routes = Some(routes);
391 self
392 }
393
394 pub fn with_cors(mut self, cors: CorsLayer) -> Self {
396 self.cors = Some(cors);
397 self
398 }
399
400 pub async fn run(self) -> Result<()> {
401 let listener = tokio::net::TcpListener::bind(&self.addr).await?;
402 let _ = self.run_with_listener(listener).await?;
403 Ok(())
404 }
405
406 pub async fn run_with_listener(self, listener: tokio::net::TcpListener) -> Result<u16> {
407 self.run_with_listener_until(listener, future::pending::<()>())
408 .await
409 }
410
411 pub async fn run_with_listener_until<F>(
412 self,
413 listener: tokio::net::TcpListener,
414 shutdown: F,
415 ) -> Result<u16>
416 where
417 F: std::future::Future<Output = ()> + Send + 'static,
418 {
419 let local_addr = listener.local_addr()?;
420
421 let state = self.state.clone();
425 let public_routes = Router::new()
426 .route("/", get(handlers::serve_root_or_virtual_host))
427 .route("/ws", get(ws_relay::ws_data))
428 .route("/ws/", get(ws_relay::ws_data))
429 .route(
430 "/__iris/store/:hash",
431 get(handlers::iris_store_get).head(handlers::iris_store_head),
432 )
433 .route(
434 "/htree/test",
435 get(handlers::htree_test).head(handlers::htree_test),
436 )
437 .route("/htree/nhash1:nhash", get(handlers::htree_nhash))
439 .route("/htree/nhash1:nhash/", get(handlers::htree_nhash))
440 .route("/htree/nhash1:nhash/*path", get(handlers::htree_nhash_path))
441 .route("/htree/npub1:npub/:treename", get(handlers::htree_npub))
443 .route("/htree/npub1:npub/:treename/", get(handlers::htree_npub))
444 .route(
445 "/htree/npub1:npub/:treename/*path",
446 get(handlers::htree_npub_path),
447 )
448 .route("/n/:pubkey/:treename", get(handlers::resolve_and_serve))
450 .route("/npub1:rest", get(handlers::serve_npub))
452 .route("/npub1:rest/*path", get(handlers::serve_npub))
453 .route(
455 "/:id",
456 get(handlers::serve_content_or_blob)
457 .head(blossom::head_blob)
458 .delete(blossom::delete_blob)
459 .options(blossom::cors_preflight),
460 )
461 .route(
462 "/upload",
463 put(blossom::upload_blob)
464 .layer::<_, std::convert::Infallible>(middleware::from_fn(
465 blossom::require_upload_auth_middleware,
466 ))
467 .layer(DefaultBodyLimit::max(blossom::MAX_SINGLE_UPLOAD_BODY_BYTES))
468 .head(blossom::head_upload)
469 .options(blossom::cors_preflight),
470 )
471 .route(
472 "/upload/batch",
473 post(blossom::upload_blob_batch)
474 .layer::<_, std::convert::Infallible>(middleware::from_fn(
475 blossom::require_upload_auth_middleware,
476 ))
477 .options(blossom::cors_preflight)
478 .layer(DefaultBodyLimit::max(
479 blossom::MAX_BATCH_UPLOAD_JSON_BODY_BYTES,
480 )),
481 )
482 .route(
483 "/upload/batch-binary",
484 post(blossom::upload_blob_batch_binary)
485 .layer::<_, std::convert::Infallible>(middleware::from_fn(
486 blossom::require_upload_auth_middleware,
487 ))
488 .options(blossom::cors_preflight)
489 .layer(DefaultBodyLimit::max(
490 blossom::MAX_BATCH_UPLOAD_BINARY_BODY_BYTES,
491 )),
492 )
493 .route(
494 "/upload/check",
495 post(blossom::upload_check).options(blossom::cors_preflight),
496 )
497 .route(
498 "/blob/batch",
499 post(handlers::download_blob_batch).options(blossom::cors_preflight),
500 )
501 .route(
502 "/list/:pubkey",
503 get(blossom::list_blobs).options(blossom::cors_preflight),
504 )
505 .route("/health", get(handlers::health_check))
507 .route("/api/pins", get(handlers::list_pins))
508 .route("/api/stats", get(handlers::storage_stats))
509 .route("/api/peers", get(handlers::webrtc_peers))
510 .route("/api/status", get(handlers::daemon_status))
511 .route("/api/p2p/signal", post(handlers::p2p_signal))
512 .route("/api/socialgraph", get(handlers::socialgraph_stats))
513 .route(
514 "/api/socialgraph/snapshot",
515 get(handlers::socialgraph_snapshot),
516 )
517 .route(
518 "/api/socialgraph/distance/:pubkey",
519 get(handlers::follow_distance),
520 )
521 .route(
523 "/api/resolve/:pubkey/:treename",
524 get(handlers::resolve_to_hash),
525 )
526 .route(
527 "/api/nostr/resolve/:pubkey/:treename",
528 get(handlers::resolve_to_hash),
529 )
530 .route("/api/nostr/profile/:pubkey", get(handlers::nostr_profile))
531 .route(
532 "/api/nostr/events",
533 post(handlers::publish_nostr_event)
534 .layer(DefaultBodyLimit::max(INTERNAL_JSON_BODY_LIMIT_BYTES)),
535 )
536 .route("/api/trees/:pubkey", get(handlers::list_trees))
537 .fallback(get(handlers::serve_virtual_host_fallback))
538 .with_state(state.clone());
539
540 let protected_routes = Router::new()
542 .route("/upload", post(handlers::upload_file))
543 .route("/api/pin/:cid", post(handlers::pin_cid))
544 .route("/api/unpin/:cid", post(handlers::unpin_cid))
545 .route("/api/gc", post(handlers::garbage_collect))
546 .layer(middleware::from_fn_with_state(
547 state.clone(),
548 auth::auth_middleware,
549 ))
550 .with_state(state.clone());
551
552 let internal_routes = Router::new()
555 .route(
556 "/__iris/store/:hash",
557 put(handlers::iris_store_put)
558 .delete(handlers::iris_store_delete)
559 .layer(DefaultBodyLimit::max(blossom::MAX_BATCH_UPLOAD_BYTES)),
560 )
561 .route(
562 "/api/cache-tree-root",
563 post(handlers::cache_tree_root)
564 .layer(DefaultBodyLimit::max(INTERNAL_JSON_BODY_LIMIT_BYTES)),
565 )
566 .route(
567 "/api/clear-tree-root-cache",
568 post(handlers::clear_tree_root_cache)
569 .layer(DefaultBodyLimit::max(INTERNAL_JSON_BODY_LIMIT_BYTES)),
570 )
571 .layer(middleware::from_fn_with_state(
572 state.clone(),
573 auth::require_auth_middleware,
574 ))
575 .with_state(state.clone());
576
577 let mut app = public_routes
578 .merge(protected_routes)
579 .merge(internal_routes)
580 .layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)) .layer(middleware::from_fn(status_metrics::record_http_status));
582
583 if let Some(extra) = self.extra_routes {
584 app = app.merge(extra.with_state(state));
585 }
586
587 if let Some(cors) = self.cors {
588 app = app.layer(cors);
589 }
590
591 let make_service = app.into_make_service_with_connect_info::<std::net::SocketAddr>();
592 serve_with_connection_limits(listener, make_service, shutdown).await?;
593
594 Ok(local_addr.port())
595 }
596
597 pub fn addr(&self) -> &str {
598 &self.addr
599 }
600}
601
602async fn serve_with_connection_limits<M, S, F>(
603 listener: tokio::net::TcpListener,
604 mut make_service: M,
605 shutdown: F,
606) -> io::Result<()>
607where
608 M: Service<SocketAddr, Error = Infallible, Response = S> + Send + 'static,
609 M::Future: Send,
610 S: Service<Request<Body>, Response = Response, Error = Infallible> + Clone + Send + 'static,
611 S::Future: Send,
612 F: std::future::Future<Output = ()> + Send + 'static,
613{
614 let (signal_tx, signal_rx) = watch::channel(());
615 let signal_tx = Arc::new(signal_tx);
616 tokio::spawn(async move {
617 shutdown.await;
618 trace!("received graceful shutdown signal; stopping daemon listener");
619 drop(signal_rx);
620 });
621
622 let (close_tx, close_rx) = watch::channel(());
623
624 loop {
625 let (tcp_stream, remote_addr) = tokio::select! {
626 accepted = accept_tcp(&listener) => {
627 match accepted? {
628 Some(connection) => connection,
629 None => continue,
630 }
631 }
632 _ = signal_tx.closed() => {
633 trace!("shutdown signal received; no longer accepting daemon connections");
634 break;
635 }
636 };
637
638 configure_tcp_stream(&tcp_stream);
639 let tcp_stream = TokioIo::new(tcp_stream);
640
641 poll_fn(|cx| make_service.poll_ready(cx))
642 .await
643 .unwrap_or_else(|err| match err {});
644
645 let tower_service = make_service
646 .call(remote_addr)
647 .await
648 .unwrap_or_else(|err| match err {})
649 .map_request(|req: Request<Incoming>| req.map(Body::new));
650 let hyper_service = TowerToHyperService::new(tower_service);
651
652 let signal_tx = Arc::clone(&signal_tx);
653 let close_rx = close_rx.clone();
654
655 tokio::spawn(async move {
656 let mut builder = HyperBuilder::new(TokioExecutor::new());
657 builder
658 .http1()
659 .timer(TokioTimer::new())
660 .header_read_timeout(HTTP1_HEADER_READ_TIMEOUT);
661 builder
662 .http2()
663 .timer(TokioTimer::new())
664 .keep_alive_interval(Some(HTTP2_KEEPALIVE_INTERVAL))
665 .keep_alive_timeout(HTTP2_KEEPALIVE_TIMEOUT);
666
667 let conn = builder.serve_connection_with_upgrades(tcp_stream, hyper_service);
668 pin_mut!(conn);
669
670 let signal_closed = signal_tx.closed().fuse();
671 pin_mut!(signal_closed);
672
673 loop {
674 tokio::select! {
675 result = conn.as_mut() => {
676 if let Err(err) = result {
677 trace!("daemon connection closed with error: {err:#}");
678 }
679 break;
680 }
681 _ = &mut signal_closed => {
682 trace!("shutdown signal received by connection task");
683 conn.as_mut().graceful_shutdown();
684 }
685 }
686 }
687
688 drop(close_rx);
689 });
690 }
691
692 drop(close_rx);
693 drop(listener);
694 close_tx.closed().await;
695
696 Ok(())
697}
698
699fn configure_tcp_stream(tcp_stream: &tokio::net::TcpStream) {
700 if let Err(err) = tcp_stream.set_nodelay(true) {
701 debug!("failed to set TCP_NODELAY on daemon connection: {err:#}");
702 }
703
704 let socket = SockRef::from(tcp_stream);
705 if let Err(err) = socket.set_tcp_keepalive(
706 &TcpKeepalive::new()
707 .with_time(TCP_KEEPALIVE_TIME)
708 .with_interval(TCP_KEEPALIVE_INTERVAL),
709 ) {
710 debug!("failed to set TCP keepalive on daemon connection: {err:#}");
711 }
712}
713
714async fn accept_tcp(
715 listener: &tokio::net::TcpListener,
716) -> io::Result<Option<(tokio::net::TcpStream, SocketAddr)>> {
717 match listener.accept().await {
718 Ok(connection) => Ok(Some(connection)),
719 Err(err) => {
720 if is_connection_error(&err) {
721 return Ok(None);
722 }
723 if is_resource_exhaustion_error(&err) {
724 error!(
725 "daemon accept failed due to file descriptor exhaustion; exiting for supervisor restart: {err}"
726 );
727 return Err(err);
728 }
729 error!("daemon accept error: {err}");
730 tokio::time::sleep(Duration::from_secs(1)).await;
731 Ok(None)
732 }
733 }
734}
735
736fn is_resource_exhaustion_error(err: &io::Error) -> bool {
737 matches!(
738 err.raw_os_error(),
739 Some(code) if code == libc::EMFILE || code == libc::ENFILE
740 )
741}
742
743fn is_connection_error(err: &io::Error) -> bool {
744 matches!(
745 err.kind(),
746 io::ErrorKind::ConnectionRefused
747 | io::ErrorKind::ConnectionAborted
748 | io::ErrorKind::ConnectionReset
749 )
750}
751
752fn current_unix_secs() -> u64 {
753 std::time::SystemTime::now()
754 .duration_since(std::time::UNIX_EPOCH)
755 .unwrap_or(std::time::Duration::ZERO)
756 .as_secs()
757}
758
759#[cfg(test)]
760mod tests {
761 use super::*;
762 use crate::nostr_relay::{NostrRelay, NostrRelayConfig};
763 use crate::storage::HashtreeStore;
764 use async_trait::async_trait;
765 use hashtree_core::{
766 from_hex, nhash_encode, sha256, to_hex, DirEntry, HashTree, HashTreeConfig, LinkType,
767 };
768 use nostr::{nips::nip19::ToBech32, EventBuilder, Keys, Kind, Timestamp};
769 use nostr_pubsub::{
770 EventBus, EventSource, PublishReport, PubsubProvider, PubsubProviderMode, QueryEvent,
771 QueryOptions, QueryReport, VerifiedEvent,
772 };
773 use serde_json::json;
774 use std::sync::atomic::{AtomicUsize, Ordering};
775 use tempfile::TempDir;
776 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
777
778 struct StaticProvider {
779 event: Option<VerifiedEvent>,
780 queries: AtomicUsize,
781 publishes: AtomicUsize,
782 mode: PubsubProviderMode,
783 }
784
785 #[async_trait]
786 impl EventBus for StaticProvider {
787 async fn publish(
788 &self,
789 _event: VerifiedEvent,
790 _source: EventSource,
791 ) -> nostr_pubsub::Result<PublishReport> {
792 self.publishes.fetch_add(1, Ordering::Relaxed);
793 Ok(PublishReport {
794 accepted: true,
795 priority: 0,
796 reason: None,
797 })
798 }
799
800 async fn query(
801 &self,
802 _filters: Vec<nostr_pubsub::Filter>,
803 _options: QueryOptions,
804 ) -> nostr_pubsub::Result<QueryReport> {
805 self.queries.fetch_add(1, Ordering::Relaxed);
806 Ok(QueryReport {
807 events: self
808 .event
809 .clone()
810 .map(|event| QueryEvent {
811 event,
812 source: EventSource::fips_endpoint("browser-router"),
813 priority: 0,
814 })
815 .into_iter()
816 .collect(),
817 })
818 }
819 }
820
821 impl PubsubProvider for StaticProvider {
822 fn mode(&self) -> PubsubProviderMode {
823 self.mode
824 }
825 }
826
827 #[test]
828 fn resource_exhaustion_errors_are_fatal_accept_errors() {
829 assert!(is_resource_exhaustion_error(&io::Error::from_raw_os_error(
830 libc::EMFILE
831 )));
832 assert!(is_resource_exhaustion_error(&io::Error::from_raw_os_error(
833 libc::ENFILE
834 )));
835 assert!(!is_resource_exhaustion_error(
836 &io::Error::from_raw_os_error(libc::ECONNRESET)
837 ));
838 }
839
840 #[test]
841 fn upload_queue_semaphores_fit_all_tokio_targets() -> Result<()> {
842 let temp_dir = TempDir::new()?;
843 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
844 let server = HashtreeServer::new(store, "127.0.0.1:0".to_string());
845
846 assert_eq!(
847 server.state.optimistic_upload_queue.available_permits(),
848 256 * 1024 * 1024
849 );
850 assert_eq!(
851 server
852 .state
853 .blossom_upload_replica_queue
854 .available_permits(),
855 256 * 1024 * 1024
856 );
857 assert_eq!(
858 bounded_upload_queue_bytes(u64::MAX),
859 tokio::sync::Semaphore::MAX_PERMITS
860 );
861 Ok(())
862 }
863
864 #[test]
865 fn server_builder_seeds_initial_tree_roots() -> Result<()> {
866 let temp_dir = TempDir::new()?;
867 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
868 let hash = from_hex("1111111111111111111111111111111111111111111111111111111111111111")?;
869 let key = from_hex("2222222222222222222222222222222222222222222222222222222222222222")?;
870 let cid = Cid {
871 hash,
872 key: Some(key),
873 };
874
875 let server = HashtreeServer::new(store, "127.0.0.1:0".to_string())
876 .with_cached_tree_roots(vec![("npub1example/sites".to_string(), cid.clone())]);
877 let cached = server
878 .state
879 .tree_root_cache
880 .lock()
881 .unwrap()
882 .get("npub1example/sites")
883 .cloned()
884 .expect("seeded root");
885
886 assert_eq!(cached.cid, cid);
887 assert_eq!(cached.source, "embedded-bootstrap");
888 assert!(cached.root_event.is_none());
889 Ok(())
890 }
891
892 #[test]
893 fn iris_localhost_hosts_map_to_existing_tree_routes() {
894 assert_eq!(
895 resolve_virtual_tree_host("NHASH1EXAMPLE.iris.localhost:8080"),
896 Some("/htree/nhash1example".to_string())
897 );
898 assert_eq!(
899 resolve_virtual_tree_host("audio.NPUB1EXAMPLE.iris.localhost:8080"),
900 Some("/htree/npub1example/audio".to_string())
901 );
902 assert_eq!(
903 resolve_virtual_tree_host("audio.extra.npub1example.iris.localhost"),
904 None
905 );
906 assert_eq!(
907 resolve_virtual_tree_host("nhash1example.htree.localhost"),
908 None
909 );
910 }
911
912 #[tokio::test]
913 async fn test_server_serve_file() -> Result<()> {
914 let temp_dir = TempDir::new()?;
915 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
916
917 let test_file = temp_dir.path().join("test.txt");
919 std::fs::write(&test_file, b"Hello, Hashtree!")?;
920
921 let cid = store.upload_file(&test_file)?;
922 let hash = from_hex(&cid)?;
923
924 let content = store.get_file(&hash)?;
926 assert!(content.is_some());
927 assert_eq!(content.unwrap(), b"Hello, Hashtree!");
928
929 Ok(())
930 }
931
932 #[tokio::test]
933 async fn test_server_list_pins() -> Result<()> {
934 let temp_dir = TempDir::new()?;
935 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
936
937 let test_file = temp_dir.path().join("test.txt");
938 std::fs::write(&test_file, b"Test")?;
939
940 let cid = store.upload_file(&test_file)?;
941 let hash = from_hex(&cid)?;
942
943 let pins = store.list_pins_raw()?;
944 assert_eq!(pins.len(), 1);
945 assert_eq!(pins[0], hash);
946
947 Ok(())
948 }
949
950 async fn spawn_test_server(
951 store: Arc<HashtreeStore>,
952 ) -> Result<(u16, tokio::task::JoinHandle<Result<()>>)> {
953 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
954 let port = listener.local_addr()?.port();
955 let server = HashtreeServer::new(store, "127.0.0.1:0".to_string());
956 let handle =
957 tokio::spawn(async move { server.run_with_listener(listener).await.map(|_| ()) });
958 Ok((port, handle))
959 }
960
961 async fn spawn_test_server_with_nostr_relay(
962 store: Arc<HashtreeStore>,
963 relay: Arc<NostrRelay>,
964 ) -> Result<(u16, tokio::task::JoinHandle<Result<()>>)> {
965 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
966 let port = listener.local_addr()?.port();
967 let server = HashtreeServer::new(store, "127.0.0.1:0".to_string()).with_nostr_relay(relay);
968 let handle =
969 tokio::spawn(async move { server.run_with_listener(listener).await.map(|_| ()) });
970 Ok((port, handle))
971 }
972
973 #[tokio::test]
974 async fn unauthenticated_native_store_mutation_is_rejected() -> Result<()> {
975 let temp_dir = TempDir::new()?;
976 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
977 let body = b"unauthorized native store write";
978 let hash = sha256(body);
979 let hash_hex = to_hex(&hash);
980 let (port, handle) = spawn_test_server(Arc::clone(&store)).await?;
981
982 let response = reqwest::Client::new()
983 .put(format!("http://127.0.0.1:{port}/__iris/store/{hash_hex}"))
984 .body(body.to_vec())
985 .send()
986 .await?;
987
988 assert_eq!(response.status(), reqwest::StatusCode::FORBIDDEN);
989 assert!(store.get_blob(&hash)?.is_none());
990 handle.abort();
991 Ok(())
992 }
993
994 #[tokio::test]
995 async fn unauthenticated_cache_tree_root_is_rejected() -> Result<()> {
996 let temp_dir = TempDir::new()?;
997 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
998 let (port, handle) = spawn_test_server(store).await?;
999
1000 let response = reqwest::Client::new()
1001 .post(format!("http://127.0.0.1:{port}/api/cache-tree-root"))
1002 .json(&json!({
1003 "npub": "npub1example",
1004 "treeName": "video",
1005 "hash": "988db3f24dc222715f1c1e1fa5876690d3147122243d72d85fd44283867cd61a",
1006 "visibility": "public"
1007 }))
1008 .send()
1009 .await?;
1010
1011 assert_eq!(response.status(), reqwest::StatusCode::FORBIDDEN);
1012 handle.abort();
1013 Ok(())
1014 }
1015
1016 #[tokio::test]
1017 async fn unauthenticated_upload_batch_is_rejected_before_json_extraction() -> Result<()> {
1018 let temp_dir = TempDir::new()?;
1019 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
1020 let (port, handle) = spawn_test_server(store).await?;
1021
1022 let response = reqwest::Client::new()
1023 .post(format!("http://127.0.0.1:{port}/upload/batch"))
1024 .header(reqwest::header::CONTENT_TYPE, "application/json")
1025 .body("not valid json")
1026 .send()
1027 .await?;
1028
1029 assert_eq!(response.status(), reqwest::StatusCode::UNAUTHORIZED);
1030 handle.abort();
1031 Ok(())
1032 }
1033
1034 #[tokio::test]
1035 async fn unauthenticated_single_upload_is_rejected_before_body_limit() -> Result<()> {
1036 let temp_dir = TempDir::new()?;
1037 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
1038 let (port, handle) = spawn_test_server(store).await?;
1039 let declared_bytes = blossom::MAX_SINGLE_UPLOAD_BODY_BYTES + 1;
1040 let mut stream = tokio::net::TcpStream::connect(("127.0.0.1", port)).await?;
1041 stream
1042 .write_all(
1043 format!(
1044 "PUT /upload HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nContent-Length: {declared_bytes}\r\nConnection: close\r\n\r\n"
1045 )
1046 .as_bytes(),
1047 )
1048 .await?;
1049
1050 let mut status_line = String::new();
1053 BufReader::new(stream).read_line(&mut status_line).await?;
1054 assert_eq!(status_line.trim_end(), "HTTP/1.1 401 Unauthorized");
1055 handle.abort();
1056 Ok(())
1057 }
1058
1059 #[tokio::test]
1060 async fn upload_options_preflight_does_not_require_auth() -> Result<()> {
1061 let temp_dir = TempDir::new()?;
1062 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
1063 let (port, handle) = spawn_test_server(store).await?;
1064
1065 let response = reqwest::Client::new()
1066 .request(
1067 reqwest::Method::OPTIONS,
1068 format!("http://127.0.0.1:{port}/upload"),
1069 )
1070 .send()
1071 .await?;
1072
1073 assert_eq!(response.status(), reqwest::StatusCode::NO_CONTENT);
1074 handle.abort();
1075 Ok(())
1076 }
1077
1078 #[tokio::test]
1079 async fn virtual_tree_hosts_serve_root_assets_and_spa_fallbacks() -> Result<()> {
1080 clear_virtual_tree_hosts_for_test();
1081
1082 let temp_dir = TempDir::new()?;
1083 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
1084 let tree = HashTree::new(HashTreeConfig::new(store.store_arc()).public());
1085
1086 let (index_cid, _) = tree
1087 .put(b"<!doctype html><title>Virtual host ok</title>")
1088 .await?;
1089 let (favicon_cid, _) = tree.put(b"ico").await?;
1090 let (main_js_cid, _) = tree.put(b"console.log('ok');").await?;
1091 let assets_dir = tree
1092 .put_directory(vec![
1093 DirEntry::from_cid("main.js", &main_js_cid).with_link_type(LinkType::File)
1094 ])
1095 .await?;
1096 let root_cid = tree
1097 .put_directory(vec![
1098 DirEntry::from_cid("index.html", &index_cid).with_link_type(LinkType::File),
1099 DirEntry::from_cid("favicon.ico", &favicon_cid).with_link_type(LinkType::File),
1100 DirEntry::from_cid("assets", &assets_dir).with_link_type(LinkType::Dir),
1101 ])
1102 .await?;
1103 let nhash = nhash_encode(&root_cid.hash)?;
1104 let host = "tree-test.htree.localhost";
1105 register_virtual_tree_host(host, &format!("/htree/{nhash}"));
1106
1107 let (port, handle) = spawn_test_server(store).await?;
1108 let base_url = format!("http://127.0.0.1:{port}");
1109 let host_header = format!("{host}:{port}");
1110 let client = reqwest::Client::new();
1111
1112 let root_response = client
1113 .get(format!("{base_url}/"))
1114 .header("Host", &host_header)
1115 .header("Accept", "text/html")
1116 .send()
1117 .await?;
1118 assert_eq!(root_response.status(), reqwest::StatusCode::OK);
1119 assert_eq!(
1120 root_response.bytes().await?.as_ref(),
1121 b"<!doctype html><title>Virtual host ok</title>"
1122 );
1123
1124 let favicon_response = client
1125 .get(format!("{base_url}/favicon.ico"))
1126 .header("Host", &host_header)
1127 .send()
1128 .await?;
1129 assert_eq!(favicon_response.status(), reqwest::StatusCode::OK);
1130 assert_eq!(favicon_response.bytes().await?.as_ref(), b"ico");
1131
1132 let js_response = client
1133 .get(format!("{base_url}/assets/main.js"))
1134 .header("Host", &host_header)
1135 .send()
1136 .await?;
1137 assert_eq!(js_response.status(), reqwest::StatusCode::OK);
1138 assert_eq!(js_response.bytes().await?.as_ref(), b"console.log('ok');");
1139
1140 let profile_response = client
1141 .get(format!("{base_url}/users/npub1example"))
1142 .header("Host", &host_header)
1143 .header("Accept", "text/html")
1144 .send()
1145 .await?;
1146 assert_eq!(profile_response.status(), reqwest::StatusCode::OK);
1147 assert_eq!(
1148 profile_response.bytes().await?.as_ref(),
1149 b"<!doctype html><title>Virtual host ok</title>"
1150 );
1151
1152 handle.abort();
1153 clear_virtual_tree_hosts_for_test();
1154
1155 Ok(())
1156 }
1157
1158 #[tokio::test]
1159 async fn iris_localhost_hosts_serve_immutable_and_named_sites() -> Result<()> {
1160 let temp_dir = TempDir::new()?;
1161 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
1162 let tree = HashTree::new(HashTreeConfig::new(store.store_arc()).public());
1163
1164 let (index_cid, _) = tree
1165 .put(b"<!doctype html><title>Iris localhost ok</title>")
1166 .await?;
1167 let (main_js_cid, _) = tree.put(b"console.log('iris localhost');").await?;
1168 let assets_dir = tree
1169 .put_directory(vec![
1170 DirEntry::from_cid("main.js", &main_js_cid).with_link_type(LinkType::File)
1171 ])
1172 .await?;
1173 let root_cid = tree
1174 .put_directory(vec![
1175 DirEntry::from_cid("index.html", &index_cid).with_link_type(LinkType::File),
1176 DirEntry::from_cid("assets", &assets_dir).with_link_type(LinkType::Dir),
1177 ])
1178 .await?;
1179
1180 let nhash = nhash_encode(&root_cid.hash)?;
1181 let npub = Keys::generate().public_key().to_bech32()?;
1182 let site = "audio";
1183 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
1184 let port = listener.local_addr()?.port();
1185 let server = HashtreeServer::new(Arc::clone(&store), "127.0.0.1:0".to_string())
1186 .with_public_plaintext_reads(true)
1187 .with_cached_tree_roots(vec![(format!("{npub}/{site}"), root_cid)]);
1188 let handle =
1189 tokio::spawn(async move { server.run_with_listener(listener).await.map(|_| ()) });
1190 let client = reqwest::Client::new();
1191 let base_url = format!("http://127.0.0.1:{port}");
1192
1193 for host in [
1194 format!("{nhash}.iris.localhost:{port}"),
1195 format!("{site}.{npub}.iris.localhost:{port}"),
1196 ] {
1197 let root_response = client
1198 .get(format!("{base_url}/"))
1199 .header("Host", &host)
1200 .header("Accept", "text/html")
1201 .send()
1202 .await?;
1203 assert_eq!(root_response.status(), reqwest::StatusCode::OK, "{host}");
1204 assert_eq!(
1205 root_response.bytes().await?.as_ref(),
1206 b"<!doctype html><title>Iris localhost ok</title>"
1207 );
1208
1209 let asset_response = client
1210 .get(format!("{base_url}/assets/main.js"))
1211 .header("Host", &host)
1212 .send()
1213 .await?;
1214 assert_eq!(asset_response.status(), reqwest::StatusCode::OK, "{host}");
1215 assert_eq!(
1216 asset_response.bytes().await?.as_ref(),
1217 b"console.log('iris localhost');"
1218 );
1219 }
1220
1221 handle.abort();
1222 Ok(())
1223 }
1224
1225 #[tokio::test]
1226 async fn named_iris_site_resolves_from_fips_event_provider_without_relays() -> Result<()> {
1227 let temp_dir = TempDir::new()?;
1228 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
1229 let tree = HashTree::new(HashTreeConfig::new(store.store_arc()).public());
1230 let (index_cid, _) = tree.put(b"fips event provider site").await?;
1231 let root_cid = tree
1232 .put_directory(vec![
1233 DirEntry::from_cid("index.html", &index_cid).with_link_type(LinkType::File)
1234 ])
1235 .await?;
1236 let owner = Keys::generate();
1237 let site = "radio";
1238 let root_event = EventBuilder::new(Kind::Custom(30064), "")
1239 .tags(vec![
1240 nostr::Tag::identifier(site),
1241 nostr::Tag::custom(nostr::TagKind::custom("l"), vec!["hashtree".to_string()]),
1242 nostr::Tag::custom(nostr::TagKind::custom("hash"), vec![to_hex(&root_cid.hash)]),
1243 ])
1244 .sign_with_keys(&owner)?;
1245 let provider = Arc::new(StaticProvider {
1246 event: Some(VerifiedEvent::try_from(root_event)?),
1247 queries: AtomicUsize::new(0),
1248 publishes: AtomicUsize::new(0),
1249 mode: PubsubProviderMode::LocalOnly,
1250 });
1251
1252 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
1253 let port = listener.local_addr()?.port();
1254 let server = HashtreeServer::new(Arc::clone(&store), "127.0.0.1:0".to_string())
1255 .with_public_plaintext_reads(true)
1256 .with_nostr_provider(provider.clone());
1257 assert!(server.state.nostr_relay_urls.is_empty());
1258 assert_eq!(
1259 server.state.nostr_provider.as_ref().unwrap().mode(),
1260 PubsubProviderMode::LocalOnly
1261 );
1262 let handle =
1263 tokio::spawn(async move { server.run_with_listener(listener).await.map(|_| ()) });
1264
1265 let npub = owner.public_key().to_bech32()?;
1266 let response = reqwest::Client::new()
1267 .get(format!("http://127.0.0.1:{port}/"))
1268 .header("Host", format!("{site}.{npub}.iris.localhost"))
1269 .header("Accept", "text/html")
1270 .send()
1271 .await?;
1272
1273 assert_eq!(response.status(), reqwest::StatusCode::OK);
1274 assert_eq!(
1275 response.bytes().await?.as_ref(),
1276 b"fips event provider site"
1277 );
1278 assert_eq!(provider.queries.load(Ordering::Relaxed), 1);
1279 handle.abort();
1280 Ok(())
1281 }
1282
1283 #[tokio::test]
1284 async fn loopback_event_publication_uses_configured_provider() -> Result<()> {
1285 let temp_dir = TempDir::new()?;
1286 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
1287 let keys = Keys::generate();
1288 let event = EventBuilder::new(Kind::TextNote, "provider test").sign_with_keys(&keys)?;
1289 let provider = Arc::new(StaticProvider {
1290 event: Some(VerifiedEvent::try_from(event.clone())?),
1291 queries: AtomicUsize::new(0),
1292 publishes: AtomicUsize::new(0),
1293 mode: PubsubProviderMode::LocalOnly,
1294 });
1295 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
1296 let port = listener.local_addr()?.port();
1297 let server = HashtreeServer::new(store, "127.0.0.1:0".to_string())
1298 .with_nostr_provider(provider.clone());
1299 assert!(server.state.nostr_relay_urls.is_empty());
1300 let handle =
1301 tokio::spawn(async move { server.run_with_listener(listener).await.map(|_| ()) });
1302
1303 let response = reqwest::Client::new()
1304 .post(format!("http://127.0.0.1:{port}/api/nostr/events"))
1305 .json(&event)
1306 .send()
1307 .await?;
1308
1309 assert_eq!(response.status(), reqwest::StatusCode::ACCEPTED);
1310 assert_eq!(provider.publishes.load(Ordering::Relaxed), 1);
1311 handle.abort();
1312 Ok(())
1313 }
1314
1315 #[tokio::test]
1316 async fn loopback_root_publication_is_immediately_resolvable_without_provider_replay(
1317 ) -> Result<()> {
1318 let temp_dir = TempDir::new()?;
1319 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
1320 let keys = Keys::generate();
1321 let tree_name = "webvm-e2e";
1322 let root_hash = "11".repeat(32);
1323 let encryption_key = "22".repeat(32);
1324 let event = EventBuilder::new(Kind::Custom(30064), &root_hash)
1325 .tags(vec![
1326 nostr::Tag::identifier(tree_name),
1327 nostr::Tag::custom(nostr::TagKind::custom("l"), vec!["hashtree".to_string()]),
1328 nostr::Tag::custom(nostr::TagKind::custom("l"), vec!["git".to_string()]),
1329 nostr::Tag::custom(nostr::TagKind::custom("hash"), vec![root_hash.clone()]),
1330 nostr::Tag::custom(nostr::TagKind::custom("key"), vec![encryption_key.clone()]),
1331 ])
1332 .sign_with_keys(&keys)?;
1333 let provider = Arc::new(StaticProvider {
1334 event: None,
1335 queries: AtomicUsize::new(0),
1336 publishes: AtomicUsize::new(0),
1337 mode: PubsubProviderMode::LocalOnly,
1338 });
1339 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
1340 let port = listener.local_addr()?.port();
1341 let server = HashtreeServer::new(store, "127.0.0.1:0".to_string())
1342 .with_nostr_provider(provider.clone());
1343 assert!(server.state.nostr_relay_urls.is_empty());
1344 let handle =
1345 tokio::spawn(async move { server.run_with_listener(listener).await.map(|_| ()) });
1346 let client = reqwest::Client::new();
1347
1348 let publish = client
1349 .post(format!("http://127.0.0.1:{port}/api/nostr/events"))
1350 .json(&event)
1351 .send()
1352 .await?;
1353 assert_eq!(publish.status(), reqwest::StatusCode::ACCEPTED);
1354
1355 let npub = keys.public_key().to_bech32()?;
1356 let resolved = client
1357 .get(format!(
1358 "http://127.0.0.1:{port}/api/nostr/resolve/{npub}/{tree_name}?refresh=1"
1359 ))
1360 .send()
1361 .await?
1362 .json::<serde_json::Value>()
1363 .await?;
1364
1365 assert_eq!(resolved["hash"], root_hash);
1366 assert_eq!(resolved["key_tag"], encryption_key);
1367 assert_eq!(resolved["source"], "local-relay");
1368 assert_eq!(provider.publishes.load(Ordering::Relaxed), 1);
1369 assert_eq!(provider.queries.load(Ordering::Relaxed), 1);
1370 handle.abort();
1371 Ok(())
1372 }
1373
1374 #[tokio::test]
1375 async fn nostr_profile_route_returns_latest_metadata_event() -> Result<()> {
1376 let temp_dir = TempDir::new()?;
1377 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
1378 let graph_store = {
1379 let _guard = crate::socialgraph::test_lock().await;
1380 crate::socialgraph::open_test_social_graph_store_with_mapsize(
1381 &temp_dir.path().join("relay-db"),
1382 Some(128 * 1024 * 1024),
1383 )?
1384 };
1385 let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph_store;
1386 let relay = Arc::new(NostrRelay::new(
1387 backend,
1388 temp_dir.path().to_path_buf(),
1389 HashSet::new(),
1390 None,
1391 NostrRelayConfig {
1392 spambox_db_max_bytes: 0,
1393 ..Default::default()
1394 },
1395 )?);
1396
1397 let author = Keys::generate();
1398 let older = EventBuilder::new(
1399 Kind::Metadata,
1400 json!({ "name": "older", "about": "before" }).to_string(),
1401 )
1402 .custom_created_at(Timestamp::from_secs(10))
1403 .sign_with_keys(&author)?;
1404 let newer = EventBuilder::new(
1405 Kind::Metadata,
1406 json!({ "name": "newer", "about": "after" }).to_string(),
1407 )
1408 .custom_created_at(Timestamp::from_secs(20))
1409 .sign_with_keys(&author)?;
1410
1411 relay.ingest_trusted_event(older).await?;
1412 relay.ingest_trusted_event(newer.clone()).await?;
1413
1414 let (port, handle) = spawn_test_server_with_nostr_relay(store, relay).await?;
1415 let response = reqwest::get(format!(
1416 "http://127.0.0.1:{port}/api/nostr/profile/{}",
1417 author.public_key().to_hex()
1418 ))
1419 .await?;
1420
1421 assert_eq!(response.status(), reqwest::StatusCode::OK);
1422 let payload: serde_json::Value = response.json().await?;
1423 assert_eq!(payload["profile"]["name"].as_str(), Some("newer"),);
1424 assert_eq!(payload["profile"]["about"].as_str(), Some("after"));
1425 assert_eq!(payload["created_at"].as_u64(), Some(20));
1426 let expected_event_id = newer.id.to_hex();
1427 assert_eq!(
1428 payload["event_id"].as_str(),
1429 Some(expected_event_id.as_str())
1430 );
1431
1432 handle.abort();
1433 Ok(())
1434 }
1435}