upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-28 03:38:21 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-28 03:38:21 +0000
commit3f74ababf338d65ac5e29e7eb5541ce416b7fe75 (patch)
tree3e06696a1833db776a7d1908dd2812dcbe86f6a7 /grasp-audit
parentc15215d704117d1035806e3b5f71afc19f5516a8 (diff)
add git http advertisment allow-reachable-sha1-in-want and allow-tip-sha1-in-want
Diffstat (limited to 'grasp-audit')
-rw-r--r--grasp-audit/src/bin/grasp-audit.rs54
-rw-r--r--grasp-audit/src/specs/grasp01/git_clone.rs154
2 files changed, 188 insertions, 20 deletions
diff --git a/grasp-audit/src/bin/grasp-audit.rs b/grasp-audit/src/bin/grasp-audit.rs
index 760ed55..c0a9273 100644
--- a/grasp-audit/src/bin/grasp-audit.rs
+++ b/grasp-audit/src/bin/grasp-audit.rs
@@ -48,6 +48,22 @@ async fn main() -> Result<()> {
48 48
49 match cli.command { 49 match cli.command {
50 Commands::Audit { relay, mode, spec, git_data_dir } => { 50 Commands::Audit { relay, mode, spec, git_data_dir } => {
51 // Early validation: check if --git-data-dir is required for the spec
52 let specs_requiring_git_dir = ["all", "git-clone", "push-auth", "repo-creation"];
53 if specs_requiring_git_dir.contains(&spec.as_str()) && git_data_dir.is_none() {
54 return Err(anyhow!(
55 "The '{}' spec requires --git-data-dir to be specified.\n\
56 \n\
57 This directory should point to the relay's git data storage.\n\
58 Example: --git-data-dir /path/to/relay/repos\n\
59 \n\
60 If using Docker, mount a volume and pass that path:\n\
61 docker run -v /tmp/repos:/srv/ngit-relay/repos ...\n\
62 cargo run -- audit --relay ws://localhost:8080 --git-data-dir /tmp/repos",
63 spec
64 ));
65 }
66
51 let mut config = match mode.as_str() { 67 let mut config = match mode.as_str() {
52 "ci" => AuditConfig::ci(), 68 "ci" => AuditConfig::ci(),
53 "production" => AuditConfig::production(), 69 "production" => AuditConfig::production(),
@@ -87,7 +103,7 @@ async fn main() -> Result<()> {
87 103
88 println!("✓ Connected\n"); 104 println!("✓ Connected\n");
89 105
90 // Helper to check if git_data_dir is required 106 // Helper to check if git_data_dir is required for individual specs
91 let require_git_data_dir = |spec_name: &str| -> Result<PathBuf> { 107 let require_git_data_dir = |spec_name: &str| -> Result<PathBuf> {
92 git_data_dir.clone().ok_or_else(|| { 108 git_data_dir.clone().ok_or_else(|| {
93 anyhow!( 109 anyhow!(
@@ -130,6 +146,9 @@ async fn main() -> Result<()> {
130 specs::RepositoryCreationTests::run_all(&client, &dir).await 146 specs::RepositoryCreationTests::run_all(&client, &dir).await
131 } 147 }
132 "all" => { 148 "all" => {
149 // git_data_dir is guaranteed by early validation
150 let dir = git_data_dir.clone().expect("git_data_dir validated earlier");
151
133 println!("Running all tests...\n"); 152 println!("Running all tests...\n");
134 let mut all_results = AuditResult::new("All GRASP-01 Tests"); 153 let mut all_results = AuditResult::new("All GRASP-01 Tests");
135 154
@@ -153,25 +172,20 @@ async fn main() -> Result<()> {
153 let cors_results = specs::CorsTests::run_all(&client, &relay_domain).await; 172 let cors_results = specs::CorsTests::run_all(&client, &relay_domain).await;
154 all_results.merge(cors_results); 173 all_results.merge(cors_results);
155 174
156 // Tests that require git_data_dir 175 // Git clone tests
157 if let Some(ref dir) = git_data_dir { 176 println!(" → Git clone tests...");
158 // Git clone tests 177 let clone_results = specs::GitCloneTests::run_all(&client, &dir, &relay_domain).await;
159 println!(" → Git clone tests..."); 178 all_results.merge(clone_results);
160 let clone_results = specs::GitCloneTests::run_all(&client, dir, &relay_domain).await; 179
161 all_results.merge(clone_results); 180 // Push authorization tests
162 181 println!(" → Push authorization tests...");
163 // Push authorization tests 182 let push_results = specs::PushAuthorizationTests::run_all(&client, &dir, &relay_domain).await;
164 println!(" → Push authorization tests..."); 183 all_results.merge(push_results);
165 let push_results = specs::PushAuthorizationTests::run_all(&client, dir, &relay_domain).await; 184
166 all_results.merge(push_results); 185 // Repository creation tests
167 186 println!(" → Repository creation tests...");
168 // Repository creation tests 187 let repo_results = specs::RepositoryCreationTests::run_all(&client, &dir).await;
169 println!(" → Repository creation tests..."); 188 all_results.merge(repo_results);
170 let repo_results = specs::RepositoryCreationTests::run_all(&client, dir).await;
171 all_results.merge(repo_results);
172 } else {
173 println!(" ⚠ Skipping git-clone, push-auth, repo-creation tests (no --git-data-dir)");
174 }
175 189
176 println!(); 190 println!();
177 all_results 191 all_results
diff --git a/grasp-audit/src/specs/grasp01/git_clone.rs b/grasp-audit/src/specs/grasp01/git_clone.rs
index 8c91c04..4666a40 100644
--- a/grasp-audit/src/specs/grasp01/git_clone.rs
+++ b/grasp-audit/src/specs/grasp01/git_clone.rs
@@ -7,6 +7,7 @@
7//! - Basic clone operation via HTTP 7//! - Basic clone operation via HTTP
8//! - Cloned repository structure validation 8//! - Cloned repository structure validation
9//! - Clone URL format verification 9//! - Clone URL format verification
10//! - SHA1 capability advertisement verification
10//! 11//!
11//! ## Running Tests 12//! ## Running Tests
12//! 13//!
@@ -34,6 +35,7 @@ impl GitCloneTests {
34 35
35 results.add(Self::test_basic_git_clone(client, git_data_dir, relay_domain).await); 36 results.add(Self::test_basic_git_clone(client, git_data_dir, relay_domain).await);
36 results.add(Self::test_clone_url_format(client, git_data_dir, relay_domain).await); 37 results.add(Self::test_clone_url_format(client, git_data_dir, relay_domain).await);
38 results.add(Self::test_sha1_capabilities_advertised(client, git_data_dir, relay_domain).await);
37 39
38 results 40 results
39 } 41 }
@@ -277,6 +279,158 @@ impl GitCloneTests {
277 ) 279 )
278 .pass() 280 .pass()
279 } 281 }
282
283 /// Test that SHA1 capabilities are advertised in git-upload-pack
284 ///
285 /// GRASP-01 requires:
286 /// "MUST include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want`
287 /// in advertisement and serve available oids."
288 ///
289 /// This test verifies:
290 /// 1. The info/refs endpoint returns the capabilities
291 /// 2. Both allow-reachable-sha1-in-want and allow-tip-sha1-in-want are present
292 pub async fn test_sha1_capabilities_advertised(
293 client: &AuditClient,
294 git_data_dir: &Path,
295 relay_domain: &str,
296 ) -> TestResult {
297 let test_name = "test_sha1_capabilities_advertised";
298 let ctx = TestContext::new(client);
299
300 // Create repository announcement
301 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
302 Ok(r) => r,
303 Err(e) => {
304 return TestResult::new(
305 test_name,
306 "GRASP-01",
307 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
308 )
309 .fail(&format!("Failed to create repo fixture: {}", e))
310 }
311 };
312
313 // Wait for repository creation
314 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
315
316 // Extract repo identifier and npub
317 let repo_id = match repo
318 .tags
319 .iter()
320 .find(|t| t.kind() == TagKind::d())
321 .and_then(|t| t.content())
322 {
323 Some(id) => id.to_string(),
324 None => {
325 return TestResult::new(
326 test_name,
327 "GRASP-01",
328 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
329 )
330 .fail("Repository announcement missing d tag")
331 }
332 };
333
334 let npub = match repo.pubkey.to_bech32() {
335 Ok(n) => n,
336 Err(e) => {
337 return TestResult::new(
338 test_name,
339 "GRASP-01",
340 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
341 )
342 .fail(&format!("Failed to convert pubkey to npub: {}", e))
343 }
344 };
345
346 // Verify repository exists
347 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
348 if !repo_path.exists() {
349 return TestResult::new(
350 test_name,
351 "GRASP-01",
352 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
353 )
354 .fail(&format!(
355 "Repository not found at: {}",
356 repo_path.display()
357 ));
358 }
359
360 // Build info/refs URL for git-upload-pack service
361 let info_refs_url = format!(
362 "http://{}/{}/{}.git/info/refs?service=git-upload-pack",
363 relay_domain, npub, repo_id
364 );
365
366 // Make HTTP request to get the advertisement
367 let http_client = reqwest::Client::new();
368 let response = match http_client.get(&info_refs_url).send().await {
369 Ok(r) => r,
370 Err(e) => {
371 return TestResult::new(
372 test_name,
373 "GRASP-01",
374 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
375 )
376 .fail(&format!("HTTP request failed: {}", e))
377 }
378 };
379
380 if !response.status().is_success() {
381 return TestResult::new(
382 test_name,
383 "GRASP-01",
384 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
385 )
386 .fail(&format!(
387 "info/refs request failed with status: {}",
388 response.status()
389 ));
390 }
391
392 // Get response body
393 let body = match response.text().await {
394 Ok(b) => b,
395 Err(e) => {
396 return TestResult::new(
397 test_name,
398 "GRASP-01",
399 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
400 )
401 .fail(&format!("Failed to read response body: {}", e))
402 }
403 };
404
405 // Check for required capabilities
406 let has_allow_reachable = body.contains("allow-reachable-sha1-in-want");
407 let has_allow_tip = body.contains("allow-tip-sha1-in-want");
408
409 if !has_allow_reachable {
410 return TestResult::new(
411 test_name,
412 "GRASP-01",
413 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
414 )
415 .fail("Missing capability: allow-reachable-sha1-in-want");
416 }
417
418 if !has_allow_tip {
419 return TestResult::new(
420 test_name,
421 "GRASP-01",
422 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
423 )
424 .fail("Missing capability: allow-tip-sha1-in-want");
425 }
426
427 TestResult::new(
428 test_name,
429 "GRASP-01",
430 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
431 )
432 .pass()
433 }
280} 434}
281 435
282#[cfg(test)] 436#[cfg(test)]