Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
httpd: Basic alias support for git
Alexis Sellier committed 3 years ago
commit 41e151266f0aada2212168a38cdeb99b297dcde4
parent 2b44ff1b55e371c419cf2076b7f97b4404b41a34
5 files changed +72 -11
modified 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,
        }
    }
modified radicle-httpd/src/git.rs
@@ -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);
+
    }
}
modified 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<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);
modified 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<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()),
    })
}
modified 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";