🏡 index : ~doyle/rgit.git

author Jordan Doyle <jordan@doyle.la> 2022-07-23 15:14:10.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2022-07-23 15:14:10.0 +01:00:00
commit
149a87f0619078331537f0e9b8386ac34fd51c04 [patch]
tree
375dbd27e98d15e67ec2adcb5114162a04cb890c
parent
f74037c1b1ba30e54d3f1b6008589ff60568e6b2
download
149a87f0619078331537f0e9b8386ac34fd51c04.tar.gz

Move smart_git to use tokio::process::Command



Diff

 Cargo.lock                    | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 Cargo.toml                    |   3 ++-
 src/git_cgi.rs                |  32 --------------------------------
 src/main.rs                   |   1 -
 src/methods/repo/mod.rs       |   9 +++++----
 src/methods/repo/smart_git.rs | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
 6 files changed, 196 insertions(+), 84 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 7933fd1..2d08078 100644
--- a/Cargo.lock
+++ a/Cargo.lock
@@ -165,6 +165,7 @@
 "bitflags",
 "bytes",
 "futures-util",
 "headers",
 "http",
 "http-body",
 "hyper",
@@ -284,10 +285,19 @@
 "block-padding",
 "byte-tools",
 "byteorder",
 "generic-array",
 "generic-array 0.12.4",
]

[[package]]
name = "block-buffer"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
 "generic-array 0.14.5",
]

[[package]]
name = "block-padding"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -530,6 +540,15 @@
checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
dependencies = [
 "memchr",
]

[[package]]
name = "cpufeatures"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
dependencies = [
 "libc",
]

[[package]]
@@ -599,6 +618,16 @@
dependencies = [
 "cfg-if 1.0.0",
 "once_cell",
]

[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
 "generic-array 0.14.5",
 "typenum",
]

[[package]]
@@ -606,8 +635,18 @@
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
dependencies = [
 "generic-array 0.12.4",
]

[[package]]
name = "digest"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
 "generic-array",
 "block-buffer 0.10.2",
 "crypto-common",
]

[[package]]
@@ -906,8 +945,18 @@
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
dependencies = [
 "typenum",
]

[[package]]
name = "generic-array"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [
 "typenum",
 "version_check",
]

[[package]]
@@ -960,6 +1009,31 @@
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"

[[package]]
name = "headers"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d"
dependencies = [
 "base64",
 "bitflags",
 "bytes",
 "headers-core",
 "http",
 "httpdate",
 "mime",
 "sha-1 0.10.0",
]

[[package]]
name = "headers-core"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
dependencies = [
 "http",
]

[[package]]
name = "heck"
@@ -1599,7 +1673,7 @@
dependencies = [
 "maplit",
 "pest",
 "sha-1",
 "sha-1 0.8.2",
]

[[package]]
@@ -1959,6 +2033,7 @@
 "time 0.3.11",
 "timeago",
 "tokio",
 "tokio-util",
 "tower",
 "tower-layer",
 "tower-service",
@@ -2092,10 +2167,21 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
dependencies = [
 "block-buffer",
 "digest",
 "block-buffer 0.7.3",
 "digest 0.8.1",
 "fake-simd",
 "opaque-debug",
]

[[package]]
name = "sha-1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
dependencies = [
 "cfg-if 1.0.0",
 "cpufeatures",
 "digest 0.10.3",
]

[[package]]
@@ -2440,6 +2526,19 @@
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "tokio-util"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45"
dependencies = [
 "bytes",
 "futures-core",
 "futures-sink",
 "pin-project-lite",
 "tokio",
]

[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 644500c..a27fb88 100644
--- a/Cargo.toml
+++ a/Cargo.toml
@@ -8,7 +8,7 @@
[dependencies]
askama = "0.11"
anyhow = "1.0"
axum = "0.5"
axum = { version = "0.5", features = ["headers"] }
axum-macros = "0.2"
bat = { version = "0.21", default-features = false, features = ["build-assets"] }
bytes = "1.1"
@@ -30,6 +30,7 @@
time = { version = "0.3", features = ["serde"] }
timeago = "0.3"
tokio = { version = "1.19", features = ["full"] }
tokio-util = { version = "0.7.3", features = ["io"] }
tower = "0.4"
tower-service = "0.3"
tower-layer = "0.3"
diff --git a/src/git_cgi.rs b/src/git_cgi.rs
deleted file mode 100644
index d46101f..0000000 100644
--- a/src/git_cgi.rs
+++ /dev/null
@@ -1,32 +1,0 @@
use std::str::FromStr;

use anyhow::{bail, Context, Result};
use axum::{
    body::{boxed, Body},
    http::{header::HeaderName, HeaderValue},
    response::Response,
};
use httparse::Status;

// https://en.wikipedia.org/wiki/Common_Gateway_Interface
pub fn cgi_to_response(buffer: &[u8]) -> Result<Response> {
    let mut headers = [httparse::EMPTY_HEADER; 10];
    let (body_offset, headers) = match httparse::parse_headers(buffer, &mut headers)? {
        Status::Complete(v) => v,
        Status::Partial => bail!("Git returned a partial response over CGI"),
    };

    let mut response = Response::new(boxed(Body::from(buffer[body_offset..].to_vec())));

    // TODO: extract status header
    for header in headers {
        response.headers_mut().insert(
            HeaderName::from_str(header.name)
                .context("Failed to parse header name from Git over CGI")?,
            HeaderValue::from_bytes(header.value)
                .context("Failed to parse header value from Git over CGI")?,
        );
    }

    Ok(response)
}
diff --git a/src/main.rs b/src/main.rs
index baf0d2d..dfc33f8 100644
--- a/src/main.rs
+++ a/src/main.rs
@@ -21,7 +21,6 @@

mod database;
mod git;
mod git_cgi;
mod layers;
mod methods;
mod syntax_highlight;
diff --git a/src/methods/repo/mod.rs b/src/methods/repo/mod.rs
index 0cbc06a..2cf2d54 100644
--- a/src/methods/repo/mod.rs
+++ a/src/methods/repo/mod.rs
@@ -31,7 +31,7 @@
    diff::{handle as handle_diff, handle_plain as handle_patch},
    log::handle as handle_log,
    refs::handle as handle_refs,
    smart_git::{handle_git_upload_pack, handle_info_refs},
    smart_git::handle as handle_smart_git,
    summary::handle as handle_summary,
    tag::handle as handle_tag,
    tree::handle as handle_tree,
@@ -47,6 +47,7 @@
where
    ReqBody: HttpBody + Send + Debug + 'static,
    <ReqBody as HttpBody>::Data: Send + Sync,
    bytes::Bytes: From<ReqBody::Data>,
    <ReqBody as HttpBody>::Error: std::error::Error + Send + Sync,
{
    let mut uri_parts: Vec<&str> = request
@@ -71,9 +72,9 @@
        // TODO: GIT_PROTOCOL
        Some("refs") if uri_parts.last() == Some(&"info") => {
            uri_parts.pop();
            h!(handle_info_refs)
            h!(handle_smart_git)
        }
        Some("git-upload-pack") => h!(handle_git_upload_pack),
        Some("git-upload-pack") => h!(handle_smart_git),
        Some("refs") => h!(handle_refs),
        Some("log") => h!(handle_log),
        Some("tree") => h!(handle_tree),
@@ -148,7 +149,7 @@
    }
}

pub type Result<T> = std::result::Result<T, Error>;
pub type Result<T, E = Error> = std::result::Result<T, E>;

pub struct Error(anyhow::Error);

diff --git a/src/methods/repo/smart_git.rs b/src/methods/repo/smart_git.rs
index 91512b4..67773dd 100644
--- a/src/methods/repo/smart_git.rs
+++ a/src/methods/repo/smart_git.rs
@@ -1,53 +1,97 @@
use std::{io::Write, process::Stdio};
use std::{io::ErrorKind, path::PathBuf, process::Stdio, str::FromStr};

use axum::{extract::Query, response::Response, Extension};
use bytes::Bytes;
use serde::Deserialize;
use anyhow::{bail, Context};
use axum::{
    body::{boxed, Body},
    extract::BodyStream,
    headers::{ContentType, HeaderName, HeaderValue},
    http::{Method, Uri},
    response::Response,
    Extension, TypedHeader,
};
use futures::TryStreamExt;
use httparse::Status;
use tokio_util::io::StreamReader;

use crate::methods::repo::{RepositoryPath, Result};
use crate::methods::repo::{Repository, RepositoryPath, Result};

#[derive(Deserialize)]
pub struct UriQuery {
    service: String,
}

#[allow(clippy::unused_async)]
pub async fn handle_info_refs(
pub async fn handle(
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Query(query): Query<UriQuery>,
    Extension(Repository(repository)): Extension<Repository>,
    method: Method,
    uri: Uri,
    body: BodyStream,
    content_type: Option<TypedHeader<ContentType>>,
) -> Result<Response> {
    // todo: tokio command
    let out = std::process::Command::new("git")
        .arg("http-backend")
        .env("REQUEST_METHOD", "GET")
        .env("PATH_INFO", "/info/refs")
        .env("GIT_PROJECT_ROOT", repository_path)
        .env("QUERY_STRING", format!("service={}", query.service))
        .output()
        .unwrap();

    Ok(crate::git_cgi::cgi_to_response(&out.stdout)?)
}
    let path = extract_path(&uri, &repository)?;

#[allow(clippy::unused_async)]
pub async fn handle_git_upload_pack(
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    body: Bytes,
) -> Result<Response> {
    // todo: tokio command
    let mut child = std::process::Command::new("git")
    let mut command = tokio::process::Command::new("git");

    if let Some(content_type) = content_type {
        command.env("CONTENT_TYPE", content_type.0.to_string());
    }

    let mut child = command
        .arg("http-backend")
        // todo: read all this from request
        .env("REQUEST_METHOD", "POST")
        .env("CONTENT_TYPE", "application/x-git-upload-pack-request")
        .env("PATH_INFO", "/git-upload-pack")
        .env("REQUEST_METHOD", method.as_str())
        .env("PATH_INFO", path)
        .env("GIT_PROJECT_ROOT", repository_path)
        .stdout(Stdio::piped())
        .env("QUERY_STRING", uri.query().unwrap_or(""))
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();
    child.stdin.as_mut().unwrap().write_all(&body).unwrap();
    let out = child.wait_with_output().unwrap();

    Ok(crate::git_cgi::cgi_to_response(&out.stdout)?)
        .context("Failed to spawn git http-backend")?;

    {
        let mut body =
            StreamReader::new(body.map_err(|e| std::io::Error::new(ErrorKind::Other, e)));
        let mut stdin = child.stdin.take().context("Stdin already taken")?;

        tokio::io::copy(&mut body, &mut stdin)
            .await
            .context("Failed to copy bytes from request to command stdin")?;
    }

    let out = child
        .wait_with_output()
        .await
        .context("Failed to read git http-backend response")?;
    let resp = cgi_to_response(&out.stdout)?;

    Ok(resp)
}

fn extract_path<'a>(uri: &'a Uri, repository: &PathBuf) -> Result<&'a str> {
    let path = uri.path();
    let path = path.strip_prefix("/").unwrap_or(path);

    if let Some(prefix) = repository.as_os_str().to_str() {
        Ok(path.strip_prefix(prefix).unwrap_or(path))
    } else {
        Err(anyhow::Error::msg("Repository name contains invalid bytes").into())
    }
}

// https://en.wikipedia.org/wiki/Common_Gateway_Interface
pub fn cgi_to_response(buffer: &[u8]) -> Result<Response, anyhow::Error> {
    let mut headers = [httparse::EMPTY_HEADER; 10];
    let (body_offset, headers) = match httparse::parse_headers(buffer, &mut headers)? {
        Status::Complete(v) => v,
        Status::Partial => bail!("Git returned a partial response over CGI"),
    };

    let mut response = Response::new(boxed(Body::from(buffer[body_offset..].to_vec())));

    // TODO: extract status header
    for header in headers {
        response.headers_mut().insert(
            HeaderName::from_str(header.name)
                .context("Failed to parse header name from Git over CGI")?,
            HeaderValue::from_bytes(header.value)
                .context("Failed to parse header value from Git over CGI")?,
        );
    }

    Ok(response)
}