1use crate::blob_cache::BlobCache;
2use crate::fips_transport::DaemonBlobResolver;
3use crate::nostr_relay::NostrRelay;
4use crate::root_events::PeerRootEvent;
5use crate::socialgraph;
6use crate::storage::HashtreeStore;
7use axum::{
8 body::Body,
9 extract::ws::Message,
10 extract::State,
11 http::{header, HeaderMap, Request, Response, StatusCode},
12 middleware::Next,
13};
14use futures::future::{BoxFuture, Shared};
15use hashtree_core::{Cid, LinkType, TreeEntry};
16use lru::LruCache;
17use nostr::Keys;
18use std::collections::{HashMap, HashSet};
19use std::hash::Hash;
20use std::num::NonZeroUsize;
21use std::sync::{
22 atomic::{AtomicU64, Ordering},
23 Arc, Mutex as StdMutex,
24};
25use std::time::{Duration, Instant};
26use tokio::{
27 sync::{mpsc, watch, Mutex, Semaphore},
28 task::JoinHandle,
29};
30
31const LOOKUP_CACHE_CAPACITY: usize = 4096;
32const LOOKUP_CACHE_HIT_TTL: Duration = Duration::from_secs(300);
33const LOOKUP_CACHE_MISS_TTL: Duration = Duration::from_secs(1);
34
35#[derive(Debug, Clone)]
36pub enum LookupResult<T> {
37 Hit(T),
38 Miss,
39}
40
41impl<T> LookupResult<T> {
42 pub fn from_option(value: Option<T>) -> Self {
43 match value {
44 Some(value) => Self::Hit(value),
45 None => Self::Miss,
46 }
47 }
48
49 pub fn into_option(self) -> Option<T> {
50 match self {
51 Self::Hit(value) => Some(value),
52 Self::Miss => None,
53 }
54 }
55
56 pub fn ttl(&self) -> Duration {
57 match self {
58 Self::Hit(_) => LOOKUP_CACHE_HIT_TTL,
59 Self::Miss => LOOKUP_CACHE_MISS_TTL,
60 }
61 }
62}
63
64pub struct TimedLruCache<K, V> {
65 cache: LruCache<K, TimedValue<V>>,
66}
67
68#[derive(Clone)]
69struct TimedValue<V> {
70 value: V,
71 expires_at: Instant,
72}
73
74impl<K: Eq + Hash, V: Clone> TimedLruCache<K, V> {
75 pub fn new(capacity: usize) -> Self {
76 Self {
77 cache: LruCache::new(NonZeroUsize::new(capacity.max(1)).unwrap()),
78 }
79 }
80
81 pub fn get_cloned(&mut self, key: &K) -> Option<V> {
82 let now = Instant::now();
83 if let Some(entry) = self.cache.get(key) {
84 if entry.expires_at > now {
85 return Some(entry.value.clone());
86 }
87 }
88 self.cache.pop(key);
89 None
90 }
91
92 pub fn put(&mut self, key: K, value: V, ttl: Duration) {
93 self.cache.put(
94 key,
95 TimedValue {
96 value,
97 expires_at: Instant::now() + ttl,
98 },
99 );
100 }
101}
102
103pub fn new_lookup_cache<K: Eq + Hash, V: Clone>() -> TimedLruCache<K, V> {
104 TimedLruCache::new(LOOKUP_CACHE_CAPACITY)
105}
106
107#[derive(Debug, Clone)]
108pub struct CachedResolvedPathEntry {
109 pub cid: Cid,
110 pub link_type: LinkType,
111}
112
113#[derive(Debug, Clone)]
114pub struct CachedTreeRootEntry {
115 pub cid: Cid,
116 pub source: &'static str,
117 pub root_event: Option<PeerRootEvent>,
118 pub event: Option<nostr::Event>,
119 pub cached_at: Instant,
120}
121
122pub type SharedBlobFetch = Shared<BoxFuture<'static, Result<bool, String>>>;
123pub type SharedBlobRead = Shared<BoxFuture<'static, Result<Option<Vec<u8>>, String>>>;
124
125pub struct UpstreamNostrSubscription {
126 pub close_tx: watch::Sender<bool>,
127 pub tasks: Vec<JoinHandle<()>>,
128}
129
130#[derive(Debug, Clone, Default)]
131pub struct UpstreamBlossomFetchSnapshot {
132 pub lookup_attempts: u64,
133 pub hits: u64,
134 pub hit_bytes: u64,
135 pub explicit_misses: u64,
136 pub indeterminate_misses: u64,
137 pub miss_cache_hits: u64,
138 pub last_indeterminate_reason: Option<String>,
139}
140
141#[derive(Default)]
142pub struct UpstreamBlossomFetchMetrics {
143 lookup_attempts: AtomicU64,
144 hits: AtomicU64,
145 hit_bytes: AtomicU64,
146 explicit_misses: AtomicU64,
147 indeterminate_misses: AtomicU64,
148 miss_cache_hits: AtomicU64,
149 last_indeterminate_reason: StdMutex<Option<String>>,
150}
151
152impl UpstreamBlossomFetchMetrics {
153 pub fn note_lookup_attempt(&self) {
154 self.lookup_attempts.fetch_add(1, Ordering::Relaxed);
155 }
156
157 pub fn note_hit(&self, bytes: usize) {
158 self.hits.fetch_add(1, Ordering::Relaxed);
159 self.hit_bytes.fetch_add(bytes as u64, Ordering::Relaxed);
160 }
161
162 pub fn note_explicit_miss(&self) {
163 self.explicit_misses.fetch_add(1, Ordering::Relaxed);
164 }
165
166 pub fn note_indeterminate_miss(&self, reason: impl Into<String>) {
167 self.indeterminate_misses.fetch_add(1, Ordering::Relaxed);
168 let reason = reason.into();
169 if let Ok(mut last_reason) = self.last_indeterminate_reason.lock() {
170 *last_reason = Some(reason.chars().take(512).collect());
171 }
172 }
173
174 pub fn note_miss_cache_hit(&self) {
175 self.miss_cache_hits.fetch_add(1, Ordering::Relaxed);
176 }
177
178 pub fn snapshot(&self) -> UpstreamBlossomFetchSnapshot {
179 UpstreamBlossomFetchSnapshot {
180 lookup_attempts: self.lookup_attempts.load(Ordering::Relaxed),
181 hits: self.hits.load(Ordering::Relaxed),
182 hit_bytes: self.hit_bytes.load(Ordering::Relaxed),
183 explicit_misses: self.explicit_misses.load(Ordering::Relaxed),
184 indeterminate_misses: self.indeterminate_misses.load(Ordering::Relaxed),
185 miss_cache_hits: self.miss_cache_hits.load(Ordering::Relaxed),
186 last_indeterminate_reason: self
187 .last_indeterminate_reason
188 .lock()
189 .ok()
190 .and_then(|reason| reason.clone()),
191 }
192 }
193}
194
195pub struct WsRelayState {
196 pub clients: Mutex<HashMap<u64, mpsc::UnboundedSender<Message>>>,
197 pub upstream_nostr_subscriptions: Mutex<HashMap<(u64, String), UpstreamNostrSubscription>>,
198 pub upstream_seen_events: Mutex<HashMap<(u64, String), HashSet<String>>>,
199 pub upstream_pending_eose: Mutex<HashMap<(u64, String), usize>>,
200 pub next_client_id: AtomicU64,
201 pub upstream_relay_bytes_sent: AtomicU64,
202 pub upstream_relay_bytes_received: AtomicU64,
203}
204
205impl WsRelayState {
206 pub fn new() -> Self {
207 Self {
208 clients: Mutex::new(HashMap::new()),
209 upstream_nostr_subscriptions: Mutex::new(HashMap::new()),
210 upstream_seen_events: Mutex::new(HashMap::new()),
211 upstream_pending_eose: Mutex::new(HashMap::new()),
212 next_client_id: AtomicU64::new(1),
213 upstream_relay_bytes_sent: AtomicU64::new(0),
214 upstream_relay_bytes_received: AtomicU64::new(0),
215 }
216 }
217
218 pub fn next_id(&self) -> u64 {
219 self.next_client_id.fetch_add(1, Ordering::SeqCst)
220 }
221
222 pub fn note_upstream_relay_send(&self, bytes: usize) {
223 self.upstream_relay_bytes_sent
224 .fetch_add(bytes as u64, Ordering::Relaxed);
225 }
226
227 pub fn note_upstream_relay_receive(&self, bytes: usize) {
228 self.upstream_relay_bytes_received
229 .fetch_add(bytes as u64, Ordering::Relaxed);
230 }
231
232 pub fn upstream_relay_bandwidth(&self) -> (u64, u64) {
233 (
234 self.upstream_relay_bytes_sent.load(Ordering::Relaxed),
235 self.upstream_relay_bytes_received.load(Ordering::Relaxed),
236 )
237 }
238}
239
240#[derive(Clone)]
241pub struct AppState {
242 pub store: Arc<HashtreeStore>,
243 pub auth: Option<AuthCredentials>,
244 pub daemon_started_at: u64,
246 pub peer_mode: crate::config::ServerMode,
247 pub hash_get_enabled: bool,
248 pub fips_endpoint: Option<Arc<hashtree_fips_transport::FipsEndpoint>>,
250 pub fips_blob_resolver: Option<Arc<DaemonBlobResolver>>,
252 pub fetch_from_fips_peers: bool,
253 pub ws_relay: Arc<WsRelayState>,
255 pub max_upload_bytes: usize,
257 pub public_writes: bool,
260 pub public_plaintext_reads: bool,
263 pub require_random_untrusted_ingest: bool,
265 pub optimistic_blossom_uploads: bool,
268 pub optimistic_upload_queue_bytes: usize,
271 pub optimistic_upload_queue: Arc<Semaphore>,
272 pub allowed_pubkeys: HashSet<String>,
274 pub upstream_blossom: Vec<String>,
276 pub upstream_http_client: reqwest::Client,
279 pub upstream_blossom_miss_cache: Arc<StdMutex<TimedLruCache<String, ()>>>,
283 pub upstream_blossom_fetch_metrics: Arc<UpstreamBlossomFetchMetrics>,
285 pub blossom_upload_replicas: Vec<String>,
287 pub blossom_upload_replica_queue_bytes: usize,
290 pub blossom_upload_replica_queue: Arc<Semaphore>,
291 pub blossom_upload_replica_keys: Option<Arc<Keys>>,
293 pub blossom_upload_replica_scheduler:
295 Arc<crate::server::blossom::BlossomUploadReplicaScheduler>,
296 pub social_graph: Option<Arc<socialgraph::SocialGraphAccessControl>>,
298 pub social_graph_store: Option<Arc<dyn socialgraph::SocialGraphBackend>>,
300 pub social_graph_root: Option<[u8; 32]>,
302 pub socialgraph_snapshot_public: bool,
304 pub nostr_relay: Option<Arc<NostrRelay>>,
306 pub nostr_provider: Option<Arc<dyn nostr_pubsub::PubsubProvider>>,
308 pub nostr_relay_urls: Vec<String>,
310 pub tree_root_cache: Arc<StdMutex<HashMap<String, CachedTreeRootEntry>>>,
312 pub inflight_blob_fetches: Arc<Mutex<HashMap<String, SharedBlobFetch>>>,
314 pub inflight_blob_reads: Arc<Mutex<HashMap<String, SharedBlobRead>>>,
317 pub(super) blob_cache: Arc<BlobCache>,
319 pub directory_listing_cache: Arc<StdMutex<TimedLruCache<String, LookupResult<Vec<TreeEntry>>>>>,
321 pub resolved_path_cache:
323 Arc<StdMutex<TimedLruCache<String, LookupResult<CachedResolvedPathEntry>>>>,
324 pub thumbnail_path_cache: Arc<StdMutex<TimedLruCache<String, LookupResult<String>>>>,
326 pub cid_size_cache: Arc<StdMutex<TimedLruCache<String, LookupResult<u64>>>>,
328}
329
330pub fn new_upstream_http_client() -> reqwest::Client {
331 reqwest::Client::builder()
332 .timeout(Duration::from_secs(10))
333 .build()
334 .expect("build upstream Blossom HTTP client")
335}
336
337#[derive(Clone)]
338pub struct AuthCredentials {
339 pub username: String,
340 pub password: String,
341}
342
343fn basic_auth_authorized(headers: &HeaderMap, auth: &AuthCredentials) -> bool {
344 let auth_header = headers
345 .get(header::AUTHORIZATION)
346 .and_then(|v| v.to_str().ok());
347
348 let Some(header_value) = auth_header else {
349 return false;
350 };
351 let Some(credentials) = header_value.strip_prefix("Basic ") else {
352 return false;
353 };
354
355 use base64::Engine;
356 let engine = base64::engine::general_purpose::STANDARD;
357 let Ok(decoded) = engine.decode(credentials) else {
358 return false;
359 };
360 let Ok(decoded_str) = String::from_utf8(decoded) else {
361 return false;
362 };
363 let expected = format!("{}:{}", auth.username, auth.password);
364 decoded_str == expected
365}
366
367fn unauthorized_basic_response() -> Response<Body> {
368 Response::builder()
369 .status(StatusCode::UNAUTHORIZED)
370 .header(header::WWW_AUTHENTICATE, "Basic realm=\"hashtree\"")
371 .body(Body::from("Unauthorized"))
372 .unwrap()
373}
374
375pub async fn auth_middleware(
377 State(state): State<AppState>,
378 request: Request<Body>,
379 next: Next,
380) -> Result<Response<Body>, StatusCode> {
381 let Some(auth) = &state.auth else {
383 return Ok(next.run(request).await);
384 };
385
386 if basic_auth_authorized(request.headers(), auth) {
387 Ok(next.run(request).await)
388 } else {
389 Ok(unauthorized_basic_response())
390 }
391}
392
393pub async fn require_auth_middleware(
395 State(state): State<AppState>,
396 request: Request<Body>,
397 next: Next,
398) -> Result<Response<Body>, StatusCode> {
399 let Some(auth) = &state.auth else {
400 return Ok(Response::builder()
401 .status(StatusCode::FORBIDDEN)
402 .body(Body::from("Authentication is not configured"))
403 .unwrap());
404 };
405
406 if basic_auth_authorized(request.headers(), auth) {
407 Ok(next.run(request).await)
408 } else {
409 Ok(unauthorized_basic_response())
410 }
411}