src/server/http/git_http.rs
Ref: Size: 5.1 KiB
use std::sync::Arc;
use axum::body::Bytes;
use axum::extract::{Path, Query, State};
use axum::http::{HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use super::AppState;
/// Max request body for git upload-pack negotiation (10 MiB).
/// Git smart HTTP negotiation payloads are typically small (a few KiB of want/have lines),
/// but we allow headroom for repos with many refs.
pub const UPLOAD_PACK_BODY_LIMIT: usize = 10 * 1024 * 1024;
#[derive(serde::Deserialize)]
pub struct InfoRefsQuery {
pub service: Option<String>,
}
pub async fn info_refs(
Path(repo_dot_git): Path<String>,
Query(query): Query<InfoRefsQuery>,
State(state): State<Arc<AppState>>,
) -> Response {
let service = match query.service.as_deref() {
Some("git-upload-pack") => "git-upload-pack",
_ => {
return (StatusCode::FORBIDDEN, "Only git-upload-pack is supported").into_response();
}
};
let repo_name = repo_dot_git.strip_suffix(".git").unwrap_or(&repo_dot_git);
let entry = match crate::repos::resolve(&state.repos_dir, repo_name) {
Some(e) => e,
None => return (StatusCode::NOT_FOUND, format!("Repository '{}' not found.", repo_name)).into_response(),
};
let git_dir = if entry.bare {
entry.path.clone()
} else {
entry.path.join(".git")
};
let output = match Command::new("git")
.args(["upload-pack", "--stateless-rpc", "--advertise-refs"])
.arg(&git_dir)
.output()
.await
{
Ok(o) => o,
Err(e) => {
tracing::error!("Failed to spawn git upload-pack: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to run git upload-pack").into_response();
}
};
if !output.status.success() {
tracing::error!(
"git upload-pack --advertise-refs exited with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
return (StatusCode::INTERNAL_SERVER_ERROR, "git upload-pack failed").into_response();
}
// Prepend pkt-line service announcement:
// "{4-hex-length}# service=git-upload-pack\n" + "0000"
let service_line = format!("# service={}\n", service);
let pkt_len = service_line.len() + 4; // +4 for the 4 hex digits themselves
let pkt_prefix = format!("{:04x}{}", pkt_len, service_line);
let flush = b"0000";
let mut body = Vec::new();
body.extend_from_slice(pkt_prefix.as_bytes());
body.extend_from_slice(flush);
body.extend_from_slice(&output.stdout);
(
StatusCode::OK,
[
("Content-Type", "application/x-git-upload-pack-advertisement"),
("Cache-Control", "no-cache"),
],
body,
)
.into_response()
}
pub async fn upload_pack(
Path(repo_dot_git): Path<String>,
State(state): State<Arc<AppState>>,
body: Bytes,
) -> Response {
let repo_name = repo_dot_git.strip_suffix(".git").unwrap_or(&repo_dot_git);
let entry = match crate::repos::resolve(&state.repos_dir, repo_name) {
Some(e) => e,
None => return (StatusCode::NOT_FOUND, format!("Repository '{}' not found.", repo_name)).into_response(),
};
let git_dir = if entry.bare {
entry.path.clone()
} else {
entry.path.join(".git")
};
let mut child = match Command::new("git")
.args(["upload-pack", "--stateless-rpc"])
.arg(&git_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
{
Ok(c) => c,
Err(e) => {
tracing::error!("Failed to spawn git upload-pack: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to run git upload-pack").into_response();
}
};
if let Some(mut stdin) = child.stdin.take() {
if let Err(e) = stdin.write_all(&body).await {
tracing::error!("Failed to write to git upload-pack stdin: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to communicate with git upload-pack").into_response();
}
}
let output = match child.wait_with_output().await {
Ok(o) => o,
Err(e) => {
tracing::error!("Failed to wait for git upload-pack: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, "git upload-pack failed").into_response();
}
};
if !output.status.success() {
tracing::error!(
"git upload-pack exited with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
return (StatusCode::INTERNAL_SERVER_ERROR, "git upload-pack failed").into_response();
}
let mut response = (
StatusCode::OK,
output.stdout,
)
.into_response();
let headers = response.headers_mut();
headers.insert(
"Content-Type",
HeaderValue::from_static("application/x-git-upload-pack-result"),
);
headers.insert(
"Cache-Control",
HeaderValue::from_static("no-cache"),
);
response
}