From 6e90444406fb86d90883a7d2dd6c1ed4d38f7855 Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Sun, 12 Mar 2023 18:13:17 +0100 Subject: [PATCH] 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` --- radicle-httpd/src/error.rs | 5 ++++ radicle-httpd/src/git.rs | 59 ++++++++++++++++++++++++++++++++------ radicle-httpd/src/lib.rs | 5 +++- radicle-httpd/src/main.rs | 13 +++++++-- radicle-httpd/src/test.rs | 1 + 5 files changed, 72 insertions(+), 11 deletions(-) diff --git a/radicle-httpd/src/error.rs b/radicle-httpd/src/error.rs index 96572bc8..29ada51d 100644 --- a/radicle-httpd/src/error.rs +++ b/radicle-httpd/src/error.rs @@ -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, } } diff --git a/radicle-httpd/src/git.rs b/radicle-httpd/src/git.rs index 0785f096..14042838 100644 --- a/radicle-httpd/src/git.rs +++ b/radicle-httpd/src/git.rs @@ -21,14 +21,14 @@ use radicle::profile::Profile; use crate::error::Error; -pub fn router(profile: Arc) -> Router { +pub fn router(profile: Arc, aliases: HashMap) -> Router { Router::new() .route("/:project/*request", any(git_handler)) - .with_state(profile) + .with_state((profile, aliases)) } async fn git_handler( - State(profile): State>, + State((profile, aliases)): State<(Arc, HashMap)>, 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); + } } diff --git a/radicle-httpd/src/lib.rs b/radicle-httpd/src/lib.rs index 59a25293..650bbf82 100644 --- a/radicle-httpd/src/lib.rs +++ b/radicle-httpd/src/lib.rs @@ -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, 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); diff --git a/radicle-httpd/src/main.rs b/radicle-httpd/src/main.rs index 4dac4b78..2d4c0759 100644 --- a/radicle-httpd/src/main.rs +++ b/radicle-httpd/src/main.rs @@ -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 { 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 { 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 ]"); + println!("usage: radicle-httpd [--listen ] [--alias ].."); process::exit(0); } _ => return Err(arg.unexpected()), } } Ok(httpd::Options { + aliases, listen: listen.unwrap_or_else(|| ([0, 0, 0, 0], 8080).into()), }) } diff --git a/radicle-httpd/src/test.rs b/radicle-httpd/src/test.rs index 5ba34246..13d95b63 100644 --- a/radicle-httpd/src/test.rs +++ b/radicle-httpd/src/test.rs @@ -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";