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