Skip to main content

hashtree_cli/server/
mod.rs

1mod auth;
2pub mod blossom;
3mod handlers;
4mod ws_relay;
5mod mime;
6#[cfg(feature = "p2p")]
7pub mod stun;
8mod ui;
9
10use anyhow::Result;
11use axum::{
12    extract::DefaultBodyLimit,
13    middleware,
14    routing::{get, post, put},
15    Router,
16};
17use tower_http::cors::CorsLayer;
18use crate::socialgraph;
19use crate::storage::HashtreeStore;
20use crate::webrtc::WebRTCState;
21use crate::nostr_relay::NostrRelay;
22use std::collections::HashSet;
23use std::sync::Arc;
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                nostr_relay: None,
48            },
49            addr,
50            extra_routes: None,
51            cors: None,
52        }
53    }
54
55    /// Set maximum upload size for Blossom uploads
56    pub fn with_max_upload_bytes(mut self, bytes: usize) -> Self {
57        self.state.max_upload_bytes = bytes;
58        self
59    }
60
61    /// Set whether to allow public writes (anyone with valid Nostr auth)
62    /// When false, only social graph members can write
63    pub fn with_public_writes(mut self, public: bool) -> Self {
64        self.state.public_writes = public;
65        self
66    }
67
68    /// Set WebRTC state for P2P peer queries
69    pub fn with_webrtc_peers(mut self, webrtc_state: Arc<WebRTCState>) -> Self {
70        self.state.webrtc_peers = Some(webrtc_state);
71        self
72    }
73
74    pub fn with_auth(mut self, username: String, password: String) -> Self {
75        self.state.auth = Some(AuthCredentials { username, password });
76        self
77    }
78
79    /// Set allowed pubkeys for blossom write access (hex format)
80    pub fn with_allowed_pubkeys(mut self, pubkeys: HashSet<String>) -> Self {
81        self.state.allowed_pubkeys = pubkeys;
82        self
83    }
84
85    /// Set upstream Blossom servers for cascade fetching
86    pub fn with_upstream_blossom(mut self, servers: Vec<String>) -> Self {
87        self.state.upstream_blossom = servers;
88        self
89    }
90
91    /// Set social graph access control
92    pub fn with_social_graph(mut self, sg: Arc<socialgraph::SocialGraphAccessControl>) -> Self {
93        self.state.social_graph = Some(sg);
94        self
95    }
96
97    /// Set Nostr relay state (shared for /ws and WebRTC)
98    pub fn with_nostr_relay(mut self, relay: Arc<NostrRelay>) -> Self {
99        self.state.nostr_relay = Some(relay);
100        self
101    }
102
103    /// Merge extra routes into the daemon router (e.g. Tauri embeds /nip07).
104    pub fn with_extra_routes(mut self, routes: Router<AppState>) -> Self {
105        self.extra_routes = Some(routes);
106        self
107    }
108
109    /// Apply a CORS layer to all routes (used by embedded clients like Tauri).
110    pub fn with_cors(mut self, cors: CorsLayer) -> Self {
111        self.cors = Some(cors);
112        self
113    }
114
115    pub async fn run(self) -> Result<()> {
116        let listener = tokio::net::TcpListener::bind(&self.addr).await?;
117        let _ = self.run_with_listener(listener).await?;
118        Ok(())
119    }
120
121    pub async fn run_with_listener(self, listener: tokio::net::TcpListener) -> Result<u16> {
122        let local_addr = listener.local_addr()?;
123
124        // Public endpoints (no auth required)
125        // Note: /:id serves both CID and blossom SHA256 hash lookups
126        // The handler differentiates based on hash format (64 char hex = blossom)
127        let state = self.state.clone();
128        let public_routes = Router::new()
129            .route("/", get(handlers::serve_root))
130            .route("/ws", get(ws_relay::ws_data))
131            .route("/htree/test", get(handlers::htree_test).head(handlers::htree_test))
132            // /htree/nhash1...[/path] - content-addressed (immutable)
133            .route("/htree/nhash1:nhash", get(handlers::htree_nhash))
134            .route("/htree/nhash1:nhash/*path", get(handlers::htree_nhash_path))
135            // /htree/npub1.../tree[/path] - mutable (resolver-backed)
136            .route("/htree/npub1:npub/:treename", get(handlers::htree_npub))
137            .route("/htree/npub1:npub/:treename/*path", get(handlers::htree_npub_path))
138            // Nostr resolver endpoints - resolve npub/treename to content
139            .route("/n/:pubkey/:treename", get(handlers::resolve_and_serve))
140            // Direct npub route (clients should parse nhash and request by hex hash)
141            .route("/npub1:rest", get(handlers::serve_npub))
142            // Blossom endpoints (BUD-01, BUD-02)
143            .route("/:id", get(handlers::serve_content_or_blob)
144                .head(blossom::head_blob)
145                .delete(blossom::delete_blob)
146                .options(blossom::cors_preflight))
147            .route("/upload", put(blossom::upload_blob)
148                .options(blossom::cors_preflight))
149            .route("/list/:pubkey", get(blossom::list_blobs)
150                .options(blossom::cors_preflight))
151            // Hashtree API endpoints
152            .route("/health", get(handlers::health_check))
153            .route("/api/pins", get(handlers::list_pins))
154            .route("/api/stats", get(handlers::storage_stats))
155            .route("/api/peers", get(handlers::webrtc_peers))
156            .route("/api/status", get(handlers::daemon_status))
157            .route("/api/socialgraph", get(handlers::socialgraph_stats))
158            .route("/api/socialgraph/distance/:pubkey", get(handlers::follow_distance))
159            // Resolver API endpoints
160            .route("/api/resolve/:pubkey/:treename", get(handlers::resolve_to_hash))
161            .route("/api/trees/:pubkey", get(handlers::list_trees))
162            .with_state(state.clone());
163
164        // Protected endpoints (require auth if enabled)
165        let protected_routes = Router::new()
166            .route("/upload", post(handlers::upload_file))
167            .route("/api/pin/:cid", post(handlers::pin_cid))
168            .route("/api/unpin/:cid", post(handlers::unpin_cid))
169            .route("/api/gc", post(handlers::garbage_collect))
170            .layer(middleware::from_fn_with_state(
171                state.clone(),
172                auth::auth_middleware,
173            ))
174            .with_state(state.clone());
175
176        let mut app = public_routes
177            .merge(protected_routes)
178            .layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)); // 10GB limit
179
180        if let Some(extra) = self.extra_routes {
181            app = app.merge(extra.with_state(state));
182        }
183
184        if let Some(cors) = self.cors {
185            app = app.layer(cors);
186        }
187
188        axum::serve(
189            listener,
190            app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
191        ).await?;
192
193        Ok(local_addr.port())
194    }
195
196    pub fn addr(&self) -> &str {
197        &self.addr
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::storage::HashtreeStore;
205    use tempfile::TempDir;
206    use hashtree_core::from_hex;
207
208    #[tokio::test]
209    async fn test_server_serve_file() -> Result<()> {
210        let temp_dir = TempDir::new()?;
211        let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
212
213        // Create and upload a test file
214        let test_file = temp_dir.path().join("test.txt");
215        std::fs::write(&test_file, b"Hello, Hashtree!")?;
216
217        let cid = store.upload_file(&test_file)?;
218        let hash = from_hex(&cid)?;
219
220        // Verify we can get it
221        let content = store.get_file(&hash)?;
222        assert!(content.is_some());
223        assert_eq!(content.unwrap(), b"Hello, Hashtree!");
224
225        Ok(())
226    }
227
228    #[tokio::test]
229    async fn test_server_list_pins() -> Result<()> {
230        let temp_dir = TempDir::new()?;
231        let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
232
233        let test_file = temp_dir.path().join("test.txt");
234        std::fs::write(&test_file, b"Test")?;
235
236        let cid = store.upload_file(&test_file)?;
237        let hash = from_hex(&cid)?;
238
239        let pins = store.list_pins_raw()?;
240        assert_eq!(pins.len(), 1);
241        assert_eq!(pins[0], hash);
242
243        Ok(())
244    }
245}