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-11-21 13:37:57 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-21 13:37:57 +0000
commit9a8c551adfada379704d594a6ff3862f13857b8e (patch)
treea902c6b313ab40a8914a34380ee194524dd67604 /src/git/handlers.rs
parent12756fa66e3ec7f9dd24c66598085772829a8063 (diff)
add git http handling
Diffstat (limited to 'src/git/handlers.rs')
-rw-r--r--src/git/handlers.rs209
1 files changed, 209 insertions, 0 deletions
diff --git a/src/git/handlers.rs b/src/git/handlers.rs
new file mode 100644
index 0000000..0efc9d0
--- /dev/null
+++ b/src/git/handlers.rs
@@ -0,0 +1,209 @@
1//! Git HTTP Protocol Handlers
2//!
3//! This module implements the HTTP handlers for Git Smart HTTP protocol.
4
5use std::path::PathBuf;
6use hyper::{body::Bytes, Response, StatusCode};
7use tokio::io::{AsyncReadExt, AsyncWriteExt};
8use tracing::{debug, error, warn};
9
10use super::protocol::{GitService, PktLine};
11use super::subprocess::GitSubprocess;
12
13/// Handle GET /info/refs?service=git-{upload,receive}-pack
14///
15/// This advertises the repository's refs to the client.
16pub async fn handle_info_refs(
17 repo_path: PathBuf,
18 service: GitService,
19) -> Result<Response<String>, GitError> {
20 debug!("Handling info/refs for {:?} with service {:?}", repo_path, service);
21
22 // Check if repository exists
23 if !repo_path.exists() {
24 warn!("Repository not found: {:?}", repo_path);
25 return Err(GitError::RepositoryNotFound);
26 }
27
28 // Spawn git with --advertise-refs
29 let mut git = GitSubprocess::spawn(service, &repo_path, true)
30 .map_err(|e| {
31 error!("Failed to spawn git process: {}", e);
32 GitError::ProcessSpawnFailed(e)
33 })?;
34
35 // Read the output from git
36 let mut output = Vec::new();
37 if let Some(stdout) = git.take_stdout() {
38 let mut stdout = stdout;
39 stdout.read_to_end(&mut output).await
40 .map_err(|e| {
41 error!("Failed to read git output: {}", e);
42 GitError::IoError(e)
43 })?;
44 }
45
46 // Wait for process to complete
47 let status = git.wait().await
48 .map_err(|e| {
49 error!("Failed to wait for git process: {}", e);
50 GitError::IoError(e)
51 })?;
52
53 if !status.success() {
54 error!("Git process failed with status: {:?}", status);
55 return Err(GitError::GitFailed(status.code()));
56 }
57
58 // Build response with pkt-line header
59 let mut response_body = Vec::new();
60
61 // First line: service advertisement
62 let service_line = format!("# service={}\n", service.as_str());
63 response_body.extend_from_slice(&PktLine::data(service_line.as_bytes()).encode());
64 response_body.extend_from_slice(&PktLine::flush().encode());
65
66 // Then the git output
67 response_body.extend_from_slice(&output);
68
69 Ok(Response::builder()
70 .status(StatusCode::OK)
71 .header("content-type", service.advertisement_content_type())
72 .header("cache-control", "no-cache")
73 .body(String::from_utf8_lossy(&response_body).to_string())
74 .unwrap())
75}
76
77/// Handle POST /git-upload-pack (clone/fetch)
78pub async fn handle_upload_pack(
79 repo_path: PathBuf,
80 request_body: Bytes,
81) -> Result<Response<String>, GitError> {
82 debug!("Handling upload-pack for {:?}", repo_path);
83
84 if !repo_path.exists() {
85 return Err(GitError::RepositoryNotFound);
86 }
87
88 // Spawn git upload-pack
89 let mut git = GitSubprocess::spawn(GitService::UploadPack, &repo_path, false)
90 .map_err(GitError::ProcessSpawnFailed)?;
91
92 // Write request to git's stdin
93 if let Some(mut stdin) = git.take_stdin() {
94 stdin.write_all(&request_body).await
95 .map_err(GitError::IoError)?;
96 // Close stdin to signal end of input
97 drop(stdin);
98 }
99
100 // Read response from git's stdout
101 let mut output = Vec::new();
102 if let Some(stdout) = git.take_stdout() {
103 let mut stdout = stdout;
104 stdout.read_to_end(&mut output).await
105 .map_err(GitError::IoError)?;
106 }
107
108 // Wait for process
109 let status = git.wait().await
110 .map_err(GitError::IoError)?;
111
112 if !status.success() {
113 return Err(GitError::GitFailed(status.code()));
114 }
115
116 Ok(Response::builder()
117 .status(StatusCode::OK)
118 .header("content-type", GitService::UploadPack.result_content_type())
119 .header("cache-control", "no-cache")
120 .body(String::from_utf8_lossy(&output).to_string())
121 .unwrap())
122}
123
124/// Handle POST /git-receive-pack (push)
125///
126/// This includes an authorization hook point where GRASP validation will be added.
127pub async fn handle_receive_pack(
128 repo_path: PathBuf,
129 request_body: Bytes,
130) -> Result<Response<String>, GitError> {
131 debug!("Handling receive-pack for {:?}", repo_path);
132
133 if !repo_path.exists() {
134 return Err(GitError::RepositoryNotFound);
135 }
136
137 // TODO: Add GRASP authorization here
138 // For now, we'll accept all pushes to enable testing
139 debug!("Authorization check would go here (currently accepting all pushes)");
140
141 // Spawn git receive-pack
142 let mut git = GitSubprocess::spawn(GitService::ReceivePack, &repo_path, false)
143 .map_err(GitError::ProcessSpawnFailed)?;
144
145 // Write request to git's stdin
146 if let Some(mut stdin) = git.take_stdin() {
147 stdin.write_all(&request_body).await
148 .map_err(GitError::IoError)?;
149 drop(stdin);
150 }
151
152 // Read response from git's stdout
153 let mut output = Vec::new();
154 if let Some(stdout) = git.take_stdout() {
155 let mut stdout = stdout;
156 stdout.read_to_end(&mut output).await
157 .map_err(GitError::IoError)?;
158 }
159
160 // Wait for process
161 let status = git.wait().await
162 .map_err(GitError::IoError)?;
163
164 if !status.success() {
165 return Err(GitError::GitFailed(status.code()));
166 }
167
168 Ok(Response::builder()
169 .status(StatusCode::OK)
170 .header("content-type", GitService::ReceivePack.result_content_type())
171 .header("cache-control", "no-cache")
172 .body(String::from_utf8_lossy(&output).to_string())
173 .unwrap())
174}
175
176/// Errors that can occur in Git handlers
177#[derive(Debug)]
178pub enum GitError {
179 RepositoryNotFound,
180 ProcessSpawnFailed(std::io::Error),
181 IoError(std::io::Error),
182 GitFailed(Option<i32>),
183 Unauthorized,
184}
185
186impl std::fmt::Display for GitError {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 match self {
189 Self::RepositoryNotFound => write!(f, "repository not found"),
190 Self::ProcessSpawnFailed(e) => write!(f, "failed to spawn git process: {}", e),
191 Self::IoError(e) => write!(f, "IO error: {}", e),
192 Self::GitFailed(code) => write!(f, "git process failed with code: {:?}", code),
193 Self::Unauthorized => write!(f, "unauthorized"),
194 }
195 }
196}
197
198impl std::error::Error for GitError {}
199
200impl GitError {
201 /// Convert to HTTP status code
202 pub fn status_code(&self) -> StatusCode {
203 match self {
204 Self::RepositoryNotFound => StatusCode::NOT_FOUND,
205 Self::Unauthorized => StatusCode::FORBIDDEN,
206 _ => StatusCode::INTERNAL_SERVER_ERROR,
207 }
208 }
209} \ No newline at end of file