From 149a87f0619078331537f0e9b8386ac34fd51c04 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 23 Jul 2022 15:14:10 +0100 Subject: [PATCH] Move smart_git to use tokio::process::Command --- 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 { - 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, ::Data: Send + Sync, + bytes::Bytes: From, ::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 = std::result::Result; +pub type Result = std::result::Result; 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, - Query(query): Query, + Extension(Repository(repository)): Extension, + method: Method, + uri: Uri, + body: BodyStream, + content_type: Option>, ) -> Result { - // 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, - body: Bytes, -) -> Result { - // 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 { + 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) } -- rgit 0.1.3