hashtree_cli/server/
mod.rs1mod 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, public_writes: true, allowed_pubkeys: HashSet::new(), 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 pub fn with_max_upload_bytes(mut self, bytes: usize) -> Self {
57 self.state.max_upload_bytes = bytes;
58 self
59 }
60
61 pub fn with_public_writes(mut self, public: bool) -> Self {
64 self.state.public_writes = public;
65 self
66 }
67
68 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 pub fn with_allowed_pubkeys(mut self, pubkeys: HashSet<String>) -> Self {
81 self.state.allowed_pubkeys = pubkeys;
82 self
83 }
84
85 pub fn with_upstream_blossom(mut self, servers: Vec<String>) -> Self {
87 self.state.upstream_blossom = servers;
88 self
89 }
90
91 pub fn with_social_graph(mut self, sg: Arc<socialgraph::SocialGraphAccessControl>) -> Self {
93 self.state.social_graph = Some(sg);
94 self
95 }
96
97 pub fn with_nostr_relay(mut self, relay: Arc<NostrRelay>) -> Self {
99 self.state.nostr_relay = Some(relay);
100 self
101 }
102
103 pub fn with_extra_routes(mut self, routes: Router<AppState>) -> Self {
105 self.extra_routes = Some(routes);
106 self
107 }
108
109 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 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 .route("/htree/nhash1:nhash", get(handlers::htree_nhash))
134 .route("/htree/nhash1:nhash/*path", get(handlers::htree_nhash_path))
135 .route("/htree/npub1:npub/:treename", get(handlers::htree_npub))
137 .route("/htree/npub1:npub/:treename/*path", get(handlers::htree_npub_path))
138 .route("/n/:pubkey/:treename", get(handlers::resolve_and_serve))
140 .route("/npub1:rest", get(handlers::serve_npub))
142 .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 .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 .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 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)); 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 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 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}