upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/git/handlers.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 11:56:49 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 11:58:34 +0000
commit7a78815e29b01c83f3d0ec195ba717a2eba8cd37 (patch)
tree4c5ccd9b812f1d1d75ed218501192ddc5459fd12 /src/git/handlers.rs
parente6ceab90de1acad154624022a6036efac18abab6 (diff)
reject push when refs/nostr/<event-id> doesnt match known event and delete incorrect ref on event receive
Diffstat (limited to 'src/git/handlers.rs')
-rw-r--r--src/git/handlers.rs211
1 files changed, 118 insertions, 93 deletions
diff --git a/src/git/handlers.rs b/src/git/handlers.rs
index 23d4b5b..00f2449 100644
--- a/src/git/handlers.rs
+++ b/src/git/handlers.rs
@@ -2,17 +2,18 @@
2//! 2//!
3//! This module implements the HTTP handlers for Git Smart HTTP protocol. 3//! This module implements the HTTP handlers for Git Smart HTTP protocol.
4 4
5use std::path::PathBuf;
6use std::sync::Arc;
7use hyper::{body::Bytes, Response, StatusCode};
8use http_body_util::Full; 5use http_body_util::Full;
6use hyper::{body::Bytes, Response, StatusCode};
9use nostr_relay_builder::prelude::MemoryDatabase; 7use nostr_relay_builder::prelude::MemoryDatabase;
10use nostr_sdk::EventId; 8use nostr_sdk::EventId;
9use std::path::PathBuf;
10use std::sync::Arc;
11use tokio::io::{AsyncReadExt, AsyncWriteExt}; 11use tokio::io::{AsyncReadExt, AsyncWriteExt};
12use tracing::{debug, error, info, warn}; 12use tracing::{debug, error, info, warn};
13 13
14use super::authorization::{ 14use super::authorization::{
15 get_authorization_for_owner, parse_pushed_refs, validate_push_refs, AuthorizationResult, 15 get_authorization_for_owner, parse_pushed_refs, validate_nostr_ref_pushes, validate_push_refs,
16 AuthorizationResult,
16}; 17};
17use super::protocol::{GitService, PktLine}; 18use super::protocol::{GitService, PktLine};
18use super::subprocess::GitSubprocess; 19use super::subprocess::GitSubprocess;
@@ -27,7 +28,10 @@ pub async fn handle_info_refs(
27 repo_path: PathBuf, 28 repo_path: PathBuf,
28 service: GitService, 29 service: GitService,
29) -> Result<Response<Full<Bytes>>, GitError> { 30) -> Result<Response<Full<Bytes>>, GitError> {
30 debug!("Handling info/refs for {:?} with service {:?}", repo_path, service); 31 debug!(
32 "Handling info/refs for {:?} with service {:?}",
33 repo_path, service
34 );
31 35
32 // Check if repository exists 36 // Check if repository exists
33 if !repo_path.exists() { 37 if !repo_path.exists() {
@@ -36,55 +40,54 @@ pub async fn handle_info_refs(
36 } 40 }
37 41
38 // Spawn git with --advertise-refs 42 // Spawn git with --advertise-refs
39 let mut git = GitSubprocess::spawn(service, &repo_path, true) 43 let mut git = GitSubprocess::spawn(service, &repo_path, true).map_err(|e| {
40 .map_err(|e| { 44 error!("Failed to spawn git process: {}", e);
41 error!("Failed to spawn git process: {}", e); 45 GitError::ProcessSpawnFailed(e)
42 GitError::ProcessSpawnFailed(e) 46 })?;
43 })?;
44 47
45 // Read the output from git 48 // Read the output from git
46 let mut output = Vec::new(); 49 let mut output = Vec::new();
47 let mut stderr_output = Vec::new(); 50 let mut stderr_output = Vec::new();
48 51
49 if let Some(stdout) = git.take_stdout() { 52 if let Some(stdout) = git.take_stdout() {
50 let mut stdout = stdout; 53 let mut stdout = stdout;
51 stdout.read_to_end(&mut output).await 54 stdout.read_to_end(&mut output).await.map_err(|e| {
52 .map_err(|e| { 55 error!("Failed to read git output: {}", e);
53 error!("Failed to read git output: {}", e); 56 GitError::IoError(e)
54 GitError::IoError(e) 57 })?;
55 })?;
56 } 58 }
57 59
58 if let Some(stderr) = git.take_stderr() { 60 if let Some(stderr) = git.take_stderr() {
59 let mut stderr = stderr; 61 let mut stderr = stderr;
60 stderr.read_to_end(&mut stderr_output).await 62 stderr.read_to_end(&mut stderr_output).await.map_err(|e| {
61 .map_err(|e| { 63 error!("Failed to read git stderr: {}", e);
62 error!("Failed to read git stderr: {}", e); 64 GitError::IoError(e)
63 GitError::IoError(e) 65 })?;
64 })?;
65 } 66 }
66 67
67 // Wait for process to complete 68 // Wait for process to complete
68 let status = git.wait().await 69 let status = git.wait().await.map_err(|e| {
69 .map_err(|e| { 70 error!("Failed to wait for git process: {}", e);
70 error!("Failed to wait for git process: {}", e); 71 GitError::IoError(e)
71 GitError::IoError(e) 72 })?;
72 })?;
73 73
74 if !status.success() { 74 if !status.success() {
75 let stderr_str = String::from_utf8_lossy(&stderr_output); 75 let stderr_str = String::from_utf8_lossy(&stderr_output);
76 error!("Git process failed with status: {:?}, stderr: {}", status, stderr_str); 76 error!(
77 "Git process failed with status: {:?}, stderr: {}",
78 status, stderr_str
79 );
77 return Err(GitError::GitFailed(status.code())); 80 return Err(GitError::GitFailed(status.code()));
78 } 81 }
79 82
80 // Build response with pkt-line header 83 // Build response with pkt-line header
81 let mut response_body = Vec::new(); 84 let mut response_body = Vec::new();
82 85
83 // First line: service advertisement 86 // First line: service advertisement
84 let service_line = format!("# service={}\n", service.as_str()); 87 let service_line = format!("# service={}\n", service.as_str());
85 response_body.extend_from_slice(&PktLine::data(service_line.as_bytes()).encode()); 88 response_body.extend_from_slice(&PktLine::data(service_line.as_bytes()).encode());
86 response_body.extend_from_slice(&PktLine::flush().encode()); 89 response_body.extend_from_slice(&PktLine::flush().encode());
87 90
88 // Then the git output 91 // Then the git output
89 response_body.extend_from_slice(&output); 92 response_body.extend_from_slice(&output);
90 93
@@ -113,7 +116,9 @@ pub async fn handle_upload_pack(
113 116
114 // Write request to git's stdin 117 // Write request to git's stdin
115 if let Some(mut stdin) = git.take_stdin() { 118 if let Some(mut stdin) = git.take_stdin() {
116 stdin.write_all(&request_body).await 119 stdin
120 .write_all(&request_body)
121 .await
117 .map_err(GitError::IoError)?; 122 .map_err(GitError::IoError)?;
118 // Close stdin to signal end of input 123 // Close stdin to signal end of input
119 drop(stdin); 124 drop(stdin);
@@ -122,22 +127,25 @@ pub async fn handle_upload_pack(
122 // Read response from git's stdout 127 // Read response from git's stdout
123 let mut output = Vec::new(); 128 let mut output = Vec::new();
124 let mut stderr_output = Vec::new(); 129 let mut stderr_output = Vec::new();
125 130
126 if let Some(stdout) = git.take_stdout() { 131 if let Some(stdout) = git.take_stdout() {
127 let mut stdout = stdout; 132 let mut stdout = stdout;
128 stdout.read_to_end(&mut output).await 133 stdout
134 .read_to_end(&mut output)
135 .await
129 .map_err(GitError::IoError)?; 136 .map_err(GitError::IoError)?;
130 } 137 }
131 138
132 if let Some(stderr) = git.take_stderr() { 139 if let Some(stderr) = git.take_stderr() {
133 let mut stderr = stderr; 140 let mut stderr = stderr;
134 stderr.read_to_end(&mut stderr_output).await 141 stderr
142 .read_to_end(&mut stderr_output)
143 .await
135 .map_err(GitError::IoError)?; 144 .map_err(GitError::IoError)?;
136 } 145 }
137 146
138 // Wait for process 147 // Wait for process
139 let status = git.wait().await 148 let status = git.wait().await.map_err(GitError::IoError)?;
140 .map_err(GitError::IoError)?;
141 149
142 if !status.success() { 150 if !status.success() {
143 let stderr_str = String::from_utf8_lossy(&stderr_output); 151 let stderr_str = String::from_utf8_lossy(&stderr_output);
@@ -194,10 +202,7 @@ pub async fn handle_receive_pack(
194 match authorize_push(db, identifier, owner_pubkey, &request_body).await { 202 match authorize_push(db, identifier, owner_pubkey, &request_body).await {
195 Ok(auth_result) => { 203 Ok(auth_result) => {
196 if !auth_result.authorized { 204 if !auth_result.authorized {
197 warn!( 205 warn!("Push rejected for {}: {}", identifier, auth_result.reason);
198 "Push rejected for {}: {}",
199 identifier, auth_result.reason
200 );
201 return Err(GitError::Unauthorized); 206 return Err(GitError::Unauthorized);
202 } 207 }
203 info!( 208 info!(
@@ -209,10 +214,7 @@ pub async fn handle_receive_pack(
209 authorized_state = auth_result.state; 214 authorized_state = auth_result.state;
210 } 215 }
211 Err(e) => { 216 Err(e) => {
212 warn!( 217 warn!("Authorization check failed for {}: {}", identifier, e);
213 "Authorization check failed for {}: {}",
214 identifier, e
215 );
216 return Err(GitError::Unauthorized); 218 return Err(GitError::Unauthorized);
217 } 219 }
218 } 220 }
@@ -226,7 +228,9 @@ pub async fn handle_receive_pack(
226 228
227 // Write request to git's stdin 229 // Write request to git's stdin
228 if let Some(mut stdin) = git.take_stdin() { 230 if let Some(mut stdin) = git.take_stdin() {
229 stdin.write_all(&request_body).await 231 stdin
232 .write_all(&request_body)
233 .await
230 .map_err(GitError::IoError)?; 234 .map_err(GitError::IoError)?;
231 drop(stdin); 235 drop(stdin);
232 } 236 }
@@ -234,22 +238,25 @@ pub async fn handle_receive_pack(
234 // Read response from git's stdout 238 // Read response from git's stdout
235 let mut output = Vec::new(); 239 let mut output = Vec::new();
236 let mut stderr_output = Vec::new(); 240 let mut stderr_output = Vec::new();
237 241
238 if let Some(stdout) = git.take_stdout() { 242 if let Some(stdout) = git.take_stdout() {
239 let mut stdout = stdout; 243 let mut stdout = stdout;
240 stdout.read_to_end(&mut output).await 244 stdout
245 .read_to_end(&mut output)
246 .await
241 .map_err(GitError::IoError)?; 247 .map_err(GitError::IoError)?;
242 } 248 }
243 249
244 if let Some(stderr) = git.take_stderr() { 250 if let Some(stderr) = git.take_stderr() {
245 let mut stderr = stderr; 251 let mut stderr = stderr;
246 stderr.read_to_end(&mut stderr_output).await 252 stderr
253 .read_to_end(&mut stderr_output)
254 .await
247 .map_err(GitError::IoError)?; 255 .map_err(GitError::IoError)?;
248 } 256 }
249 257
250 // Wait for process 258 // Wait for process
251 let status = git.wait().await 259 let status = git.wait().await.map_err(GitError::IoError)?;
252 .map_err(GitError::IoError)?;
253 260
254 if !status.success() { 261 if !status.success() {
255 let stderr_str = String::from_utf8_lossy(&stderr_output); 262 let stderr_str = String::from_utf8_lossy(&stderr_output);
@@ -266,10 +273,7 @@ pub async fn handle_receive_pack(
266 if let Some(commit) = state.get_branch_commit(branch_name) { 273 if let Some(commit) = state.get_branch_commit(branch_name) {
267 match try_set_head_if_available(&repo_path, head_ref, commit) { 274 match try_set_head_if_available(&repo_path, head_ref, commit) {
268 Ok(true) => { 275 Ok(true) => {
269 info!( 276 info!("Set HEAD to {} after push to {:?}", head_ref, repo_path);
270 "Set HEAD to {} after push to {:?}",
271 head_ref, repo_path
272 );
273 } 277 }
274 Ok(false) => { 278 Ok(false) => {
275 debug!( 279 debug!(
@@ -278,10 +282,7 @@ pub async fn handle_receive_pack(
278 ); 282 );
279 } 283 }
280 Err(e) => { 284 Err(e) => {
281 warn!( 285 warn!("Failed to set HEAD after push: {}", e);
282 "Failed to set HEAD after push: {}",
283 e
284 );
285 } 286 }
286 } 287 }
287 } 288 }
@@ -291,7 +292,10 @@ pub async fn handle_receive_pack(
291 292
292 Ok(Response::builder() 293 Ok(Response::builder()
293 .status(StatusCode::OK) 294 .status(StatusCode::OK)
294 .header("content-type", GitService::ReceivePack.result_content_type()) 295 .header(
296 "content-type",
297 GitService::ReceivePack.result_content_type(),
298 )
295 .header("cache-control", "no-cache") 299 .header("cache-control", "no-cache")
296 .body(Full::new(Bytes::from(output))) 300 .body(Full::new(Bytes::from(output)))
297 .unwrap()) 301 .unwrap())
@@ -305,6 +309,7 @@ pub async fn handle_receive_pack(
305/// 3. Collects authorized publishers from that announcement (owner + maintainers) 309/// 3. Collects authorized publishers from that announcement (owner + maintainers)
306/// 4. Gets the latest authorized state from those publishers 310/// 4. Gets the latest authorized state from those publishers
307/// 5. Validates that pushed refs match the state 311/// 5. Validates that pushed refs match the state
312/// 6. Validates refs/nostr/<event-id> has valid event id and if event exists, `c` tag matches ref
308async fn authorize_push( 313async fn authorize_push(
309 database: &Arc<MemoryDatabase>, 314 database: &Arc<MemoryDatabase>,
310 identifier: &str, 315 identifier: &str,
@@ -323,59 +328,79 @@ async fn authorize_push(
323 debug!(" {} {} -> {}", ref_name, old_oid, new_oid); 328 debug!(" {} {} -> {}", ref_name, old_oid, new_oid);
324 } 329 }
325 330
326 // Check if ALL pushed refs are to refs/nostr/ with valid EventId format 331 // Separate refs/nostr/ refs from other refs
327 // Per GRASP-01: "MUST accept pushes via this service to `refs/nostr/<event-id>`" 332 // Per GRASP-01: "MUST accept pushes via this service to `refs/nostr/<event-id>`"
328 // These pushes only require EventId format validation, not state validation 333 let (nostr_refs, other_refs): (Vec<_>, Vec<_>) = pushed_refs
329 let all_refs_nostr_valid = !pushed_refs.is_empty() 334 .iter()
330 && pushed_refs.iter().all(|(_, _, ref_name)| { 335 .partition(|(_, _, ref_name)| ref_name.starts_with("refs/nostr/"));
331 if let Some(event_id_str) = ref_name.strip_prefix("refs/nostr/") { 336
332 // Validate it parses as a valid EventId 337 // Validate refs/nostr/ refs if any exist
333 EventId::parse(event_id_str).is_ok() 338 if !nostr_refs.is_empty() {
334 } else { 339 debug!(
335 false 340 "Found {} refs/nostr/ refs - validating against events",
336 } 341 nostr_refs.len()
337 }); 342 );
338 343
339 if all_refs_nostr_valid { 344 // Validate refs/nostr/ pushes: checks event ID format and commit matching
340 debug!("All refs are refs/nostr/ with valid EventId format - authorized without state check"); 345 let nostr_refs_owned: Vec<(String, String, String)> = nostr_refs
341 // Return success for refs/nostr/ pushes without requiring state 346 .into_iter()
347 .map(|(a, b, c)| (a.clone(), b.clone(), c.clone()))
348 .collect();
349 if let Err(e) = validate_nostr_ref_pushes(database, &nostr_refs_owned).await {
350 warn!("refs/nostr/ validation failed: {}", e);
351 return Ok(AuthorizationResult::denied(format!(
352 "refs/nostr/ validation failed: {}",
353 e
354 )));
355 }
356 debug!("refs/nostr/ push validated successfully");
357 }
358
359 // If only refs/nostr/ refs, we're done - return success
360 if other_refs.is_empty() {
361 debug!("Only refs/nostr/ refs in push - authorization complete");
342 return Ok(AuthorizationResult { 362 return Ok(AuthorizationResult {
343 authorized: true, 363 authorized: true,
344 reason: "Push to refs/nostr/ with valid EventId format".to_string(), 364 reason: "Push to refs/nostr/ validated against events".to_string(),
345 state: None, 365 state: None,
346 maintainers: vec![], 366 maintainers: vec![],
347 }); 367 });
348 } 368 }
349 369
350 // For non-refs/nostr/ pushes, require state validation as normal 370 // For non-refs/nostr/ refs, require state validation
351 debug!("Non-refs/nostr/ push detected - checking state authorization"); 371 debug!(
372 "Found {} non-refs/nostr/ refs - checking state authorization",
373 other_refs.len()
374 );
352 let auth_result = get_authorization_for_owner(database, identifier, owner_pubkey).await?; 375 let auth_result = get_authorization_for_owner(database, identifier, owner_pubkey).await?;
353 376
354 if !auth_result.authorized { 377 if !auth_result.authorized {
355 return Ok(auth_result); 378 return Ok(auth_result);
356 } 379 }
357 380
358 // Parse refs from the push request 381 // Convert other_refs for validation
359 let pushed_refs = parse_pushed_refs(request_body); 382 let other_refs_owned: Vec<(String, String, String)> = other_refs
360 debug!("Parsed {} refs from push request", pushed_refs.len()); 383 .into_iter()
361 for (old_oid, new_oid, ref_name) in &pushed_refs { 384 .map(|(a, b, c)| (a.clone(), b.clone(), c.clone()))
362 debug!(" {} {} -> {}", ref_name, old_oid, new_oid); 385 .collect();
363 }
364 386
365 // Validate refs against state 387 // Validate non-refs/nostr/ refs against state
366 if let Some(ref state) = auth_result.state { 388 if let Some(ref state) = auth_result.state {
367 debug!("Validating against state with {} branches", state.branches.len()); 389 debug!(
368 390 "Validating against state with {} branches",
391 state.branches.len()
392 );
393
369 // If we have a state event but couldn't parse any refs, reject the push. 394 // If we have a state event but couldn't parse any refs, reject the push.
370 // This protects against parsing failures allowing unauthorized pushes. 395 // This protects against parsing failures allowing unauthorized pushes.
371 if pushed_refs.is_empty() && !state.branches.is_empty() { 396 if other_refs_owned.is_empty() && !state.branches.is_empty() {
372 warn!("No refs parsed from push request but state event has branches - rejecting"); 397 warn!("No refs parsed from push request but state event has branches - rejecting");
373 return Ok(AuthorizationResult::denied( 398 return Ok(AuthorizationResult::denied(
374 "Failed to parse refs from push request - cannot validate against state" 399 "Failed to parse refs from push request - cannot validate against state",
375 )); 400 ));
376 } 401 }
377 402
378 if let Err(e) = validate_push_refs(state, &pushed_refs) { 403 if let Err(e) = validate_push_refs(state, &other_refs_owned) {
379 warn!("Ref validation failed: {}", e); 404 warn!("Ref validation failed: {}", e);
380 return Ok(AuthorizationResult::denied(format!( 405 return Ok(AuthorizationResult::denied(format!(
381 "Ref validation failed: {}", 406 "Ref validation failed: {}",
@@ -423,4 +448,4 @@ impl GitError {
423 _ => StatusCode::INTERNAL_SERVER_ERROR, 448 _ => StatusCode::INTERNAL_SERVER_ERROR,
424 } 449 }
425 } 450 }
426} \ No newline at end of file 451}