107 lines · 3748 bytes
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 }