httpd: Basic alias support for git

Allows cloning via git using an alias instead of the full RID.

Eg. `git clone https://seed.radicle.xyz/heartwood.git`
This commit is contained in:
Alexis Sellier 2023-03-12 18:13:17 +01:00
parent 166f7e7cc2
commit 6e90444406
5 changed files with 72 additions and 11 deletions

View File

@ -4,6 +4,10 @@ use axum::response::{IntoResponse, Response};
/// Errors relating to the HTTP backend.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// The entity was not found.
#[error("not found")]
NotFound,
/// I/O error.
#[error("i/o error: {0}")]
Io(#[from] std::io::Error),
@ -47,6 +51,7 @@ impl Error {
Error::ServiceUnavailable(_) => http::StatusCode::SERVICE_UNAVAILABLE,
Error::InvalidId => http::StatusCode::NOT_FOUND,
Error::Id(_) => http::StatusCode::NOT_FOUND,
Error::NotFound => http::StatusCode::NOT_FOUND,
_ => http::StatusCode::INTERNAL_SERVER_ERROR,
}
}

View File

@ -21,14 +21,14 @@ use radicle::profile::Profile;
use crate::error::Error;
pub fn router(profile: Arc<Profile>) -> Router {
pub fn router(profile: Arc<Profile>, aliases: HashMap<String, Id>) -> Router {
Router::new()
.route("/:project/*request", any(git_handler))
.with_state(profile)
.with_state((profile, aliases))
}
async fn git_handler(
State(profile): State<Arc<Profile>>,
State((profile, aliases)): State<(Arc<Profile>, HashMap<String, Id>)>,
AxumPath((project, request)): AxumPath<(String, String)>,
method: Method,
headers: HeaderMap,
@ -37,10 +37,21 @@ async fn git_handler(
body: Bytes,
) -> impl IntoResponse {
let query = query.0.unwrap_or_default();
let id: Id = project.strip_suffix(".git").unwrap_or(&project).parse()?;
let name = project.strip_suffix(".git").unwrap_or(&project);
let rid: Id = match name.parse() {
Ok(rid) => rid,
Err(_) => {
let Some(rid) = aliases.get(name) else {
return Err(Error::NotFound);
};
*rid
}
};
let (status, headers, body) =
git_http_backend(&profile, method, headers, body, remote, id, &request, query).await?;
let (status, headers, body) = git_http_backend(
&profile, method, headers, body, remote, rid, &request, query,
)
.await?;
let mut response_headers = HeaderMap::new();
for (name, vec) in headers.iter() {
@ -195,22 +206,54 @@ async fn git_http_backend(
#[cfg(test)]
mod routes {
use std::collections::HashMap;
use std::net::SocketAddr;
use std::str::FromStr;
use axum::extract::connect_info::MockConnectInfo;
use axum::http::StatusCode;
use radicle::identity::Id;
use crate::test::{self, get};
use crate::test::{self, get, RID};
#[tokio::test]
async fn test_invalid_route_returns_404() {
let tmp = tempfile::tempdir().unwrap();
let ctx = test::seed(tmp.path());
let app = super::router(ctx.profile().to_owned())
let app = super::router(ctx.profile().to_owned(), HashMap::new())
.layer(MockConnectInfo(SocketAddr::from(([0, 0, 0, 0], 8080))));
let response = get(&app, "/aa/a").await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_info_request() {
let tmp = tempfile::tempdir().unwrap();
let ctx = test::seed(tmp.path());
let app = super::router(ctx.profile().to_owned(), HashMap::new())
.layer(MockConnectInfo(SocketAddr::from(([0, 0, 0, 0], 8080))));
let response = get(&app, format!("/{RID}.git/info/refs")).await;
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_aliases() {
let tmp = tempfile::tempdir().unwrap();
let ctx = test::seed(tmp.path());
let app = super::router(
ctx.profile().to_owned(),
HashMap::from_iter([(String::from("heartwood"), Id::from_str(RID).unwrap())]),
)
.layer(MockConnectInfo(SocketAddr::from(([0, 0, 0, 0], 8080))));
let response = get(&app, "/woodheart.git/info/refs").await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
let response = get(&app, "/heartwood.git/info/refs").await;
assert_eq!(response.status(), StatusCode::OK);
}
}

View File

@ -2,6 +2,7 @@
#![allow(clippy::too_many_arguments)]
pub mod error;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::process::Command;
use std::str;
@ -14,6 +15,7 @@ use axum::body::{Body, BoxBody, HttpBody};
use axum::http::{Request, Response};
use axum::middleware;
use axum::Router;
use radicle::identity::Id;
use tower_http::trace::TraceLayer;
use tracing_extra::{tracing_middleware, ColoredStatus, Paint, RequestId, TracingInfo};
@ -28,6 +30,7 @@ mod tracing_extra;
#[derive(Debug, Clone)]
pub struct Options {
pub aliases: HashMap<String, Id>,
pub listen: SocketAddr,
}
@ -48,7 +51,7 @@ pub async fn run(options: Options) -> anyhow::Result<()> {
let ctx = api::Context::new(profile.clone());
let api_router = api::router(ctx);
let git_router = git::router(profile.clone());
let git_router = git::router(profile.clone(), options.aliases);
let raw_router = raw::router(profile);
tracing::info!("listening on http://{}", options.listen);

View File

@ -1,5 +1,6 @@
use std::process;
use std::{collections::HashMap, process};
use radicle::prelude::Id;
use radicle_httpd as httpd;
use tracing::dispatcher::Dispatch;
@ -49,6 +50,7 @@ fn parse_options() -> Result<httpd::Options, lexopt::Error> {
let mut parser = lexopt::Parser::from_env();
let mut listen = None;
let mut aliases = HashMap::new();
while let Some(arg) = parser.next()? {
match arg {
@ -56,14 +58,21 @@ fn parse_options() -> Result<httpd::Options, lexopt::Error> {
let addr = parser.value()?.parse()?;
listen = Some(addr);
}
Long("alias") | Short('a') => {
let alias: String = parser.value()?.parse()?;
let id: Id = parser.value()?.parse()?;
aliases.insert(alias, id);
}
Long("help") => {
println!("usage: radicle-httpd [--listen <addr>]");
println!("usage: radicle-httpd [--listen <addr>] [--alias <name> <rid>]..");
process::exit(0);
}
_ => return Err(arg.unexpected()),
}
}
Ok(httpd::Options {
aliases,
listen: listen.unwrap_or_else(|| ([0, 0, 0, 0], 8080).into()),
})
}

View File

@ -23,6 +23,7 @@ use radicle_crypto::test::signer::MockSigner;
use crate::api::{auth, Context};
pub const RID: &str = "rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp";
pub const HEAD: &str = "1e978d19f251cd9821d9d9a76d1bd436bf0690d5";
pub const HEAD_1: &str = "f604ce9fd5b7cc77b7609beda45ea8760bee78f7";
pub const PATCH_ID: &str = "6ec9a764a888576abc7e582dbf82a31e23a9789d";