Skip to main content

hashtree_cli/server/
mod.rs

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, // 5 MB default
43                public_writes: true,               // Allow anyone with valid Nostr auth by default
44                allowed_pubkeys: HashSet::new(), // No pubkeys allowed by default (use public_writes)
45                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    /// Set maximum upload size for Blossom uploads
60    pub fn with_max_upload_bytes(mut self, bytes: usize) -> Self {
61        self.state.max_upload_bytes = bytes;
62        self
63    }
64
65    /// Set whether to allow public writes (anyone with valid Nostr auth)
66    /// When false, only social graph members can write
67    pub fn with_public_writes(mut self, public: bool) -> Self {
68        self.state.public_writes = public;
69        self
70    }
71
72    /// Set WebRTC state for P2P peer queries
73    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    /// Set allowed pubkeys for blossom write access (hex format)
84    pub fn with_allowed_pubkeys(mut self, pubkeys: HashSet<String>) -> Self {
85        self.state.allowed_pubkeys = pubkeys;
86        self
87    }
88
89    /// Set upstream Blossom servers for cascade fetching
90    pub fn with_upstream_blossom(mut self, servers: Vec<String>) -> Self {
91        self.state.upstream_blossom = servers;
92        self
93    }
94
95    /// Set social graph access control
96    pub fn with_social_graph(mut self, sg: Arc<socialgraph::SocialGraphAccessControl>) -> Self {
97        self.state.social_graph = Some(sg);
98        self
99    }
100
101    /// Configure social graph snapshot export (store handle + root)
102    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    /// Set Nostr relay state (shared for /ws and WebRTC)
115    pub fn with_nostr_relay(mut self, relay: Arc<NostrRelay>) -> Self {
116        self.state.nostr_relay = Some(relay);
117        self
118    }
119
120    /// Merge extra routes into the daemon router (e.g. Tauri embeds /nip07).
121    pub fn with_extra_routes(mut self, routes: Router<AppState>) -> Self {
122        self.extra_routes = Some(routes);
123        self
124    }
125
126    /// Apply a CORS layer to all routes (used by embedded clients like Tauri).
127    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        // Public endpoints (no auth required)
142        // Note: /:id serves both CID and blossom SHA256 hash lookups
143        // The handler differentiates based on hash format (64 char hex = blossom)
144        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            // /htree/nhash1...[/path] - content-addressed (immutable)
153            .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            // /htree/npub1.../tree[/path] - mutable (resolver-backed)
157            .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            // Nostr resolver endpoints - resolve npub/treename to content
164            .route("/n/:pubkey/:treename", get(handlers::resolve_and_serve))
165            // Direct npub route (clients should parse nhash and request by hex hash)
166            .route("/npub1:rest", get(handlers::serve_npub))
167            // Blossom endpoints (BUD-01, BUD-02)
168            .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            // Hashtree API endpoints
184            .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            // Resolver API endpoints
199            .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        // Protected endpoints (require auth if enabled)
211        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)); // 10GB limit
225
226        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        // Create and upload a test file
261        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        // Verify we can get it
268        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}