upleb.uk

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

summaryrefslogtreecommitdiff
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
parente6c056023bac4a83930b9c40f4a9513c3680cb67 (diff)
add repo land page and 404 page per GRASP-01
-rw-r--r--grasp-audit/src/specs/grasp01/repository_creation.rs193
-rw-r--r--src/http/landing.rs407
-rw-r--r--src/http/mod.rs106
-rw-r--r--tests/repository_creation.rs2
4 files changed, 701 insertions, 7 deletions
diff --git a/grasp-audit/src/specs/grasp01/repository_creation.rs b/grasp-audit/src/specs/grasp01/repository_creation.rs
index 0b3eed5..1014aa3 100644
--- a/grasp-audit/src/specs/grasp01/repository_creation.rs
+++ b/grasp-audit/src/specs/grasp01/repository_creation.rs
@@ -27,6 +27,8 @@ impl RepositoryCreationTests {
27 let mut results = crate::AuditResult::new("GRASP-01 Repository Creation Tests"); 27 let mut results = crate::AuditResult::new("GRASP-01 Repository Creation Tests");
28 28
29 results.add(Self::test_bare_repo_created_on_announcement(client, relay_domain).await); 29 results.add(Self::test_bare_repo_created_on_announcement(client, relay_domain).await);
30 results.add(Self::test_webpage_served_for_existing_repo(client, relay_domain).await);
31 results.add(Self::test_404_for_nonexistent_repo(client, relay_domain).await);
30 32
31 results 33 results
32 } 34 }
@@ -109,6 +111,144 @@ impl RepositoryCreationTests {
109 ) 111 )
110 .pass() 112 .pass()
111 } 113 }
114
115 /// Test that a webpage is served for an existing repository
116 ///
117 /// This test verifies:
118 /// 1. Creates a valid repository announcement
119 /// 2. Accesses the repository URL without git service parameters
120 /// 3. Verifies a webpage is returned (any 2xx status with HTML content)
121 ///
122 /// GRASP-01: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s)"
123 pub async fn test_webpage_served_for_existing_repo(
124 client: &AuditClient,
125 relay_domain: &str,
126 ) -> TestResult {
127 let test_name = "test_webpage_served_for_existing_repo";
128 let ctx = TestContext::new(client);
129
130 // Create a repository announcement
131 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
132 Ok(r) => r,
133 Err(e) => {
134 return TestResult::new(
135 test_name,
136 "GRASP-01",
137 "Relay SHOULD serve a webpage for existing repositories",
138 )
139 .fail(format!("Failed to create repo fixture: {}", e))
140 }
141 };
142
143 // Wait for repository creation
144 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
145
146 // Extract repo identifier and npub
147 let repo_id = match repo
148 .tags
149 .iter()
150 .find(|t| t.kind() == TagKind::d())
151 .and_then(|t| t.content())
152 {
153 Some(id) => id.to_string(),
154 None => {
155 return TestResult::new(
156 test_name,
157 "GRASP-01",
158 "Relay SHOULD serve a webpage for existing repositories",
159 )
160 .fail("Repository announcement missing d tag")
161 }
162 };
163
164 let npub = match repo.pubkey.to_bech32() {
165 Ok(n) => n,
166 Err(e) => {
167 return TestResult::new(
168 test_name,
169 "GRASP-01",
170 "Relay SHOULD serve a webpage for existing repositories",
171 )
172 .fail(format!("Failed to convert pubkey to npub: {}", e))
173 }
174 };
175
176 // Check that a webpage is served at the repository URL
177 if let Err(e) = check_webpage_served(relay_domain, &npub, &repo_id).await {
178 return TestResult::new(
179 test_name,
180 "GRASP-01",
181 "Relay SHOULD serve a webpage for existing repositories",
182 )
183 .fail(format!("Webpage not served: {}", e));
184 }
185
186 TestResult::new(
187 test_name,
188 "GRASP-01",
189 "Relay SHOULD serve a webpage for existing repositories",
190 )
191 .pass()
192 }
193
194 /// Test that 404 is returned for non-existent repositories
195 ///
196 /// This test verifies:
197 /// 1. Accesses a URL for a repository that doesn't exist
198 /// 2. Verifies a 404 status is returned
199 ///
200 /// GRASP-01: "...and a 404 page for repositories it doesn't host"
201 pub async fn test_404_for_nonexistent_repo(
202 client: &AuditClient,
203 relay_domain: &str,
204 ) -> TestResult {
205 let test_name = "test_404_for_nonexistent_repo";
206
207 let ctx = TestContext::new(client);
208
209 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
210 Ok(r) => r,
211 Err(e) => {
212 return TestResult::new(
213 test_name,
214 "GRASP-01",
215 "Relay SHOULD serve a webpage for existing repositories",
216 )
217 .fail(format!("Failed to create repo fixture: {}", e))
218 }
219 };
220
221 let npub = match repo.pubkey.to_bech32() {
222 Ok(n) => n,
223 Err(e) => {
224 return TestResult::new(
225 test_name,
226 "GRASP-01",
227 "Relay SHOULD serve a webpage for existing repositories",
228 )
229 .fail(format!("Failed to convert pubkey to npub: {}", e))
230 }
231 };
232 // Use a clearly non-existent repo id but real npub
233 let fake_repo_id = "nonexistent-repo-12345";
234
235 // Check that 404 is returned
236 if let Err(e) = check_404_for_nonexistent_repo(relay_domain, &npub, fake_repo_id).await {
237 return TestResult::new(
238 test_name,
239 "GRASP-01",
240 "Relay SHOULD return 404 for repositories it doesn't host",
241 )
242 .fail(format!("Expected 404, got: {}", e));
243 }
244
245 TestResult::new(
246 test_name,
247 "GRASP-01",
248 "Relay SHOULD return 404 for repositories it doesn't host",
249 )
250 .pass()
251 }
112} 252}
113 253
114/// Helper function to check if a repository is accessible via Smart HTTP service 254/// Helper function to check if a repository is accessible via Smart HTTP service
@@ -156,3 +296,56 @@ async fn check_repo_accessible_via_http(
156 296
157 Ok(()) 297 Ok(())
158} 298}
299
300/// Helper function to check if a webpage is served for an existing repository
301///
302/// Verifies that accessing the repository URL returns a webpage (2xx status)
303/// URL format: http://domain/npub/identifier.git
304async fn check_webpage_served(relay_domain: &str, npub: &str, repo_id: &str) -> Result<(), String> {
305 let repo_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
306
307 let http_client = reqwest::Client::new();
308 let response = http_client
309 .get(&repo_url)
310 .send()
311 .await
312 .map_err(|e| format!("HTTP request failed: {}", e))?;
313
314 if !response.status().is_success() {
315 return Err(format!(
316 "Expected 2xx status for existing repo webpage, got {} for URL: {}",
317 response.status(),
318 repo_url
319 ));
320 }
321
322 Ok(())
323}
324
325/// Helper function to check that 404 is returned for non-existent repository
326///
327/// Verifies that accessing a non-existent repository URL returns 404
328async fn check_404_for_nonexistent_repo(
329 relay_domain: &str,
330 npub: &str,
331 repo_id: &str,
332) -> Result<(), String> {
333 let repo_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
334
335 let http_client = reqwest::Client::new();
336 let response = http_client
337 .get(&repo_url)
338 .send()
339 .await
340 .map_err(|e| format!("HTTP request failed: {}", e))?;
341
342 if response.status().as_u16() != 404 {
343 return Err(format!(
344 "Expected 404 status for non-existent repo, got {} for URL: {}",
345 response.status(),
346 repo_url
347 ));
348 }
349
350 Ok(())
351}
diff --git a/src/http/landing.rs b/src/http/landing.rs
index 55ffb26..ddde09a 100644
--- a/src/http/landing.rs
+++ b/src/http/landing.rs
@@ -13,3 +13,410 @@ pub fn get_html(config: &Config) -> String {
13 bind_address = config.bind_address, 13 bind_address = config.bind_address,
14 ) 14 )
15} 15}
16
17/// Generate a generic 404 page for unknown paths
18///
19/// Used for any path that doesn't match a known route
20pub fn get_generic_404_html(config: &Config, path: &str) -> String {
21 format!(
22 r#"<!DOCTYPE html>
23<html lang="en">
24<head>
25 <meta charset="UTF-8">
26 <meta name="viewport" content="width=device-width, initial-scale=1.0">
27 <title>Not Found - {relay_name}</title>
28 <style>
29 * {{
30 margin: 0;
31 padding: 0;
32 box-sizing: border-box;
33 }}
34 body {{
35 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
36 line-height: 1.6;
37 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
38 color: #333;
39 min-height: 100vh;
40 display: flex;
41 align-items: center;
42 justify-content: center;
43 padding: 20px;
44 }}
45 .container {{
46 max-width: 600px;
47 background: white;
48 padding: 40px;
49 border-radius: 12px;
50 box-shadow: 0 20px 60px rgba(0,0,0,0.3);
51 text-align: center;
52 }}
53 h1 {{
54 color: #e74c3c;
55 margin-bottom: 10px;
56 font-size: 4em;
57 }}
58 h2 {{
59 color: #333;
60 margin-bottom: 20px;
61 font-size: 1.5em;
62 }}
63 .path-info {{
64 background: #f9f9f9;
65 padding: 15px;
66 border-radius: 8px;
67 margin: 20px 0;
68 border-left: 4px solid #e74c3c;
69 }}
70 code {{
71 background: #f4f4f4;
72 padding: 3px 8px;
73 border-radius: 4px;
74 font-family: 'Courier New', monospace;
75 color: #667eea;
76 font-size: 0.85em;
77 word-break: break-all;
78 }}
79 .back-link {{
80 margin-top: 20px;
81 }}
82 a {{
83 color: #667eea;
84 text-decoration: none;
85 }}
86 a:hover {{
87 text-decoration: underline;
88 }}
89 .footer {{
90 margin-top: 30px;
91 padding-top: 20px;
92 border-top: 1px solid #eee;
93 color: #999;
94 font-size: 0.9em;
95 }}
96 </style>
97</head>
98<body>
99 <div class="container">
100 <h1>404</h1>
101 <h2>Not Found</h2>
102 <p>The page you're looking for doesn't exist.</p>
103
104 <div class="path-info">
105 <p><strong>Requested path:</strong> <code>{path}</code></p>
106 </div>
107
108 <div class="back-link">
109 <a href="/">← Back to {relay_name}</a>
110 </div>
111
112 <div class="footer">
113 <p>Powered by <strong>ngit-grasp</strong></p>
114 </div>
115 </div>
116</body>
117</html>"#,
118 relay_name = config.relay_name,
119 path = path,
120 )
121}
122
123/// Generate a 404 page for a non-existent repository
124///
125/// GRASP-01: "...and a 404 page for repositories it doesn't host"
126pub fn get_404_html(config: &Config, npub: &str, identifier: &str) -> String {
127 format!(
128 r#"<!DOCTYPE html>
129<html lang="en">
130<head>
131 <meta charset="UTF-8">
132 <meta name="viewport" content="width=device-width, initial-scale=1.0">
133 <title>Repository Not Found - {relay_name}</title>
134 <style>
135 * {{
136 margin: 0;
137 padding: 0;
138 box-sizing: border-box;
139 }}
140 body {{
141 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
142 line-height: 1.6;
143 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
144 color: #333;
145 min-height: 100vh;
146 display: flex;
147 align-items: center;
148 justify-content: center;
149 padding: 20px;
150 }}
151 .container {{
152 max-width: 600px;
153 background: white;
154 padding: 40px;
155 border-radius: 12px;
156 box-shadow: 0 20px 60px rgba(0,0,0,0.3);
157 text-align: center;
158 }}
159 h1 {{
160 color: #e74c3c;
161 margin-bottom: 10px;
162 font-size: 4em;
163 }}
164 h2 {{
165 color: #333;
166 margin-bottom: 20px;
167 font-size: 1.5em;
168 }}
169 .repo-info {{
170 background: #f9f9f9;
171 padding: 15px;
172 border-radius: 8px;
173 margin: 20px 0;
174 border-left: 4px solid #e74c3c;
175 }}
176 code {{
177 background: #f4f4f4;
178 padding: 3px 8px;
179 border-radius: 4px;
180 font-family: 'Courier New', monospace;
181 color: #667eea;
182 font-size: 0.85em;
183 word-break: break-all;
184 }}
185 .back-link {{
186 margin-top: 20px;
187 }}
188 a {{
189 color: #667eea;
190 text-decoration: none;
191 }}
192 a:hover {{
193 text-decoration: underline;
194 }}
195 .footer {{
196 margin-top: 30px;
197 padding-top: 20px;
198 border-top: 1px solid #eee;
199 color: #999;
200 font-size: 0.9em;
201 }}
202 </style>
203</head>
204<body>
205 <div class="container">
206 <h1>404</h1>
207 <h2>Repository Not Found</h2>
208 <p>The repository you're looking for doesn't exist on this GRASP server.</p>
209
210 <div class="repo-info">
211 <p><strong>Owner:</strong> <code>{npub}</code></p>
212 <p><strong>Repository:</strong> <code>{identifier}</code></p>
213 </div>
214
215 <p>This repository may not have been announced to this server, or the URL may be incorrect.</p>
216
217 <div class="back-link">
218 <a href="/">← Back to {relay_name}</a>
219 </div>
220
221 <div class="footer">
222 <p>Powered by <strong>ngit-grasp</strong></p>
223 </div>
224 </div>
225</body>
226</html>"#,
227 relay_name = config.relay_name,
228 npub = npub,
229 identifier = identifier,
230 )
231}
232
233/// Generate a webpage for an existing repository
234///
235/// GRASP-01: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s)
236/// to browse the repository"
237pub fn get_repo_html(config: &Config, npub: &str, identifier: &str) -> String {
238 let clone_url = format!(
239 "http://{}/{}/{}.git",
240 config.domain, npub, identifier
241 );
242
243 format!(
244 r#"<!DOCTYPE html>
245<html lang="en">
246<head>
247 <meta charset="UTF-8">
248 <meta name="viewport" content="width=device-width, initial-scale=1.0">
249 <title>{identifier} - {relay_name}</title>
250 <style>
251 * {{
252 margin: 0;
253 padding: 0;
254 box-sizing: border-box;
255 }}
256 body {{
257 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
258 line-height: 1.6;
259 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
260 color: #333;
261 min-height: 100vh;
262 display: flex;
263 align-items: center;
264 justify-content: center;
265 padding: 20px;
266 }}
267 .container {{
268 max-width: 800px;
269 background: white;
270 padding: 40px;
271 border-radius: 12px;
272 box-shadow: 0 20px 60px rgba(0,0,0,0.3);
273 }}
274 h1 {{
275 color: #667eea;
276 margin-bottom: 10px;
277 font-size: 2em;
278 }}
279 h2 {{
280 color: #764ba2;
281 margin-top: 25px;
282 margin-bottom: 15px;
283 font-size: 1.3em;
284 border-bottom: 2px solid #667eea;
285 padding-bottom: 8px;
286 }}
287 .subtitle {{
288 color: #666;
289 margin-bottom: 25px;
290 font-size: 1em;
291 }}
292 .repo-info {{
293 background: #f9f9f9;
294 padding: 15px;
295 border-radius: 8px;
296 margin: 15px 0;
297 border-left: 4px solid #667eea;
298 }}
299 code {{
300 background: #f4f4f4;
301 padding: 3px 8px;
302 border-radius: 4px;
303 font-family: 'Courier New', monospace;
304 color: #667eea;
305 font-size: 0.85em;
306 word-break: break-all;
307 }}
308 .clone-box {{
309 background: #2d3748;
310 color: #e2e8f0;
311 padding: 15px;
312 border-radius: 8px;
313 margin: 15px 0;
314 font-family: 'Courier New', monospace;
315 font-size: 0.9em;
316 overflow-x: auto;
317 }}
318 .clone-box code {{
319 background: transparent;
320 color: #68d391;
321 padding: 0;
322 }}
323 ul {{
324 margin: 15px 0;
325 padding-left: 25px;
326 }}
327 li {{
328 margin: 10px 0;
329 }}
330 a {{
331 color: #667eea;
332 text-decoration: none;
333 }}
334 a:hover {{
335 text-decoration: underline;
336 }}
337 .client-list {{
338 display: grid;
339 gap: 10px;
340 margin: 15px 0;
341 }}
342 .client-item {{
343 background: #f9f9f9;
344 padding: 12px 15px;
345 border-radius: 8px;
346 display: flex;
347 justify-content: space-between;
348 align-items: center;
349 }}
350 .badge {{
351 display: inline-block;
352 background: #667eea;
353 color: white;
354 padding: 4px 10px;
355 border-radius: 12px;
356 font-size: 0.8em;
357 }}
358 .footer {{
359 margin-top: 30px;
360 padding-top: 20px;
361 border-top: 1px solid #eee;
362 text-align: center;
363 color: #999;
364 font-size: 0.9em;
365 }}
366 .back-link {{
367 margin-bottom: 20px;
368 }}
369 </style>
370</head>
371<body>
372 <div class="container">
373 <div class="back-link">
374 <a href="/">← Back to {relay_name}</a>
375 </div>
376
377 <h1>📦 {identifier}</h1>
378 <p class="subtitle">Git repository hosted on {relay_name}</p>
379
380 <h2>📋 Repository Information</h2>
381 <div class="repo-info">
382 <p><strong>Owner:</strong> <code>{npub}</code></p>
383 <p><strong>Repository:</strong> <code>{identifier}</code></p>
384 </div>
385
386 <h2>🔗 Clone this Repository</h2>
387 <div class="clone-box">
388 git clone <code>{clone_url}</code>
389 </div>
390
391 <h2>🌐 Browse with Git Nostr Clients</h2>
392 <p>You can browse this repository using these Git Nostr clients:</p>
393 <div class="client-list">
394 <div class="client-item">
395 <span><strong>gitworkshop.dev</strong> - Web-based repository browser</span>
396 <a href="https://gitworkshop.dev" target="_blank">Visit →</a>
397 </div>
398 <div class="client-item">
399 <span><strong>ngit</strong> - Command-line Git + Nostr tool</span>
400 <a href="https://github.com/DanConwayDev/ngit-cli" target="_blank">GitHub →</a>
401 </div>
402 </div>
403
404 <h2>📚 About GRASP</h2>
405 <p>This repository is hosted using the <strong>GRASP</strong> (Git Relays Authorized via Signed-Nostr Proofs) protocol.</p>
406 <ul>
407 <li><a href="https://gitworkshop.dev/repo/grasp/01.md" target="_blank">GRASP-01 Specification</a></li>
408 <li><a href="https://github.com/nostr-protocol/nips/blob/master/34.md" target="_blank">NIP-34: Git Stuff</a></li>
409 </ul>
410
411 <div class="footer">
412 <p>Powered by <strong>ngit-grasp</strong></p>
413 </div>
414 </div>
415</body>
416</html>"#,
417 relay_name = config.relay_name,
418 npub = npub,
419 identifier = identifier,
420 clone_url = clone_url,
421 )
422}
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}
diff --git a/tests/repository_creation.rs b/tests/repository_creation.rs
index 301203b..352e2cc 100644
--- a/tests/repository_creation.rs
+++ b/tests/repository_creation.rs
@@ -59,3 +59,5 @@ macro_rules! isolated_test {
59 59
60// Generate isolated tests for all repository creation tests 60// Generate isolated tests for all repository creation tests
61isolated_test!(test_bare_repo_created_on_announcement); 61isolated_test!(test_bare_repo_created_on_announcement);
62isolated_test!(test_webpage_served_for_existing_repo);
63isolated_test!(test_404_for_nonexistent_repo);