gitscope
← rgit / 0.1.1 / src/methods/repo/smart_git.rs
https://dev.kronoz.win/r/rgit.git
src/methods/repo/smart_git.rs · Rust
Blame Raw
  1use std::{io::ErrorKind, path::Path, process::Stdio, str::FromStr};
  2
  3use anyhow::{bail, Context};
  4use axum::{
  5    body::{boxed, Body},
  6    extract::BodyStream,
  7    headers::{ContentType, HeaderName, HeaderValue},
  8    http::{Method, Uri},
  9    response::Response,
 10    Extension, TypedHeader,
 11};
 12use futures::TryStreamExt;
 13use httparse::Status;
 14use tokio_util::io::StreamReader;
 15use tracing::warn;
 16
 17use crate::methods::repo::{Repository, RepositoryPath, Result};
 18use crate::StatusCode;
 19
 20#[allow(clippy::unused_async)]
 21pub async fn handle(
 22    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
 23    Extension(Repository(repository)): Extension<Repository>,
 24    method: Method,
 25    uri: Uri,
 26    content_type: Option<TypedHeader<ContentType>>,
 27    body: BodyStream,
 28) -> Result<Response> {
 29    let path = extract_path(&uri, &repository)?;
 30
 31    let mut command = tokio::process::Command::new("git");
 32
 33    if let Some(content_type) = content_type {
 34        command.env("CONTENT_TYPE", content_type.0.to_string());
 35    }
 36
 37    let mut child = command
 38        .arg("http-backend")
 39        .env("REQUEST_METHOD", method.as_str())
 40        .env("PATH_INFO", path)
 41        .env("GIT_PROJECT_ROOT", repository_path)
 42        .env("QUERY_STRING", uri.query().unwrap_or(""))
 43        .stdin(Stdio::piped())
 44        .stdout(Stdio::piped())
 45        .stderr(Stdio::piped())
 46        .spawn()
 47        .context("Failed to spawn git http-backend")?;
 48
 49    {
 50        let mut body =
 51            StreamReader::new(body.map_err(|e| std::io::Error::new(ErrorKind::Other, e)));
 52        let mut stdin = child.stdin.take().context("Stdin already taken")?;
 53
 54        tokio::io::copy(&mut body, &mut stdin)
 55            .await
 56            .context("Failed to copy bytes from request to command stdin")?;
 57    }
 58
 59    let out = child
 60        .wait_with_output()
 61        .await
 62        .context("Failed to read git http-backend response")?;
 63    let resp = cgi_to_response(&out.stdout)?;
 64
 65    if !out.stderr.is_empty() {
 66        warn!(
 67            "Git returned an error: `{}`",
 68            String::from_utf8_lossy(&out.stderr)
 69        );
 70    }
 71
 72    Ok(resp)
 73}
 74
 75fn extract_path<'a>(uri: &'a Uri, repository: &Path) -> Result<&'a str> {
 76    let path = uri.path();
 77    let path = path.strip_prefix('/').unwrap_or(path);
 78
 79    if let Some(prefix) = repository.as_os_str().to_str() {
 80        Ok(path.strip_prefix(prefix).unwrap_or(path))
 81    } else {
 82        Err(anyhow::Error::msg("Repository name contains invalid bytes").into())
 83    }
 84}
 85
 86// https://en.wikipedia.org/wiki/Common_Gateway_Interface
 87pub fn cgi_to_response(buffer: &[u8]) -> Result<Response, anyhow::Error> {
 88    let mut headers = [httparse::EMPTY_HEADER; 10];
 89    let (body_offset, headers) = match httparse::parse_headers(buffer, &mut headers)? {
 90        Status::Complete(v) => v,
 91        Status::Partial => bail!("Git returned a partial response over CGI"),
 92    };
 93
 94    let mut response = Response::new(boxed(Body::from(buffer[body_offset..].to_vec())));
 95
 96    // TODO: extract status header
 97    for header in headers {
 98        response.headers_mut().insert(
 99            HeaderName::from_str(header.name)
100                .context("Failed to parse header name from Git over CGI")?,
101            HeaderValue::from_bytes(header.value)
102                .context("Failed to parse header value from Git over CGI")?,
103        );
104    }
105
106    if let Some(status) = response
107        .headers_mut()
108        .remove("Status")
109        .filter(|s| s.len() >= 3)
110    {
111        let status = &status.as_ref()[..3];
112
113        if let Ok(status) = StatusCode::from_bytes(status) {
114            *response.status_mut() = status;
115        }
116    }
117
118    Ok(response)
119}