| 1 | use axum::{body::Body, http::{Request, Response, StatusCode, HeaderMap}}; |
| 2 | use http_body_util::BodyExt; |
| 3 | use std::{process::Stdio, sync::Arc}; |
| 4 | use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, process::Command}; |
| 5 | use tokio_util::io::ReaderStream; |
| 6 | |
| 7 | use crate::config::Config; |
| 8 | |
| 9 | #[derive(Clone)] |
| 10 | pub struct GitRunner { |
| 11 | pub cfg: Arc<Config>, |
| 12 | } |
| 13 | |
| 14 | impl GitRunner { |
| 15 | pub fn new(cfg: Arc<Config>) -> Self { Self { cfg } } |
| 16 | |
| 17 | pub async fn run_cgi( |
| 18 | &self, |
| 19 | path_info: &str, |
| 20 | req: Request<Body>, |
| 21 | remote_user: Option<&str>, |
| 22 | ) -> Result<Response<Body>, StatusCode> { |
| 23 | let (parts, mut body) = req.into_parts(); |
| 24 | |
| 25 | let mut cmd = Command::new(&self.cfg.git_http_backend); |
| 26 | cmd.env("GIT_PROJECT_ROOT", &self.cfg.repos_root) |
| 27 | .env("GIT_HTTP_EXPORT_ALL", "1") |
| 28 | .env("REQUEST_METHOD", parts.method.as_str()) |
| 29 | .env("PATH_INFO", path_info) |
| 30 | .env("QUERY_STRING", parts.uri.query().unwrap_or("")) |
| 31 | .env("CONTENT_TYPE", parts.headers.get("content-type").and_then(|v| v.to_str().ok()).unwrap_or("")) |
| 32 | .env("REMOTE_USER", remote_user.unwrap_or("")) |
| 33 | .env("MAX_REPO_BYTES", self.cfg.max_repo_bytes.to_string()) |
| 34 | .stdin(Stdio::piped()) |
| 35 | .stdout(Stdio::piped()); |
| 36 | |
| 37 | if let Some(cl) = parts.headers.get("content-length").and_then(|v| v.to_str().ok()) { |
| 38 | cmd.env("CONTENT_LENGTH", cl); |
| 39 | } |
| 40 | |
| 41 | let mut child = cmd.spawn().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; |
| 42 | let mut stdin = child.stdin.take().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; |
| 43 | let stdout = child.stdout.take().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; |
| 44 | |
| 45 | tokio::spawn(async move { |
| 46 | while let Some(frame) = body.frame().await { |
| 47 | if let Ok(frame) = frame { |
| 48 | if let Ok(chunk) = frame.into_data() { |
| 49 | if stdin.write_all(&chunk).await.is_err() { break; } |
| 50 | } |
| 51 | } else { |
| 52 | break; |
| 53 | } |
| 54 | } |
| 55 | let _ = stdin.shutdown().await; |
| 56 | }); |
| 57 | |
| 58 | let mut reader = BufReader::new(stdout); |
| 59 | let (status, headers) = parse_cgi_headers(&mut reader).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; |
| 60 | |
| 61 | let stream = ReaderStream::new(reader); |
| 62 | let body = Body::from_stream(stream); |
| 63 | |
| 64 | let mut resp = Response::new(body); |
| 65 | *resp.status_mut() = status; |
| 66 | *resp.headers_mut() = headers; |
| 67 | |
| 68 | tokio::spawn(async move { let _ = child.wait().await; }); |
| 69 | Ok(resp) |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | async fn parse_cgi_headers<R: tokio::io::AsyncBufRead + Unpin>( |
| 74 | reader: &mut R, |
| 75 | ) -> Result<(StatusCode, HeaderMap), std::io::Error> { |
| 76 | let mut status = StatusCode::OK; |
| 77 | let mut headers = HeaderMap::new(); |
| 78 | |
| 79 | loop { |
| 80 | let mut line = String::new(); |
| 81 | let n = reader.read_line(&mut line).await?; |
| 82 | if n == 0 { break; } |
| 83 | |
| 84 | let line = line.trim_end_matches(&['\r','\n'][..]); |
| 85 | if line.is_empty() { break; } |
| 86 | |
| 87 | if let Some(rest) = line.strip_prefix("Status:") { |
| 88 | if let Some(code_str) = rest.trim().split_whitespace().next() { |
| 89 | if let Ok(code) = code_str.parse::<u16>() { |
| 90 | status = StatusCode::from_u16(code).unwrap_or(StatusCode::OK); |
| 91 | } |
| 92 | } |
| 93 | continue; |
| 94 | } |
| 95 | |
| 96 | if let Some((k, v)) = line.split_once(':') { |
| 97 | if let (Ok(hk), Ok(hv)) = ( |
| 98 | axum::http::HeaderName::from_bytes(k.trim().as_bytes()), |
| 99 | axum::http::HeaderValue::from_str(v.trim()), |
| 100 | ) { |
| 101 | headers.insert(hk, hv); |
| 102 | } |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | Ok((status, headers)) |
| 107 | } |