git_http_backend/actix/
git_upload_pack.rs

1use crate::GitConfig;
2use actix_web::http::header::HeaderValue;
3use actix_web::http::{header, StatusCode};
4use actix_web::web::Payload;
5use actix_web::{web, HttpRequest, HttpResponse, HttpResponseBuilder, Responder};
6use flate2::read::GzDecoder;
7use futures_util::StreamExt;
8use std::io;
9use std::io::{Cursor, Read, Write};
10use std::process::{Command, Stdio};
11
12pub async fn git_upload_pack(
13    request: HttpRequest,
14    mut payload: Payload,
15    service: web::Data<impl GitConfig>,
16) -> impl Responder {
17    let uri = request.uri();
18    let path = uri.path().to_string().replace("/git-upload-pack", "");
19    let path = service.rewrite(path).await;
20    let version = request
21        .headers()
22        .get("Git-Protocol")
23        .unwrap_or(&HeaderValue::from_str("").unwrap())
24        .to_str()
25        .map(|s| s.to_string())
26        .unwrap_or("".to_string());
27
28    let mut resp = HttpResponseBuilder::new(StatusCode::OK);
29    resp.append_header(("Content-Type", "application/x-git-upload-pack-advertise"));
30    resp.append_header(("Connection", "Keep-Alive"));
31    resp.append_header(("Transfer-Encoding", "chunked"));
32    resp.append_header(("X-Content-Type-Options", "nosniff"));
33    let mut cmd = Command::new("git");
34    cmd.arg("upload-pack");
35    cmd.arg("--stateless-rpc");
36    cmd.arg(".");
37    if !version.is_empty() {
38        cmd.env("GIT_PROTOCOL", version.clone());
39    }
40    cmd.stderr(Stdio::piped());
41    cmd.stdin(Stdio::piped());
42    cmd.stdout(Stdio::piped());
43    cmd.current_dir(path);
44
45    let span = cmd.spawn();
46    let mut span = match span {
47        Ok(span) => span,
48        Err(e) => {
49            eprintln!("Error running command: {}", e);
50            return HttpResponse::InternalServerError().body("Error running command");
51        }
52    };
53
54    let mut stdin = span.stdin.take().unwrap();
55    let mut stdout = span.stdout.take().unwrap();
56    let _stderr = span.stderr.take().unwrap();
57    let mut bytes = web::BytesMut::new();
58    while let Some(chunk) = payload.next().await {
59        match chunk {
60            Ok(data) => bytes.extend_from_slice(&data),
61            Err(e) => {
62                return HttpResponse::InternalServerError()
63                    .body(format!("Failed to read request body: {}", e))
64            }
65        }
66    }
67    let body_data = match request
68        .headers()
69        .get(header::CONTENT_ENCODING)
70        .and_then(|v| v.to_str().ok())
71    {
72        Some("gzip") => {
73            let mut decoder = GzDecoder::new(Cursor::new(bytes));
74            let mut decoded_data = Vec::new();
75            if let Err(e) = io::copy(&mut decoder, &mut decoded_data) {
76                return HttpResponse::InternalServerError()
77                    .body(format!("Failed to decode gzip body: {}", e));
78            }
79            decoded_data
80        }
81        _ => bytes.to_vec(),
82    };
83    if let Err(e) = stdin.write_all(&body_data) {
84        return HttpResponse::InternalServerError()
85            .body(format!("Failed to write to git process: {}", e));
86    }
87    drop(stdin);
88
89    let body_stream = actix_web::body::BodyStream::new(async_stream::stream! {
90        let mut buffer = [0; 8192];
91        loop {
92            match stdout.read(&mut buffer) {
93                Ok(0) => break, // EOF
94                Ok(n) => {
95                    yield Ok::<_, io::Error>(web::Bytes::copy_from_slice(&buffer[..n]))
96                },
97                Err(e) => {
98                    eprintln!("Error reading stdout: {}", e);
99                    break;
100                }
101            }
102        }
103    });
104    resp.body(body_stream)
105}