diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-21 13:37:57 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-21 13:37:57 +0000 |
| commit | 9a8c551adfada379704d594a6ff3862f13857b8e (patch) | |
| tree | a902c6b313ab40a8914a34380ee194524dd67604 /src/git/handlers.rs | |
| parent | 12756fa66e3ec7f9dd24c66598085772829a8063 (diff) | |
add git http handling
Diffstat (limited to 'src/git/handlers.rs')
| -rw-r--r-- | src/git/handlers.rs | 209 |
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 | |||
| 5 | use std::path::PathBuf; | ||
| 6 | use hyper::{body::Bytes, Response, StatusCode}; | ||
| 7 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; | ||
| 8 | use tracing::{debug, error, warn}; | ||
| 9 | |||
| 10 | use super::protocol::{GitService, PktLine}; | ||
| 11 | use 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. | ||
| 16 | pub 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) | ||
| 78 | pub 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. | ||
| 127 | pub 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)] | ||
| 178 | pub enum GitError { | ||
| 179 | RepositoryNotFound, | ||
| 180 | ProcessSpawnFailed(std::io::Error), | ||
| 181 | IoError(std::io::Error), | ||
| 182 | GitFailed(Option<i32>), | ||
| 183 | Unauthorized, | ||
| 184 | } | ||
| 185 | |||
| 186 | impl 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 | |||
| 198 | impl std::error::Error for GitError {} | ||
| 199 | |||
| 200 | impl 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 | ||