mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
add repo land page and 404 page per GRASP-01
This commit is contained in:
@@ -27,6 +27,8 @@ impl RepositoryCreationTests {
|
||||
let mut results = crate::AuditResult::new("GRASP-01 Repository Creation Tests");
|
||||
|
||||
results.add(Self::test_bare_repo_created_on_announcement(client, relay_domain).await);
|
||||
results.add(Self::test_webpage_served_for_existing_repo(client, relay_domain).await);
|
||||
results.add(Self::test_404_for_nonexistent_repo(client, relay_domain).await);
|
||||
|
||||
results
|
||||
}
|
||||
@@ -109,6 +111,144 @@ impl RepositoryCreationTests {
|
||||
)
|
||||
.pass()
|
||||
}
|
||||
|
||||
/// Test that a webpage is served for an existing repository
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. Creates a valid repository announcement
|
||||
/// 2. Accesses the repository URL without git service parameters
|
||||
/// 3. Verifies a webpage is returned (any 2xx status with HTML content)
|
||||
///
|
||||
/// GRASP-01: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s)"
|
||||
pub async fn test_webpage_served_for_existing_repo(
|
||||
client: &AuditClient,
|
||||
relay_domain: &str,
|
||||
) -> TestResult {
|
||||
let test_name = "test_webpage_served_for_existing_repo";
|
||||
let ctx = TestContext::new(client);
|
||||
|
||||
// Create a repository announcement
|
||||
let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Relay SHOULD serve a webpage for existing repositories",
|
||||
)
|
||||
.fail(format!("Failed to create repo fixture: {}", e))
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for repository creation
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
|
||||
// Extract repo identifier and npub
|
||||
let repo_id = match repo
|
||||
.tags
|
||||
.iter()
|
||||
.find(|t| t.kind() == TagKind::d())
|
||||
.and_then(|t| t.content())
|
||||
{
|
||||
Some(id) => id.to_string(),
|
||||
None => {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Relay SHOULD serve a webpage for existing repositories",
|
||||
)
|
||||
.fail("Repository announcement missing d tag")
|
||||
}
|
||||
};
|
||||
|
||||
let npub = match repo.pubkey.to_bech32() {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Relay SHOULD serve a webpage for existing repositories",
|
||||
)
|
||||
.fail(format!("Failed to convert pubkey to npub: {}", e))
|
||||
}
|
||||
};
|
||||
|
||||
// Check that a webpage is served at the repository URL
|
||||
if let Err(e) = check_webpage_served(relay_domain, &npub, &repo_id).await {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Relay SHOULD serve a webpage for existing repositories",
|
||||
)
|
||||
.fail(format!("Webpage not served: {}", e));
|
||||
}
|
||||
|
||||
TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Relay SHOULD serve a webpage for existing repositories",
|
||||
)
|
||||
.pass()
|
||||
}
|
||||
|
||||
/// Test that 404 is returned for non-existent repositories
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. Accesses a URL for a repository that doesn't exist
|
||||
/// 2. Verifies a 404 status is returned
|
||||
///
|
||||
/// GRASP-01: "...and a 404 page for repositories it doesn't host"
|
||||
pub async fn test_404_for_nonexistent_repo(
|
||||
client: &AuditClient,
|
||||
relay_domain: &str,
|
||||
) -> TestResult {
|
||||
let test_name = "test_404_for_nonexistent_repo";
|
||||
|
||||
let ctx = TestContext::new(client);
|
||||
|
||||
let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Relay SHOULD serve a webpage for existing repositories",
|
||||
)
|
||||
.fail(format!("Failed to create repo fixture: {}", e))
|
||||
}
|
||||
};
|
||||
|
||||
let npub = match repo.pubkey.to_bech32() {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Relay SHOULD serve a webpage for existing repositories",
|
||||
)
|
||||
.fail(format!("Failed to convert pubkey to npub: {}", e))
|
||||
}
|
||||
};
|
||||
// Use a clearly non-existent repo id but real npub
|
||||
let fake_repo_id = "nonexistent-repo-12345";
|
||||
|
||||
// Check that 404 is returned
|
||||
if let Err(e) = check_404_for_nonexistent_repo(relay_domain, &npub, fake_repo_id).await {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Relay SHOULD return 404 for repositories it doesn't host",
|
||||
)
|
||||
.fail(format!("Expected 404, got: {}", e));
|
||||
}
|
||||
|
||||
TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Relay SHOULD return 404 for repositories it doesn't host",
|
||||
)
|
||||
.pass()
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to check if a repository is accessible via Smart HTTP service
|
||||
@@ -156,3 +296,56 @@ async fn check_repo_accessible_via_http(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to check if a webpage is served for an existing repository
|
||||
///
|
||||
/// Verifies that accessing the repository URL returns a webpage (2xx status)
|
||||
/// URL format: http://domain/npub/identifier.git
|
||||
async fn check_webpage_served(relay_domain: &str, npub: &str, repo_id: &str) -> Result<(), String> {
|
||||
let repo_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
|
||||
|
||||
let http_client = reqwest::Client::new();
|
||||
let response = http_client
|
||||
.get(&repo_url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("HTTP request failed: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!(
|
||||
"Expected 2xx status for existing repo webpage, got {} for URL: {}",
|
||||
response.status(),
|
||||
repo_url
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to check that 404 is returned for non-existent repository
|
||||
///
|
||||
/// Verifies that accessing a non-existent repository URL returns 404
|
||||
async fn check_404_for_nonexistent_repo(
|
||||
relay_domain: &str,
|
||||
npub: &str,
|
||||
repo_id: &str,
|
||||
) -> Result<(), String> {
|
||||
let repo_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
|
||||
|
||||
let http_client = reqwest::Client::new();
|
||||
let response = http_client
|
||||
.get(&repo_url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("HTTP request failed: {}", e))?;
|
||||
|
||||
if response.status().as_u16() != 404 {
|
||||
return Err(format!(
|
||||
"Expected 404 status for non-existent repo, got {} for URL: {}",
|
||||
response.status(),
|
||||
repo_url
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13,3 +13,410 @@ pub fn get_html(config: &Config) -> String {
|
||||
bind_address = config.bind_address,
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate a generic 404 page for unknown paths
|
||||
///
|
||||
/// Used for any path that doesn't match a known route
|
||||
pub fn get_generic_404_html(config: &Config, path: &str) -> String {
|
||||
format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Not Found - {relay_name}</title>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||
line-height: 1.6;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #333;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 600px;
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
text-align: center;
|
||||
}}
|
||||
h1 {{
|
||||
color: #e74c3c;
|
||||
margin-bottom: 10px;
|
||||
font-size: 4em;
|
||||
}}
|
||||
h2 {{
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5em;
|
||||
}}
|
||||
.path-info {{
|
||||
background: #f9f9f9;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #e74c3c;
|
||||
}}
|
||||
code {{
|
||||
background: #f4f4f4;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #667eea;
|
||||
font-size: 0.85em;
|
||||
word-break: break-all;
|
||||
}}
|
||||
.back-link {{
|
||||
margin-top: 20px;
|
||||
}}
|
||||
a {{
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}}
|
||||
a:hover {{
|
||||
text-decoration: underline;
|
||||
}}
|
||||
.footer {{
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
color: #999;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>404</h1>
|
||||
<h2>Not Found</h2>
|
||||
<p>The page you're looking for doesn't exist.</p>
|
||||
|
||||
<div class="path-info">
|
||||
<p><strong>Requested path:</strong> <code>{path}</code></p>
|
||||
</div>
|
||||
|
||||
<div class="back-link">
|
||||
<a href="/">← Back to {relay_name}</a>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Powered by <strong>ngit-grasp</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#,
|
||||
relay_name = config.relay_name,
|
||||
path = path,
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate a 404 page for a non-existent repository
|
||||
///
|
||||
/// GRASP-01: "...and a 404 page for repositories it doesn't host"
|
||||
pub fn get_404_html(config: &Config, npub: &str, identifier: &str) -> String {
|
||||
format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Repository Not Found - {relay_name}</title>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||
line-height: 1.6;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #333;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 600px;
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
text-align: center;
|
||||
}}
|
||||
h1 {{
|
||||
color: #e74c3c;
|
||||
margin-bottom: 10px;
|
||||
font-size: 4em;
|
||||
}}
|
||||
h2 {{
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5em;
|
||||
}}
|
||||
.repo-info {{
|
||||
background: #f9f9f9;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #e74c3c;
|
||||
}}
|
||||
code {{
|
||||
background: #f4f4f4;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #667eea;
|
||||
font-size: 0.85em;
|
||||
word-break: break-all;
|
||||
}}
|
||||
.back-link {{
|
||||
margin-top: 20px;
|
||||
}}
|
||||
a {{
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}}
|
||||
a:hover {{
|
||||
text-decoration: underline;
|
||||
}}
|
||||
.footer {{
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
color: #999;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>404</h1>
|
||||
<h2>Repository Not Found</h2>
|
||||
<p>The repository you're looking for doesn't exist on this GRASP server.</p>
|
||||
|
||||
<div class="repo-info">
|
||||
<p><strong>Owner:</strong> <code>{npub}</code></p>
|
||||
<p><strong>Repository:</strong> <code>{identifier}</code></p>
|
||||
</div>
|
||||
|
||||
<p>This repository may not have been announced to this server, or the URL may be incorrect.</p>
|
||||
|
||||
<div class="back-link">
|
||||
<a href="/">← Back to {relay_name}</a>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Powered by <strong>ngit-grasp</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#,
|
||||
relay_name = config.relay_name,
|
||||
npub = npub,
|
||||
identifier = identifier,
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate a webpage for an existing repository
|
||||
///
|
||||
/// GRASP-01: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s)
|
||||
/// to browse the repository"
|
||||
pub fn get_repo_html(config: &Config, npub: &str, identifier: &str) -> String {
|
||||
let clone_url = format!(
|
||||
"http://{}/{}/{}.git",
|
||||
config.domain, npub, identifier
|
||||
);
|
||||
|
||||
format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{identifier} - {relay_name}</title>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||
line-height: 1.6;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #333;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 800px;
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
}}
|
||||
h1 {{
|
||||
color: #667eea;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2em;
|
||||
}}
|
||||
h2 {{
|
||||
color: #764ba2;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.3em;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 8px;
|
||||
}}
|
||||
.subtitle {{
|
||||
color: #666;
|
||||
margin-bottom: 25px;
|
||||
font-size: 1em;
|
||||
}}
|
||||
.repo-info {{
|
||||
background: #f9f9f9;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 15px 0;
|
||||
border-left: 4px solid #667eea;
|
||||
}}
|
||||
code {{
|
||||
background: #f4f4f4;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #667eea;
|
||||
font-size: 0.85em;
|
||||
word-break: break-all;
|
||||
}}
|
||||
.clone-box {{
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 15px 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
overflow-x: auto;
|
||||
}}
|
||||
.clone-box code {{
|
||||
background: transparent;
|
||||
color: #68d391;
|
||||
padding: 0;
|
||||
}}
|
||||
ul {{
|
||||
margin: 15px 0;
|
||||
padding-left: 25px;
|
||||
}}
|
||||
li {{
|
||||
margin: 10px 0;
|
||||
}}
|
||||
a {{
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}}
|
||||
a:hover {{
|
||||
text-decoration: underline;
|
||||
}}
|
||||
.client-list {{
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
}}
|
||||
.client-item {{
|
||||
background: #f9f9f9;
|
||||
padding: 12px 15px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}}
|
||||
.badge {{
|
||||
display: inline-block;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
}}
|
||||
.footer {{
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
.back-link {{
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="back-link">
|
||||
<a href="/">← Back to {relay_name}</a>
|
||||
</div>
|
||||
|
||||
<h1>📦 {identifier}</h1>
|
||||
<p class="subtitle">Git repository hosted on {relay_name}</p>
|
||||
|
||||
<h2>📋 Repository Information</h2>
|
||||
<div class="repo-info">
|
||||
<p><strong>Owner:</strong> <code>{npub}</code></p>
|
||||
<p><strong>Repository:</strong> <code>{identifier}</code></p>
|
||||
</div>
|
||||
|
||||
<h2>🔗 Clone this Repository</h2>
|
||||
<div class="clone-box">
|
||||
git clone <code>{clone_url}</code>
|
||||
</div>
|
||||
|
||||
<h2>🌐 Browse with Git Nostr Clients</h2>
|
||||
<p>You can browse this repository using these Git Nostr clients:</p>
|
||||
<div class="client-list">
|
||||
<div class="client-item">
|
||||
<span><strong>gitworkshop.dev</strong> - Web-based repository browser</span>
|
||||
<a href="https://gitworkshop.dev" target="_blank">Visit →</a>
|
||||
</div>
|
||||
<div class="client-item">
|
||||
<span><strong>ngit</strong> - Command-line Git + Nostr tool</span>
|
||||
<a href="https://github.com/DanConwayDev/ngit-cli" target="_blank">GitHub →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>📚 About GRASP</h2>
|
||||
<p>This repository is hosted using the <strong>GRASP</strong> (Git Relays Authorized via Signed-Nostr Proofs) protocol.</p>
|
||||
<ul>
|
||||
<li><a href="https://gitworkshop.dev/repo/grasp/01.md" target="_blank">GRASP-01 Specification</a></li>
|
||||
<li><a href="https://github.com/nostr-protocol/nips/blob/master/34.md" target="_blank">NIP-34: Git Stuff</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="footer">
|
||||
<p>Powered by <strong>ngit-grasp</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#,
|
||||
relay_name = config.relay_name,
|
||||
npub = npub,
|
||||
identifier = identifier,
|
||||
clone_url = clone_url,
|
||||
)
|
||||
}
|
||||
|
||||
+99
-7
@@ -32,6 +32,47 @@ const CORS_ALLOW_ORIGIN: &str = "*";
|
||||
const CORS_ALLOW_METHODS: &str = "GET, POST";
|
||||
const CORS_ALLOW_HEADERS: &str = "Content-Type";
|
||||
|
||||
/// Extract npub and identifier from a repository URL path (no git subpath required)
|
||||
///
|
||||
/// Parses paths like `/<npub>/<identifier>.git` (for repository webpage/404)
|
||||
///
|
||||
/// Returns (npub, identifier) if the path matches a repository URL pattern
|
||||
fn parse_repo_url(path: &str) -> Option<(&str, &str)> {
|
||||
// Remove leading slash
|
||||
let path = path.strip_prefix('/').unwrap_or(path);
|
||||
|
||||
// Split into components
|
||||
let parts: Vec<&str> = path.split('/').collect();
|
||||
|
||||
// Must be exactly 2 parts: npub and repo.git (no subpath)
|
||||
if parts.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let npub = parts[0];
|
||||
let repo_part = parts[1];
|
||||
|
||||
// The repo part must end with .git
|
||||
if !repo_part.ends_with(".git") {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Must have an npub that looks valid (starts with npub1)
|
||||
if !npub.starts_with("npub1") {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Extract identifier (remove .git suffix)
|
||||
let identifier = repo_part.strip_suffix(".git").unwrap_or(repo_part);
|
||||
|
||||
// Identifier must not be empty
|
||||
if identifier.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((npub, identifier))
|
||||
}
|
||||
|
||||
/// Add CORS headers to a response builder
|
||||
fn add_cors_headers(builder: hyper::http::response::Builder) -> hyper::http::response::Builder {
|
||||
builder
|
||||
@@ -230,6 +271,45 @@ impl Service<Request<Incoming>> for HttpService {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for repository URL pattern (e.g., /npub/repo.git without subpath)
|
||||
// GRASP-01: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s)
|
||||
// to browse the repository and a 404 page for repositories it doesn't host"
|
||||
if let Some((npub, identifier)) = parse_repo_url(&path) {
|
||||
let npub = npub.to_string();
|
||||
let identifier = identifier.to_string();
|
||||
let config = self.config.clone();
|
||||
let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier);
|
||||
|
||||
tracing::debug!(
|
||||
"Repository URL request: {} (npub={}, id={}, path={:?})",
|
||||
path,
|
||||
npub,
|
||||
identifier,
|
||||
repo_path
|
||||
);
|
||||
|
||||
return Box::pin(async move {
|
||||
// Check if repository exists
|
||||
if repo_path.exists() {
|
||||
// Serve repository webpage
|
||||
let html = landing::get_repo_html(&config, &npub, &identifier);
|
||||
Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp"))
|
||||
.status(200)
|
||||
.header("content-type", "text/html; charset=utf-8")
|
||||
.body(Full::new(Bytes::from(html)))
|
||||
.unwrap())
|
||||
} else {
|
||||
// Serve 404 page for non-existent repository
|
||||
let html = landing::get_404_html(&config, &npub, &identifier);
|
||||
Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp"))
|
||||
.status(404)
|
||||
.header("content-type", "text/html; charset=utf-8")
|
||||
.body(Full::new(Bytes::from(html)))
|
||||
.unwrap())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if this is a WebSocket upgrade request
|
||||
if let (Some(c), Some(w)) = (
|
||||
req.headers().get("connection"),
|
||||
@@ -275,14 +355,26 @@ impl Service<Request<Incoming>> for HttpService {
|
||||
}
|
||||
}
|
||||
|
||||
// Serve landing page for HTTP requests
|
||||
let html = landing::get_html(&self.config);
|
||||
// Only serve landing page for root path "/", 404 for everything else
|
||||
let config = self.config.clone();
|
||||
Box::pin(async move {
|
||||
Ok(base
|
||||
.status(200)
|
||||
.header("content-type", "text/html; charset=utf-8")
|
||||
.body(Full::new(Bytes::from(html)))
|
||||
.unwrap())
|
||||
if path == "/" {
|
||||
// Serve landing page for root
|
||||
let html = landing::get_html(&config);
|
||||
Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp"))
|
||||
.status(200)
|
||||
.header("content-type", "text/html; charset=utf-8")
|
||||
.body(Full::new(Bytes::from(html)))
|
||||
.unwrap())
|
||||
} else {
|
||||
// Serve generic 404 for unknown paths
|
||||
let html = landing::get_generic_404_html(&config, &path);
|
||||
Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp"))
|
||||
.status(404)
|
||||
.header("content-type", "text/html; charset=utf-8")
|
||||
.body(Full::new(Bytes::from(html)))
|
||||
.unwrap())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,3 +59,5 @@ macro_rules! isolated_test {
|
||||
|
||||
// Generate isolated tests for all repository creation tests
|
||||
isolated_test!(test_bare_repo_created_on_announcement);
|
||||
isolated_test!(test_webpage_served_for_existing_repo);
|
||||
isolated_test!(test_404_for_nonexistent_repo);
|
||||
|
||||
Reference in New Issue
Block a user