httpd: Move `git` router & handler into its own file
Signed-off-by: xphoniex <dj.2dixx@gmail.com>
This commit is contained in:
parent
158f1ff7fa
commit
be5afcc600
|
|
@ -0,0 +1,194 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::{io, net, str};
|
||||||
|
|
||||||
|
use axum::body::Bytes;
|
||||||
|
use axum::extract::{ConnectInfo, Path as AxumPath, RawQuery, State};
|
||||||
|
use axum::http::header::HeaderName;
|
||||||
|
use axum::http::{HeaderMap, Method, StatusCode};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::routing::any;
|
||||||
|
use axum::Router;
|
||||||
|
use flate2::write::GzDecoder;
|
||||||
|
use hyper::body::Buf as _;
|
||||||
|
|
||||||
|
use radicle::identity::Id;
|
||||||
|
use radicle::profile::Profile;
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
pub fn router(profile: Arc<Profile>) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/:project/*request", any(git_handler))
|
||||||
|
.with_state(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn git_handler(
|
||||||
|
State(profile): State<Arc<Profile>>,
|
||||||
|
AxumPath((project, request)): AxumPath<(String, String)>,
|
||||||
|
method: Method,
|
||||||
|
headers: HeaderMap,
|
||||||
|
ConnectInfo(remote): ConnectInfo<SocketAddr>,
|
||||||
|
query: RawQuery,
|
||||||
|
body: Bytes,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let query = query.0.unwrap_or_default();
|
||||||
|
let id: Id = project.strip_suffix(".git").unwrap_or(&project).parse()?;
|
||||||
|
|
||||||
|
let (status, headers, body) =
|
||||||
|
git_http_backend(&profile, method, headers, body, remote, id, &request, query).await?;
|
||||||
|
|
||||||
|
let mut response_headers = HeaderMap::new();
|
||||||
|
for (name, vec) in headers.iter() {
|
||||||
|
for value in vec {
|
||||||
|
let header: HeaderName = name.try_into()?;
|
||||||
|
response_headers.insert(header, value.parse()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<_, Error>((status, response_headers, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn git_http_backend(
|
||||||
|
profile: &Profile,
|
||||||
|
method: Method,
|
||||||
|
headers: HeaderMap,
|
||||||
|
mut body: Bytes,
|
||||||
|
remote: net::SocketAddr,
|
||||||
|
id: Id,
|
||||||
|
path: &str,
|
||||||
|
query: String,
|
||||||
|
) -> Result<(StatusCode, HashMap<String, Vec<String>>, Vec<u8>), Error> {
|
||||||
|
let git_dir = radicle::storage::git::paths::repository(&profile.storage, &id);
|
||||||
|
let content_type =
|
||||||
|
if let Some(Ok(content_type)) = headers.get("Content-Type").map(|h| h.to_str()) {
|
||||||
|
content_type
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reject push requests.
|
||||||
|
match (path, query.as_str()) {
|
||||||
|
("git-receive-pack", _) | (_, "service=git-receive-pack") => {
|
||||||
|
return Err(Error::ServiceUnavailable("git-receive-pack"));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::debug!("id: {:?}", id);
|
||||||
|
tracing::debug!("headers: {:?}", headers);
|
||||||
|
tracing::debug!("path: {:?}", path);
|
||||||
|
tracing::debug!("method: {:?}", method.as_str());
|
||||||
|
tracing::debug!("remote: {:?}", remote.to_string());
|
||||||
|
|
||||||
|
let mut cmd = Command::new("git");
|
||||||
|
let mut child = cmd
|
||||||
|
.arg("http-backend")
|
||||||
|
.env("REQUEST_METHOD", method.as_str())
|
||||||
|
.env("GIT_PROJECT_ROOT", git_dir)
|
||||||
|
// "The GIT_HTTP_EXPORT_ALL environmental variable may be passed to git-http-backend to bypass
|
||||||
|
// the check for the "git-daemon-export-ok" file in each repository before allowing export of
|
||||||
|
// that repository."
|
||||||
|
.env("GIT_HTTP_EXPORT_ALL", String::default())
|
||||||
|
.env("PATH_INFO", Path::new("/").join(path))
|
||||||
|
.env("CONTENT_TYPE", content_type)
|
||||||
|
.env("QUERY_STRING", query)
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
// Whether the request body is compressed.
|
||||||
|
let gzip = matches!(
|
||||||
|
headers.get("Content-Encoding").map(|h| h.to_str()),
|
||||||
|
Some(Ok("gzip"))
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
// This is safe because we captured the child's stdin.
|
||||||
|
let mut stdin = child.stdin.take().unwrap();
|
||||||
|
|
||||||
|
// Copy the request body to git-http-backend's stdin.
|
||||||
|
if gzip {
|
||||||
|
let mut decoder = GzDecoder::new(&mut stdin);
|
||||||
|
let mut reader = body.reader();
|
||||||
|
|
||||||
|
io::copy(&mut reader, &mut decoder)?;
|
||||||
|
decoder.finish()?;
|
||||||
|
} else {
|
||||||
|
while body.has_remaining() {
|
||||||
|
let mut chunk = body.chunk();
|
||||||
|
let count = chunk.len();
|
||||||
|
|
||||||
|
io::copy(&mut chunk, &mut stdin)?;
|
||||||
|
body.advance(count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match child.wait_with_output() {
|
||||||
|
Ok(output) if output.status.success() => {
|
||||||
|
tracing::info!("git-http-backend: exited successfully for {}", id);
|
||||||
|
|
||||||
|
let mut reader = std::io::Cursor::new(output.stdout);
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
|
||||||
|
// Parse headers returned by git so that we can use them in the client response.
|
||||||
|
for line in io::Read::by_ref(&mut reader).lines() {
|
||||||
|
let line = line?;
|
||||||
|
|
||||||
|
if line.is_empty() || line == "\r" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut parts = line.splitn(2, ':');
|
||||||
|
let key = parts.next();
|
||||||
|
let value = parts.next();
|
||||||
|
|
||||||
|
if let (Some(key), Some(value)) = (key, value) {
|
||||||
|
let value = &value[1..];
|
||||||
|
|
||||||
|
headers
|
||||||
|
.entry(key.to_string())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(value.to_string());
|
||||||
|
} else {
|
||||||
|
return Err(Error::Backend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = {
|
||||||
|
tracing::debug!("git-http-backend: {:?}", &headers);
|
||||||
|
|
||||||
|
let line = headers.remove("Status").unwrap_or_default();
|
||||||
|
let line = line.into_iter().next().unwrap_or_default();
|
||||||
|
let mut parts = line.split(' ');
|
||||||
|
|
||||||
|
parts
|
||||||
|
.next()
|
||||||
|
.and_then(|p| p.parse().ok())
|
||||||
|
.unwrap_or(StatusCode::OK)
|
||||||
|
};
|
||||||
|
|
||||||
|
let position = reader.position() as usize;
|
||||||
|
let body = reader.into_inner().split_off(position);
|
||||||
|
|
||||||
|
Ok((status, headers, body))
|
||||||
|
}
|
||||||
|
Ok(output) => {
|
||||||
|
tracing::error!("git-http-backend: exited with code {}", output.status);
|
||||||
|
|
||||||
|
if let Ok(output) = std::str::from_utf8(&output.stderr) {
|
||||||
|
tracing::error!("git-http-backend: stderr: {}", output.trim_end());
|
||||||
|
}
|
||||||
|
Err(Error::Backend)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
panic!("failed to wait for git-http-backend: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,41 +2,25 @@
|
||||||
#![allow(clippy::too_many_arguments)]
|
#![allow(clippy::too_many_arguments)]
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::io::prelude::*;
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::Path;
|
use std::process::Command;
|
||||||
use std::process::{Command, Stdio};
|
use std::str;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{io, net, str};
|
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use axum::body::Body;
|
use axum::body::{Body, BoxBody};
|
||||||
use axum::body::{BoxBody, Bytes};
|
use axum::http::{Request, Response};
|
||||||
use axum::extract::{ConnectInfo, Path as AxumPath, RawQuery};
|
use axum::Router;
|
||||||
use axum::http::header::HeaderName;
|
|
||||||
use axum::http::{HeaderMap, Method, Request, Response, StatusCode};
|
|
||||||
use axum::response::IntoResponse;
|
|
||||||
use axum::routing::any;
|
|
||||||
use axum::{Extension, Router};
|
|
||||||
use flate2::write::GzDecoder;
|
|
||||||
use hyper::body::Buf as _;
|
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing::Span;
|
use tracing::Span;
|
||||||
|
|
||||||
use radicle::identity::Id;
|
|
||||||
use radicle::profile::Profile;
|
|
||||||
|
|
||||||
use error::Error;
|
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
|
mod git;
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
pub listen: net::SocketAddr,
|
pub listen: SocketAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the Server.
|
/// Run the Server.
|
||||||
|
|
@ -51,12 +35,9 @@ pub async fn run(options: Options) -> anyhow::Result<()> {
|
||||||
let profile = Arc::new(radicle::Profile::load()?);
|
let profile = Arc::new(radicle::Profile::load()?);
|
||||||
tracing::info!("using radicle home at {}", profile.home().display());
|
tracing::info!("using radicle home at {}", profile.home().display());
|
||||||
|
|
||||||
let git_router = Router::new()
|
let ctx = api::Context::new(profile.clone());
|
||||||
.route("/:project/*request", any(git_handler))
|
|
||||||
.layer(Extension(profile.clone()));
|
|
||||||
|
|
||||||
let ctx = api::Context::new(profile);
|
|
||||||
let api_router = api::router(ctx);
|
let api_router = api::router(ctx);
|
||||||
|
let git_router = git::router(profile);
|
||||||
|
|
||||||
tracing::info!("listening on http://{}", options.listen);
|
tracing::info!("listening on http://{}", options.listen);
|
||||||
|
|
||||||
|
|
@ -90,169 +71,3 @@ pub async fn run(options: Options) -> anyhow::Result<()> {
|
||||||
.await
|
.await
|
||||||
.map_err(anyhow::Error::from)
|
.map_err(anyhow::Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn git_handler(
|
|
||||||
Extension(profile): Extension<Arc<Profile>>,
|
|
||||||
AxumPath((project, request)): AxumPath<(String, String)>,
|
|
||||||
method: Method,
|
|
||||||
headers: HeaderMap,
|
|
||||||
ConnectInfo(remote): ConnectInfo<SocketAddr>,
|
|
||||||
query: RawQuery,
|
|
||||||
body: Bytes,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let query = query.0.unwrap_or_default();
|
|
||||||
let id: Id = project.strip_suffix(".git").unwrap_or(&project).parse()?;
|
|
||||||
|
|
||||||
let (status, headers, body) =
|
|
||||||
git_http_backend(&profile, method, headers, body, remote, id, &request, query).await?;
|
|
||||||
|
|
||||||
let mut response_headers = HeaderMap::new();
|
|
||||||
for (name, vec) in headers.iter() {
|
|
||||||
for value in vec {
|
|
||||||
let header: HeaderName = name.try_into()?;
|
|
||||||
response_headers.insert(header, value.parse()?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<_, Error>((status, response_headers, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn git_http_backend(
|
|
||||||
profile: &Profile,
|
|
||||||
method: Method,
|
|
||||||
headers: HeaderMap,
|
|
||||||
mut body: Bytes,
|
|
||||||
remote: net::SocketAddr,
|
|
||||||
id: Id,
|
|
||||||
path: &str,
|
|
||||||
query: String,
|
|
||||||
) -> Result<(StatusCode, HashMap<String, Vec<String>>, Vec<u8>), Error> {
|
|
||||||
let git_dir = radicle::storage::git::paths::repository(&profile.storage, &id);
|
|
||||||
let content_type =
|
|
||||||
if let Some(Ok(content_type)) = headers.get("Content-Type").map(|h| h.to_str()) {
|
|
||||||
content_type
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reject push requests.
|
|
||||||
match (path, query.as_str()) {
|
|
||||||
("git-receive-pack", _) | (_, "service=git-receive-pack") => {
|
|
||||||
return Err(Error::ServiceUnavailable("git-receive-pack"));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::debug!("id: {:?}", id);
|
|
||||||
tracing::debug!("headers: {:?}", headers);
|
|
||||||
tracing::debug!("path: {:?}", path);
|
|
||||||
tracing::debug!("method: {:?}", method.as_str());
|
|
||||||
tracing::debug!("remote: {:?}", remote.to_string());
|
|
||||||
|
|
||||||
let mut cmd = Command::new("git");
|
|
||||||
let mut child = cmd
|
|
||||||
.arg("http-backend")
|
|
||||||
.env("REQUEST_METHOD", method.as_str())
|
|
||||||
.env("GIT_PROJECT_ROOT", git_dir)
|
|
||||||
// "The GIT_HTTP_EXPORT_ALL environmental variable may be passed to git-http-backend to bypass
|
|
||||||
// the check for the "git-daemon-export-ok" file in each repository before allowing export of
|
|
||||||
// that repository."
|
|
||||||
.env("GIT_HTTP_EXPORT_ALL", String::default())
|
|
||||||
.env("PATH_INFO", Path::new("/").join(path))
|
|
||||||
.env("CONTENT_TYPE", content_type)
|
|
||||||
.env("QUERY_STRING", query)
|
|
||||||
.stderr(Stdio::piped())
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stdin(Stdio::piped())
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
// Whether the request body is compressed.
|
|
||||||
let gzip = matches!(
|
|
||||||
headers.get("Content-Encoding").map(|h| h.to_str()),
|
|
||||||
Some(Ok("gzip"))
|
|
||||||
);
|
|
||||||
|
|
||||||
{
|
|
||||||
// This is safe because we captured the child's stdin.
|
|
||||||
let mut stdin = child.stdin.take().unwrap();
|
|
||||||
|
|
||||||
// Copy the request body to git-http-backend's stdin.
|
|
||||||
if gzip {
|
|
||||||
let mut decoder = GzDecoder::new(&mut stdin);
|
|
||||||
let mut reader = body.reader();
|
|
||||||
|
|
||||||
io::copy(&mut reader, &mut decoder)?;
|
|
||||||
decoder.finish()?;
|
|
||||||
} else {
|
|
||||||
while body.has_remaining() {
|
|
||||||
let mut chunk = body.chunk();
|
|
||||||
let count = chunk.len();
|
|
||||||
|
|
||||||
io::copy(&mut chunk, &mut stdin)?;
|
|
||||||
body.advance(count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match child.wait_with_output() {
|
|
||||||
Ok(output) if output.status.success() => {
|
|
||||||
tracing::info!("git-http-backend: exited successfully for {}", id);
|
|
||||||
|
|
||||||
let mut reader = std::io::Cursor::new(output.stdout);
|
|
||||||
let mut headers = HashMap::new();
|
|
||||||
|
|
||||||
// Parse headers returned by git so that we can use them in the client response.
|
|
||||||
for line in io::Read::by_ref(&mut reader).lines() {
|
|
||||||
let line = line?;
|
|
||||||
|
|
||||||
if line.is_empty() || line == "\r" {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut parts = line.splitn(2, ':');
|
|
||||||
let key = parts.next();
|
|
||||||
let value = parts.next();
|
|
||||||
|
|
||||||
if let (Some(key), Some(value)) = (key, value) {
|
|
||||||
let value = &value[1..];
|
|
||||||
|
|
||||||
headers
|
|
||||||
.entry(key.to_string())
|
|
||||||
.or_insert_with(Vec::new)
|
|
||||||
.push(value.to_string());
|
|
||||||
} else {
|
|
||||||
return Err(Error::Backend);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = {
|
|
||||||
tracing::debug!("git-http-backend: {:?}", &headers);
|
|
||||||
|
|
||||||
let line = headers.remove("Status").unwrap_or_default();
|
|
||||||
let line = line.into_iter().next().unwrap_or_default();
|
|
||||||
let mut parts = line.split(' ');
|
|
||||||
|
|
||||||
parts
|
|
||||||
.next()
|
|
||||||
.and_then(|p| p.parse().ok())
|
|
||||||
.unwrap_or(StatusCode::OK)
|
|
||||||
};
|
|
||||||
|
|
||||||
let position = reader.position() as usize;
|
|
||||||
let body = reader.into_inner().split_off(position);
|
|
||||||
|
|
||||||
Ok((status, headers, body))
|
|
||||||
}
|
|
||||||
Ok(output) => {
|
|
||||||
tracing::error!("git-http-backend: exited with code {}", output.status);
|
|
||||||
|
|
||||||
if let Ok(output) = std::str::from_utf8(&output.stderr) {
|
|
||||||
tracing::error!("git-http-backend: stderr: {}", output.trim_end());
|
|
||||||
}
|
|
||||||
Err(Error::Backend)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
panic!("failed to wait for git-http-backend: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue