diff --git a/Cargo.lock b/Cargo.lock
@@ -165,6 +165,7 @@ dependencies = [
"bitflags",
"bytes",
"futures-util",
+ "headers",
"http",
"http-body",
"hyper",
@@ -284,7 +285,16 @@ dependencies = [
"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]]
@@ -532,6 +542,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "cpufeatures"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "crc32fast"
version = "1.3.2"
@@ -601,13 +620,33 @@ dependencies = [
"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]]
name = "digest"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
dependencies = [
- "generic-array",
+ "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 = [
+ "block-buffer 0.10.2",
+ "crypto-common",
]
[[package]]
@@ -910,6 +949,16 @@ 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]]
name = "getrandom"
version = "0.2.7"
@@ -961,6 +1010,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"
version = "0.4.0"
@@ -1599,7 +1673,7 @@ checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
dependencies = [
"maplit",
"pest",
- "sha-1",
+ "sha-1 0.8.2",
]
[[package]]
@@ -1959,6 +2033,7 @@ dependencies = [
"time 0.3.11",
"timeago",
"tokio",
+ "tokio-util",
"tower",
"tower-layer",
"tower-service",
@@ -2092,12 +2167,23 @@ version = "0.8.2"
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]]
name = "sharded-slab"
version = "0.1.4"
@@ -2442,6 +2528,19 @@ dependencies = [
"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]]
name = "toml"
version = "0.5.9"
diff --git a/Cargo.toml b/Cargo.toml
@@ -8,7 +8,7 @@ edition = "2021"
[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 @@ sled = { version = "0.34", features = ["compression"] }
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
@@ -1,32 +0,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
@@ -21,7 +21,6 @@ use crate::{git::Git, layers::logger::LoggingMiddleware};
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
@@ -31,7 +31,7 @@ use self::{
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 @@ pub async fn service<ReqBody>(mut request: Request<ReqBody>) -> Response
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 @@ where
// 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 @@ impl Deref for RepositoryPath {
}
}
-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
@@ -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};
-
-#[derive(Deserialize)]
-pub struct UriQuery {
- service: String,
-}
+use crate::methods::repo::{Repository, RepositoryPath, Result};
#[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();
+ let path = extract_path(&uri, &repository)?;
- Ok(crate::git_cgi::cgi_to_response(&out.stdout)?)
-}
+ let mut command = tokio::process::Command::new("git");
-#[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")
+ 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();
+ .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(crate::git_cgi::cgi_to_response(&out.stdout)?)
+ Ok(response)
}