1mod auth;
2pub mod blossom;
3mod handlers;
4mod mime;
5#[cfg(feature = "p2p")]
6pub mod stun;
7mod ui;
8mod ws_relay;
9
10use crate::nostr_relay::NostrRelay;
11use crate::socialgraph;
12use crate::storage::HashtreeStore;
13use crate::webrtc::WebRTCState;
14use anyhow::Result;
15use axum::{
16 extract::DefaultBodyLimit,
17 middleware,
18 routing::{get, post, put},
19 Router,
20};
21use std::collections::HashSet;
22use std::sync::Arc;
23use tower_http::cors::CorsLayer;
24
25pub use auth::{AppState, AuthCredentials};
26
27pub struct HashtreeServer {
28 state: AppState,
29 addr: String,
30 extra_routes: Option<Router<AppState>>,
31 cors: Option<CorsLayer>,
32}
33
34impl HashtreeServer {
35 pub fn new(store: Arc<HashtreeStore>, addr: String) -> Self {
36 Self {
37 state: AppState {
38 store,
39 auth: None,
40 webrtc_peers: None,
41 ws_relay: Arc::new(auth::WsRelayState::new()),
42 max_upload_bytes: 5 * 1024 * 1024, public_writes: true, allowed_pubkeys: HashSet::new(), upstream_blossom: Vec::new(),
46 social_graph: None,
47 social_graph_store: None,
48 social_graph_root: None,
49 socialgraph_snapshot_public: false,
50 nostr_relay: None,
51 tree_root_cache: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
52 },
53 addr,
54 extra_routes: None,
55 cors: None,
56 }
57 }
58
59 pub fn with_max_upload_bytes(mut self, bytes: usize) -> Self {
61 self.state.max_upload_bytes = bytes;
62 self
63 }
64
65 pub fn with_public_writes(mut self, public: bool) -> Self {
68 self.state.public_writes = public;
69 self
70 }
71
72 pub fn with_webrtc_peers(mut self, webrtc_state: Arc<WebRTCState>) -> Self {
74 self.state.webrtc_peers = Some(webrtc_state);
75 self
76 }
77
78 pub fn with_auth(mut self, username: String, password: String) -> Self {
79 self.state.auth = Some(AuthCredentials { username, password });
80 self
81 }
82
83 pub fn with_allowed_pubkeys(mut self, pubkeys: HashSet<String>) -> Self {
85 self.state.allowed_pubkeys = pubkeys;
86 self
87 }
88
89 pub fn with_upstream_blossom(mut self, servers: Vec<String>) -> Self {
91 self.state.upstream_blossom = servers;
92 self
93 }
94
95 pub fn with_social_graph(mut self, sg: Arc<socialgraph::SocialGraphAccessControl>) -> Self {
97 self.state.social_graph = Some(sg);
98 self
99 }
100
101 pub fn with_socialgraph_snapshot(
103 mut self,
104 store: Arc<dyn socialgraph::SocialGraphBackend>,
105 root: [u8; 32],
106 public: bool,
107 ) -> Self {
108 self.state.social_graph_store = Some(store);
109 self.state.social_graph_root = Some(root);
110 self.state.socialgraph_snapshot_public = public;
111 self
112 }
113
114 pub fn with_nostr_relay(mut self, relay: Arc<NostrRelay>) -> Self {
116 self.state.nostr_relay = Some(relay);
117 self
118 }
119
120 pub fn with_extra_routes(mut self, routes: Router<AppState>) -> Self {
122 self.extra_routes = Some(routes);
123 self
124 }
125
126 pub fn with_cors(mut self, cors: CorsLayer) -> Self {
128 self.cors = Some(cors);
129 self
130 }
131
132 pub async fn run(self) -> Result<()> {
133 let listener = tokio::net::TcpListener::bind(&self.addr).await?;
134 let _ = self.run_with_listener(listener).await?;
135 Ok(())
136 }
137
138 pub async fn run_with_listener(self, listener: tokio::net::TcpListener) -> Result<u16> {
139 let local_addr = listener.local_addr()?;
140
141 let state = self.state.clone();
145 let public_routes = Router::new()
146 .route("/", get(handlers::serve_root))
147 .route("/ws", get(ws_relay::ws_data))
148 .route(
149 "/htree/test",
150 get(handlers::htree_test).head(handlers::htree_test),
151 )
152 .route("/htree/nhash1:nhash", get(handlers::htree_nhash))
154 .route("/htree/nhash1:nhash/", get(handlers::htree_nhash))
155 .route("/htree/nhash1:nhash/*path", get(handlers::htree_nhash_path))
156 .route("/htree/npub1:npub/:treename", get(handlers::htree_npub))
158 .route("/htree/npub1:npub/:treename/", get(handlers::htree_npub))
159 .route(
160 "/htree/npub1:npub/:treename/*path",
161 get(handlers::htree_npub_path),
162 )
163 .route("/n/:pubkey/:treename", get(handlers::resolve_and_serve))
165 .route("/npub1:rest", get(handlers::serve_npub))
167 .route(
169 "/:id",
170 get(handlers::serve_content_or_blob)
171 .head(blossom::head_blob)
172 .delete(blossom::delete_blob)
173 .options(blossom::cors_preflight),
174 )
175 .route(
176 "/upload",
177 put(blossom::upload_blob).options(blossom::cors_preflight),
178 )
179 .route(
180 "/list/:pubkey",
181 get(blossom::list_blobs).options(blossom::cors_preflight),
182 )
183 .route("/health", get(handlers::health_check))
185 .route("/api/pins", get(handlers::list_pins))
186 .route("/api/stats", get(handlers::storage_stats))
187 .route("/api/peers", get(handlers::webrtc_peers))
188 .route("/api/status", get(handlers::daemon_status))
189 .route("/api/socialgraph", get(handlers::socialgraph_stats))
190 .route(
191 "/api/socialgraph/snapshot",
192 get(handlers::socialgraph_snapshot),
193 )
194 .route(
195 "/api/socialgraph/distance/:pubkey",
196 get(handlers::follow_distance),
197 )
198 .route(
200 "/api/resolve/:pubkey/:treename",
201 get(handlers::resolve_to_hash),
202 )
203 .route(
204 "/api/nostr/resolve/:pubkey/:treename",
205 get(handlers::resolve_to_hash),
206 )
207 .route("/api/trees/:pubkey", get(handlers::list_trees))
208 .with_state(state.clone());
209
210 let protected_routes = Router::new()
212 .route("/upload", post(handlers::upload_file))
213 .route("/api/pin/:cid", post(handlers::pin_cid))
214 .route("/api/unpin/:cid", post(handlers::unpin_cid))
215 .route("/api/gc", post(handlers::garbage_collect))
216 .layer(middleware::from_fn_with_state(
217 state.clone(),
218 auth::auth_middleware,
219 ))
220 .with_state(state.clone());
221
222 let mut app = public_routes
223 .merge(protected_routes)
224 .layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)); if let Some(extra) = self.extra_routes {
227 app = app.merge(extra.with_state(state));
228 }
229
230 if let Some(cors) = self.cors {
231 app = app.layer(cors);
232 }
233
234 axum::serve(
235 listener,
236 app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
237 )
238 .await?;
239
240 Ok(local_addr.port())
241 }
242
243 pub fn addr(&self) -> &str {
244 &self.addr
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use crate::storage::HashtreeStore;
252 use hashtree_core::from_hex;
253 use tempfile::TempDir;
254
255 #[tokio::test]
256 async fn test_server_serve_file() -> Result<()> {
257 let temp_dir = TempDir::new()?;
258 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
259
260 let test_file = temp_dir.path().join("test.txt");
262 std::fs::write(&test_file, b"Hello, Hashtree!")?;
263
264 let cid = store.upload_file(&test_file)?;
265 let hash = from_hex(&cid)?;
266
267 let content = store.get_file(&hash)?;
269 assert!(content.is_some());
270 assert_eq!(content.unwrap(), b"Hello, Hashtree!");
271
272 Ok(())
273 }
274
275 #[tokio::test]
276 async fn test_server_list_pins() -> Result<()> {
277 let temp_dir = TempDir::new()?;
278 let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
279
280 let test_file = temp_dir.path().join("test.txt");
281 std::fs::write(&test_file, b"Test")?;
282
283 let cid = store.upload_file(&test_file)?;
284 let hash = from_hex(&cid)?;
285
286 let pins = store.list_pins_raw()?;
287 assert_eq!(pins.len(), 1);
288 assert_eq!(pins[0], hash);
289
290 Ok(())
291 }
292}