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(-)
@@ -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]]
@@ -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"
@@ -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;
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())));
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)
}
@@ -21,7 +21,6 @@
mod database;
mod git;
mod git_cgi;
mod layers;
mod methods;
mod syntax_highlight;
@@ -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 @@
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);
@@ -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> {
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> {
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")
.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())
}
}
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())));
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)
}