hashtree_cli/server/
mod.rs

1mod auth;
2pub mod blossom;
3mod handlers;
4mod mime;
5pub mod stun;
6mod ui;
7
8use anyhow::Result;
9use axum::{
10    extract::DefaultBodyLimit,
11    middleware,
12    routing::{get, post, put},
13    Router,
14};
15use crate::storage::HashtreeStore;
16use crate::webrtc::WebRTCState;
17use std::collections::HashSet;
18use std::sync::Arc;
19
20pub use auth::{AppState, AuthCredentials};
21
22pub struct HashtreeServer {
23    state: AppState,
24    addr: String,
25}
26
27impl HashtreeServer {
28    pub fn new(store: Arc<HashtreeStore>, addr: String) -> Self {
29        Self {
30            state: AppState {
31                store,
32                auth: None,
33                webrtc_peers: None,
34                max_upload_bytes: 5 * 1024 * 1024, // 5 MB default
35                public_writes: true, // Allow anyone with valid Nostr auth by default
36                allowed_pubkeys: HashSet::new(), // No pubkeys allowed by default (use public_writes)
37            },
38            addr,
39        }
40    }
41
42    /// Set maximum upload size for Blossom uploads
43    pub fn with_max_upload_bytes(mut self, bytes: usize) -> Self {
44        self.state.max_upload_bytes = bytes;
45        self
46    }
47
48    /// Set whether to allow public writes (anyone with valid Nostr auth)
49    /// When false, only social graph members can write
50    pub fn with_public_writes(mut self, public: bool) -> Self {
51        self.state.public_writes = public;
52        self
53    }
54
55    /// Set WebRTC state for P2P peer queries
56    pub fn with_webrtc_peers(mut self, webrtc_state: Arc<WebRTCState>) -> Self {
57        self.state.webrtc_peers = Some(webrtc_state);
58        self
59    }
60
61    pub fn with_auth(mut self, username: String, password: String) -> Self {
62        self.state.auth = Some(AuthCredentials { username, password });
63        self
64    }
65
66    /// Set allowed pubkeys for blossom write access (hex format)
67    pub fn with_allowed_pubkeys(mut self, pubkeys: HashSet<String>) -> Self {
68        self.state.allowed_pubkeys = pubkeys;
69        self
70    }
71
72    pub async fn run(self) -> Result<()> {
73        // Public endpoints (no auth required)
74        // Note: /:id serves both CID and blossom SHA256 hash lookups
75        // The handler differentiates based on hash format (64 char hex = blossom)
76        let public_routes = Router::new()
77            .route("/", get(handlers::serve_root))
78            // Nostr resolver endpoints - resolve npub/treename to content
79            .route("/n/:pubkey/:treename", get(handlers::resolve_and_serve))
80            // Direct npub/nhash routes (cleaner URLs without /n/ prefix)
81            .route("/npub1:rest", get(handlers::serve_npub))
82            .route("/nhash1:rest", get(handlers::serve_nhash))
83            // Blossom endpoints (BUD-01, BUD-02)
84            .route("/:id", get(handlers::serve_content_or_blob)
85                .head(blossom::head_blob)
86                .delete(blossom::delete_blob)
87                .options(blossom::cors_preflight))
88            .route("/upload", put(blossom::upload_blob)
89                .options(blossom::cors_preflight))
90            .route("/list/:pubkey", get(blossom::list_blobs)
91                .options(blossom::cors_preflight))
92            // Hashtree API endpoints
93            .route("/api/pins", get(handlers::list_pins))
94            .route("/api/stats", get(handlers::storage_stats))
95            .route("/api/peers", get(handlers::webrtc_peers))
96            .route("/api/socialgraph", get(handlers::socialgraph_stats))
97            // Resolver API endpoints
98            .route("/api/resolve/:pubkey/:treename", get(handlers::resolve_to_hash))
99            .route("/api/trees/:pubkey", get(handlers::list_trees))
100            .with_state(self.state.clone());
101
102        // Protected endpoints (require auth if enabled)
103        let protected_routes = Router::new()
104            .route("/upload", post(handlers::upload_file))
105            .route("/api/pin/:cid", post(handlers::pin_cid))
106            .route("/api/unpin/:cid", post(handlers::unpin_cid))
107            .route("/api/gc", post(handlers::garbage_collect))
108            .layer(middleware::from_fn_with_state(
109                self.state.clone(),
110                auth::auth_middleware,
111            ))
112            .with_state(self.state);
113
114        let app = public_routes
115            .merge(protected_routes)
116            .layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)); // 10GB limit
117
118        let listener = tokio::net::TcpListener::bind(&self.addr).await?;
119        axum::serve(
120            listener,
121            app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
122        ).await?;
123
124        Ok(())
125    }
126
127    pub fn addr(&self) -> &str {
128        &self.addr
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::storage::HashtreeStore;
136    use tempfile::TempDir;
137    use std::path::Path;
138
139    #[tokio::test]
140    async fn test_server_serve_file() -> Result<()> {
141        let temp_dir = TempDir::new()?;
142        let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
143
144        // Create and upload a test file
145        let test_file = temp_dir.path().join("test.txt");
146        std::fs::write(&test_file, b"Hello, Hashtree!")?;
147
148        let cid = store.upload_file(&test_file)?;
149
150        // Verify we can get it
151        let content = store.get_file(&cid)?;
152        assert!(content.is_some());
153        assert_eq!(content.unwrap(), b"Hello, Hashtree!");
154
155        Ok(())
156    }
157
158    #[tokio::test]
159    async fn test_server_list_pins() -> Result<()> {
160        let temp_dir = TempDir::new()?;
161        let store = Arc::new(HashtreeStore::new(temp_dir.path().join("db"))?);
162
163        let test_file = temp_dir.path().join("test.txt");
164        std::fs::write(&test_file, b"Test")?;
165
166        let cid = store.upload_file(&test_file)?;
167
168        let pins = store.list_pins()?;
169        assert_eq!(pins.len(), 1);
170        assert_eq!(pins[0], cid);
171
172        Ok(())
173    }
174}