upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/http/mod.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 23:47:27 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 23:47:27 +0000
commit59dbbf0f2986e8d969cc30b57d70f76984a272e3 (patch)
tree303601548a7444dbfbe797684a8cd3371005b0ae /src/http/mod.rs
parente6c056023bac4a83930b9c40f4a9513c3680cb67 (diff)
add repo land page and 404 page per GRASP-01
Diffstat (limited to 'src/http/mod.rs')
-rw-r--r--src/http/mod.rs106
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 = "*";
32const CORS_ALLOW_METHODS: &str = "GET, POST"; 32const CORS_ALLOW_METHODS: &str = "GET, POST";
33const CORS_ALLOW_HEADERS: &str = "Content-Type"; 33const 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
40fn 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
36fn add_cors_headers(builder: hyper::http::response::Builder) -> hyper::http::response::Builder { 77fn 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}