hashtree_cli/server/
mod.rs

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