httpd: Add axum-listener and use axum for unix-sockets directly

This commit is contained in:
Sebastian Martinez
2025-12-23 12:33:43 +01:00
parent 97ad0a865e
commit 4d062d8347
6 changed files with 126 additions and 165 deletions
+69 -3
View File
@@ -211,6 +211,18 @@ dependencies = [
"tower-service",
]
[[package]]
name = "axum-listener"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0cd6458a1428aec99bace599e09140ecd60479f81a33f866ec628b2f5d3a793"
dependencies = [
"axum",
"futures",
"tokio",
"tracing",
]
[[package]]
name = "backtrace"
version = "0.3.73"
@@ -837,6 +849,21 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
@@ -844,6 +871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@@ -852,6 +880,40 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-executor"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.30"
@@ -864,10 +926,16 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
@@ -2022,11 +2090,11 @@ version = "0.20.0"
dependencies = [
"anyhow",
"axum",
"axum-listener",
"base64 0.22.1",
"chrono",
"flate2",
"hyper",
"hyper-util",
"infer",
"lexopt",
"lru",
@@ -2042,7 +2110,6 @@ dependencies = [
"tempfile",
"thiserror 2.0.14",
"tokio",
"tower 0.4.13",
"tower 0.5.2",
"tower-http",
"tracing",
@@ -2830,7 +2897,6 @@ dependencies = [
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
+2 -3
View File
@@ -23,11 +23,11 @@ path = "src/main.rs"
[dependencies]
anyhow = { version = "1" }
axum = { version = "0.8.4", default-features = false, features = ["json", "query", "tokio", "http1"] }
axum-listener = { version = "0.2.0" }
base64 = { version = "0.22.1" }
chrono = { version = "0.4.41", default-features = false, features = ["clock"] }
flate2 = { version = "1" }
hyper = { version = "1.6.0", default-features = false, features = ["server", "http1"] }
hyper-util = { version = "0.1.7", features = ["tokio", "server", "service"] }
hyper = { version = "1.6.0", default-features = false }
infer = { version = "0.19.0" }
lexopt = { version = "0.3.1" }
lru = { version = "0.16.0" }
@@ -40,7 +40,6 @@ serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
thiserror = { version = "2" }
tokio = { version = "1.47.1", default-features = false, features = ["macros", "rt-multi-thread"] }
tower = { version = "0.4.13", features = ["util"] }
tower-http = { version = "0.6.6", default-features = false, features = ["trace", "cors", "set-header"] }
tracing = { version = "0.1.41", default-features = false, features = ["std", "log"] }
tracing-logfmt = { version = "0.3.5", optional = true }
+27 -131
View File
@@ -4,26 +4,22 @@
pub mod error;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::process::Command;
use std::str;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Context as _;
use axum::body::{Body, HttpBody};
use axum::http::{Request, Response};
use axum::body::Body;
use axum::http::Request;
use axum::response::IntoResponse;
use axum::routing::get;
use axum::{middleware, Json, Router};
use hyper::header::CONTENT_TYPE;
use axum_listener::{DualAddr, DualListener};
use hyper::body::Body as _;
use hyper::header::{CONTENT_TYPE, FORWARDED};
use hyper::Method;
use hyper_util::rt::TokioIo;
use hyper_util::service::TowerToHyperService;
use tokio::net::{TcpListener, UnixListener};
use tower::Service;
use tower_http::cors;
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
@@ -32,9 +28,8 @@ use tracing::Span;
use radicle::identity::RepoId;
use radicle::Profile;
use tracing_extra::{tracing_middleware, ColoredStatus, Paint, RequestId, TracingInfo};
use crate::api::RADICLE_VERSION;
use crate::tracing_extra::{tracing_middleware, ColoredStatus, Paint, RequestId, TracingInfo};
mod api;
mod axum_extra;
@@ -48,25 +43,10 @@ mod tracing_extra;
/// Default cache HTTP size.
pub const DEFAULT_CACHE_SIZE: NonZeroUsize = NonZeroUsize::new(100).unwrap();
#[derive(Debug, Clone)]
pub enum ListenAddress {
Tcp(SocketAddr),
Unix(PathBuf),
}
impl std::fmt::Display for ListenAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ListenAddress::Tcp(addr) => write!(f, "http://{}", addr),
ListenAddress::Unix(path) => write!(f, "unix://{}", path.display()),
}
}
}
#[derive(Debug, Clone)]
pub struct Options {
pub aliases: HashMap<String, RepoId>,
pub listen: ListenAddress,
pub listen: DualAddr,
pub cache: Option<NonZeroUsize>,
}
@@ -80,47 +60,34 @@ pub async fn run(options: Options) -> anyhow::Result<()> {
tracing::info!("{}", str::from_utf8(&git_version)?.trim());
match &options.listen {
ListenAddress::Tcp(addr) => {
let listener = TcpListener::bind(addr).await?;
tracing::info!("listening on {}", options.listen);
run_tcp_server(listener, options).await
}
ListenAddress::Unix(path) => {
// Remove existing socket file if it exists
if path.exists() {
std::fs::remove_file(path)?;
}
let listener = UnixListener::bind(path)?;
tracing::info!("listening on {}", options.listen);
run_unix_server(listener, options).await
}
}
}
let listener = DualListener::bind(&options.listen).await?;
tracing::info!("listening on {:?}", &options.listen);
async fn run_tcp_server(listener: TcpListener, options: Options) -> anyhow::Result<()> {
let profile = Profile::load()?;
let request_id = RequestId::new();
tracing::info!("using radicle home at {}", profile.home().path().display());
let app =
router(options, profile)?
let app = router(options, profile)?
.layer(middleware::from_fn(tracing_middleware))
.layer(
TraceLayer::new_for_http()
.make_span_with(move |_request: &Request<Body>| {
tracing::info_span!("request", id = %request_id.clone().next())
.make_span_with(move |request: &Request<Body>| {
if let Some(forwarded) = request.headers().get("X-Forwarded-For").and_then(|s| s.to_str().ok()) {
tracing::info_span!("request", id = %request_id.clone().next(), "X-Forwarded-For" = forwarded)
} else {
tracing::info_span!("request", id = %request_id.clone().next())
}
})
.on_response(
|response: &Response<Body>, latency: Duration, _span: &Span| {
|response: &hyper::Response<Body>, latency: Duration, _span: &Span| {
if let Some(info) = response.extensions().get::<TracingInfo>() {
let client_addr = info.connect_info
.map(|ci| ci.0.to_string())
.unwrap_or_else(|| "unknown".to_string());
tracing::info!(
"{} \"{} {} {:?}\" {} {:?} {}",
client_addr,
match info.connect_info.0 {
DualAddr::Tcp(c) => c.to_string(),
DualAddr::Uds(_) => "unix-socket".into()
},
info.method,
info.uri,
info.version,
@@ -140,86 +107,14 @@ async fn run_tcp_server(listener: TcpListener, options: Options) -> anyhow::Resu
tracing::info!("Processed");
}
},
),
)
.into_make_service_with_connect_info::<SocketAddr>();
)
).into_make_service_with_connect_info::<DualAddr>();
axum::serve(listener, app)
.await
.map_err(anyhow::Error::from)
}
async fn run_unix_server(listener: UnixListener, options: Options) -> anyhow::Result<()> {
let profile = Profile::load()?;
let request_id = RequestId::new();
tracing::info!("using radicle home at {}", profile.home().path().display());
let app =
router(options, profile)?
.layer(middleware::from_fn(tracing_middleware))
.layer(
TraceLayer::new_for_http()
.make_span_with(move |_request: &Request<Body>| {
tracing::info_span!("request", id = %request_id.clone().next())
})
.on_response(
|response: &Response<Body>, latency: Duration, _span: &Span| {
tracing::info!(
"unix-socket \"{} {} {:?}\" {} {:?} {}",
response.extensions().get::<TracingInfo>()
.map(|info| info.method.as_str())
.unwrap_or("UNKNOWN"),
response.extensions().get::<TracingInfo>()
.map(|info| info.uri.path())
.unwrap_or("/"),
response.extensions().get::<TracingInfo>()
.map(|info| info.version)
.unwrap_or(hyper::Version::HTTP_11),
ColoredStatus(response.status()),
latency,
Paint::dim(
response
.body()
.size_hint()
.exact()
.map(|n| n.to_string())
.unwrap_or("0".to_string())
.into()
),
);
},
),
)
.into_make_service();
// Manual Unix socket serving since axum doesn't directly support Unix listeners
loop {
let (stream, _) = listener.accept().await?;
let mut app_service = app.clone();
tokio::spawn(async move {
let stream = TokioIo::new(stream);
// Create a service from the service factory for this connection
match app_service.call(()).await {
Ok(service) => {
let hyper_service = TowerToHyperService::new(service);
if let Err(err) = hyper::server::conn::http1::Builder::new()
.serve_connection(stream, hyper_service)
.await
{
tracing::error!("Error serving Unix socket connection: {}", err);
}
}
Err(err) => {
tracing::error!("Failed to create service: {:?}", err);
}
}
});
}
}
/// Create a router consisting of other sub-routers.
fn router(options: Options, profile: Profile) -> anyhow::Result<Router> {
let profile = Arc::new(profile);
@@ -315,8 +210,9 @@ mod routes {
use axum::extract::connect_info::MockConnectInfo;
use axum::http::StatusCode;
use axum_listener::DualAddr;
use crate::test::{self, get};
use crate::test;
#[tokio::test]
async fn test_invalid_route_returns_404() {
@@ -324,7 +220,7 @@ mod routes {
let app = super::router(
super::Options {
aliases: HashMap::new(),
listen: super::ListenAddress::Tcp(SocketAddr::from(([0, 0, 0, 0], 8080))),
listen: DualAddr::Tcp(SocketAddr::from(([0, 0, 0, 0], 8080))),
cache: None,
},
test::profile(tmp.path(), [0xff; 32]),
@@ -332,7 +228,7 @@ mod routes {
.unwrap()
.layer(MockConnectInfo(SocketAddr::from(([0, 0, 0, 0], 8080))));
let response = get(&app, "/aa/a").await;
let response = test::get(&app, "/aa/a").await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
+22 -23
View File
@@ -1,7 +1,7 @@
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::{collections::HashMap, process};
use axum_listener::DualAddr;
use radicle::prelude::RepoId;
use radicle::version::Version;
use radicle_httpd as httpd;
@@ -32,13 +32,13 @@ Options
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let options = parse_options()?;
// SAFETY: The logger is only initialized once.
httpd::logger::init().unwrap();
tracing::info!("starting http daemon..");
tracing::info!("version {} ({})", env!("RADICLE_VERSION"), env!("GIT_HEAD"));
let options = parse_options()?;
match httpd::run(options).await {
Ok(()) => {}
Err(err) => {
@@ -61,9 +61,23 @@ fn parse_options() -> Result<httpd::Options, lexopt::Error> {
while let Some(arg) = parser.next()? {
match arg {
Long("listen") => {
let value = parser.value()?;
let addr = parse_listen_address(&value.to_string_lossy())
.map_err(|e| lexopt::Error::from(e.to_string()))?;
let addr: DualAddr = parser.value()?.parse()?;
// Get socket path and remove it if existing
if let DualAddr::Uds(socket_path) = &addr {
if let Some(path) = socket_path.as_pathname() {
if path.exists() {
tracing::info!("Removing existing socket path at {}", path.display());
if let Err(e) = std::fs::remove_file(path) {
tracing::error!("{e}");
}
}
} else {
tracing::error!("Provided socket address isn't a valid path.");
process::exit(0);
}
}
listen = Some(addr);
}
Long("alias") | Short('a') => {
@@ -74,7 +88,7 @@ fn parse_options() -> Result<httpd::Options, lexopt::Error> {
}
Long("version") | Short('v') => {
if let Err(e) = VERSION.write(std::io::stdout()) {
eprintln!("error: {e}");
tracing::error!("{e}");
process::exit(1);
};
process::exit(0);
@@ -92,22 +106,7 @@ fn parse_options() -> Result<httpd::Options, lexopt::Error> {
}
Ok(httpd::Options {
aliases,
listen: listen.unwrap_or_else(|| httpd::ListenAddress::Tcp(([0, 0, 0, 0], 8080).into())),
listen: listen.unwrap_or_else(|| DualAddr::Tcp(([0, 0, 0, 0], 8080).into())),
cache,
})
}
fn parse_listen_address(value: &str) -> Result<httpd::ListenAddress, Box<dyn std::error::Error>> {
// Check if it's a Unix socket path (contains '/' and no ':' port separator)
if value.contains('/') && !value.contains(':') {
Ok(httpd::ListenAddress::Unix(PathBuf::from(value)))
} else if value.starts_with("unix:") {
// Support explicit unix: prefix
let path = value.strip_prefix("unix:").unwrap();
Ok(httpd::ListenAddress::Unix(PathBuf::from(path)))
} else {
// Try to parse as TCP address
let addr = value.parse()?;
Ok(httpd::ListenAddress::Tcp(addr))
}
}
+1 -1
View File
@@ -326,7 +326,7 @@ fn seed_with_signer<G: Signer<Signature>>(
let options = crate::Options {
aliases: std::collections::HashMap::new(),
listen: std::net::SocketAddr::from(([0, 0, 0, 0], 8080)),
listen: axum_listener::DualAddr::Tcp(std::net::SocketAddr::from(([0, 0, 0, 0], 8080))),
cache: Some(crate::DEFAULT_CACHE_SIZE),
};
+5 -4
View File
@@ -1,5 +1,4 @@
use std::fmt;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
@@ -9,6 +8,7 @@ use axum::http::Request;
use axum::middleware::Next;
use axum::response::IntoResponse;
use axum::Extension;
use axum_listener::DualAddr;
use hyper::{Method, StatusCode, Uri, Version};
pub use radicle_term::ansi::Paint;
@@ -28,7 +28,7 @@ impl RequestId {
#[derive(Clone)]
pub struct TracingInfo {
pub connect_info: Option<ConnectInfo<SocketAddr>>,
pub connect_info: ConnectInfo<DualAddr>,
pub method: Method,
pub version: Version,
pub uri: Uri,
@@ -50,8 +50,9 @@ impl fmt::Display for ColoredStatus {
pub async fn tracing_middleware(request: Request<Body>, next: Next) -> impl IntoResponse {
let connect_info = request
.extensions()
.get::<ConnectInfo<std::net::SocketAddr>>()
.copied();
.get::<ConnectInfo<DualAddr>>()
.unwrap()
.clone();
let method = request.method().clone();
let version = request.version();