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:
parent
166f7e7cc2
commit
6e90444406
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Reference in New Issue