git_http_backend/actix/
git_receive_pack.rs1use crate::GitConfig;
2use actix_web::http::{header, StatusCode};
3use actix_web::web::Payload;
4use actix_web::{web, HttpRequest, HttpResponse, HttpResponseBuilder, Responder};
5use flate2::read::GzDecoder;
6use futures_util::StreamExt;
7use std::io;
8use std::io::{Cursor, Read, Write};
9use std::process::{Command, Stdio};
10use tracing::info;
11
12pub async fn git_receive_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-receive-pack", "");
19 let path = service.rewrite(path).await;
20 if !path.join("HEAD").exists() || !path.join("config").exists() {
21 return HttpResponse::BadRequest().body("Repository not found or invalid.");
22 }
23
24 let is_bare_repo = match std::fs::read_to_string(path.join("config")) {
25 Ok(config) => config.contains("bare = true"),
26 Err(_) => false,
27 };
28 if !is_bare_repo {
29 return HttpResponse::BadRequest().body("Push operation requires a bare repository.");
30 }
31
32 let version = request
33 .headers()
34 .get("Git-Protocol")
35 .and_then(|v| v.to_str().ok())
36 .unwrap_or("")
37 .to_string();
38
39 let mut resp = HttpResponseBuilder::new(StatusCode::OK);
40 resp.append_header(("Content-Type", "application/x-git-receive-pack-advertise"));
41 resp.append_header(("Connection", "Keep-Alive"));
42 resp.append_header(("Transfer-Encoding", "chunked"));
43 resp.append_header(("X-Content-Type-Options", "nosniff"));
44
45 let mut cmd = Command::new("git");
46 cmd.arg("receive-pack");
47 cmd.arg("--stateless-rpc");
48 cmd.arg(".");
49 if !version.is_empty() {
50 cmd.env("GIT_PROTOCOL", version);
51 }
52 cmd.stderr(Stdio::piped());
53 cmd.stdin(Stdio::piped());
54 cmd.stdout(Stdio::piped());
55 cmd.current_dir(&path);
56
57 let mut git_process = match cmd.spawn() {
58 Ok(process) => process,
59 Err(e) => {
60 info!("Error running git command: {}", e);
61 return HttpResponse::InternalServerError().body("Error running git command");
62 }
63 };
64
65 let mut stdin = git_process.stdin.take().unwrap();
66 let mut stdout = git_process.stdout.take().unwrap();
67
68 let mut bytes = web::BytesMut::new();
69 while let Some(chunk) = payload.next().await {
70 match chunk {
71 Ok(data) => bytes.extend_from_slice(&data),
72 Err(e) => {
73 return HttpResponse::InternalServerError()
74 .body(format!("Failed to read request body: {}", e))
75 }
76 }
77 }
78
79 let body_data = match request
80 .headers()
81 .get(header::CONTENT_ENCODING)
82 .and_then(|v| v.to_str().ok())
83 {
84 Some(encoding) if encoding.contains("gzip") => {
85 let mut decoder = GzDecoder::new(Cursor::new(bytes));
86 let mut decoded_data = Vec::new();
87 if let Err(e) = io::copy(&mut decoder, &mut decoded_data) {
88 return HttpResponse::InternalServerError()
89 .body(format!("Failed to decode gzip body: {}", e));
90 }
91 decoded_data
92 }
93 _ => bytes.to_vec(),
94 };
95
96 if let Err(e) = stdin.write_all(&body_data) {
97 return HttpResponse::InternalServerError()
98 .body(format!("Failed to write to git process: {}", e));
99 }
100 drop(stdin);
101
102 let body_stream = actix_web::body::BodyStream::new(async_stream::stream! {
103 let mut buffer = [0; 8192];
104 loop {
105 match stdout.read(&mut buffer) {
106 Ok(0) => break, Ok(n) => yield Ok::<_, io::Error>(web::Bytes::copy_from_slice(&buffer[..n])),
108 Err(e) => {
109 eprintln!("Error reading stdout: {}", e);
110 break;
111 }
112 }
113 }
114 });
115 resp.body(body_stream)
116}