diff options
Diffstat (limited to 'src/http/mod.rs')
| -rw-r--r-- | src/http/mod.rs | 106 |
1 files changed, 99 insertions, 7 deletions
diff --git a/src/http/mod.rs b/src/http/mod.rs index f43cf86..6da027c 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs | |||
| @@ -32,6 +32,47 @@ const CORS_ALLOW_ORIGIN: &str = "*"; | |||
| 32 | const CORS_ALLOW_METHODS: &str = "GET, POST"; | 32 | const CORS_ALLOW_METHODS: &str = "GET, POST"; |
| 33 | const CORS_ALLOW_HEADERS: &str = "Content-Type"; | 33 | const CORS_ALLOW_HEADERS: &str = "Content-Type"; |
| 34 | 34 | ||
| 35 | /// Extract npub and identifier from a repository URL path (no git subpath required) | ||
| 36 | /// | ||
| 37 | /// Parses paths like `/<npub>/<identifier>.git` (for repository webpage/404) | ||
| 38 | /// | ||
| 39 | /// Returns (npub, identifier) if the path matches a repository URL pattern | ||
| 40 | fn parse_repo_url(path: &str) -> Option<(&str, &str)> { | ||
| 41 | // Remove leading slash | ||
| 42 | let path = path.strip_prefix('/').unwrap_or(path); | ||
| 43 | |||
| 44 | // Split into components | ||
| 45 | let parts: Vec<&str> = path.split('/').collect(); | ||
| 46 | |||
| 47 | // Must be exactly 2 parts: npub and repo.git (no subpath) | ||
| 48 | if parts.len() != 2 { | ||
| 49 | return None; | ||
| 50 | } | ||
| 51 | |||
| 52 | let npub = parts[0]; | ||
| 53 | let repo_part = parts[1]; | ||
| 54 | |||
| 55 | // The repo part must end with .git | ||
| 56 | if !repo_part.ends_with(".git") { | ||
| 57 | return None; | ||
| 58 | } | ||
| 59 | |||
| 60 | // Must have an npub that looks valid (starts with npub1) | ||
| 61 | if !npub.starts_with("npub1") { | ||
| 62 | return None; | ||
| 63 | } | ||
| 64 | |||
| 65 | // Extract identifier (remove .git suffix) | ||
| 66 | let identifier = repo_part.strip_suffix(".git").unwrap_or(repo_part); | ||
| 67 | |||
| 68 | // Identifier must not be empty | ||
| 69 | if identifier.is_empty() { | ||
| 70 | return None; | ||
| 71 | } | ||
| 72 | |||
| 73 | Some((npub, identifier)) | ||
| 74 | } | ||
| 75 | |||
| 35 | /// Add CORS headers to a response builder | 76 | /// Add CORS headers to a response builder |
| 36 | fn add_cors_headers(builder: hyper::http::response::Builder) -> hyper::http::response::Builder { | 77 | fn add_cors_headers(builder: hyper::http::response::Builder) -> hyper::http::response::Builder { |
| 37 | builder | 78 | builder |
| @@ -230,6 +271,45 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 230 | } | 271 | } |
| 231 | } | 272 | } |
| 232 | 273 | ||
| 274 | // Check for repository URL pattern (e.g., /npub/repo.git without subpath) | ||
| 275 | // GRASP-01: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s) | ||
| 276 | // to browse the repository and a 404 page for repositories it doesn't host" | ||
| 277 | if let Some((npub, identifier)) = parse_repo_url(&path) { | ||
| 278 | let npub = npub.to_string(); | ||
| 279 | let identifier = identifier.to_string(); | ||
| 280 | let config = self.config.clone(); | ||
| 281 | let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); | ||
| 282 | |||
| 283 | tracing::debug!( | ||
| 284 | "Repository URL request: {} (npub={}, id={}, path={:?})", | ||
| 285 | path, | ||
| 286 | npub, | ||
| 287 | identifier, | ||
| 288 | repo_path | ||
| 289 | ); | ||
| 290 | |||
| 291 | return Box::pin(async move { | ||
| 292 | // Check if repository exists | ||
| 293 | if repo_path.exists() { | ||
| 294 | // Serve repository webpage | ||
| 295 | let html = landing::get_repo_html(&config, &npub, &identifier); | ||
| 296 | Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp")) | ||
| 297 | .status(200) | ||
| 298 | .header("content-type", "text/html; charset=utf-8") | ||
| 299 | .body(Full::new(Bytes::from(html))) | ||
| 300 | .unwrap()) | ||
| 301 | } else { | ||
| 302 | // Serve 404 page for non-existent repository | ||
| 303 | let html = landing::get_404_html(&config, &npub, &identifier); | ||
| 304 | Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp")) | ||
| 305 | .status(404) | ||
| 306 | .header("content-type", "text/html; charset=utf-8") | ||
| 307 | .body(Full::new(Bytes::from(html))) | ||
| 308 | .unwrap()) | ||
| 309 | } | ||
| 310 | }); | ||
| 311 | } | ||
| 312 | |||
| 233 | // Check if this is a WebSocket upgrade request | 313 | // Check if this is a WebSocket upgrade request |
| 234 | if let (Some(c), Some(w)) = ( | 314 | if let (Some(c), Some(w)) = ( |
| 235 | req.headers().get("connection"), | 315 | req.headers().get("connection"), |
| @@ -275,14 +355,26 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 275 | } | 355 | } |
| 276 | } | 356 | } |
| 277 | 357 | ||
| 278 | // Serve landing page for HTTP requests | 358 | // Only serve landing page for root path "/", 404 for everything else |
| 279 | let html = landing::get_html(&self.config); | 359 | let config = self.config.clone(); |
| 280 | Box::pin(async move { | 360 | Box::pin(async move { |
| 281 | Ok(base | 361 | if path == "/" { |
| 282 | .status(200) | 362 | // Serve landing page for root |
| 283 | .header("content-type", "text/html; charset=utf-8") | 363 | let html = landing::get_html(&config); |
| 284 | .body(Full::new(Bytes::from(html))) | 364 | Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp")) |
| 285 | .unwrap()) | 365 | .status(200) |
| 366 | .header("content-type", "text/html; charset=utf-8") | ||
| 367 | .body(Full::new(Bytes::from(html))) | ||
| 368 | .unwrap()) | ||
| 369 | } else { | ||
| 370 | // Serve generic 404 for unknown paths | ||
| 371 | let html = landing::get_generic_404_html(&config, &path); | ||
| 372 | Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp")) | ||
| 373 | .status(404) | ||
| 374 | .header("content-type", "text/html; charset=utf-8") | ||
| 375 | .body(Full::new(Bytes::from(html))) | ||
| 376 | .unwrap()) | ||
| 377 | } | ||
| 286 | }) | 378 | }) |
| 287 | } | 379 | } |
| 288 | } | 380 | } |