use crate::handlers;
use crate::proxy;
use crate::proxy::{GuestConnector, ProxyState};
use crate::trace::trace_id_middleware;
use arcbox_core::Runtime;
use axum::extract::OriginalUri;
use axum::{
Router, middleware,
routing::{delete, post},
};
use std::sync::Arc;
#[derive(Clone)]
pub struct AppState {
pub runtime: Arc<Runtime>,
pub proxy: Arc<ProxyState>,
}
pub fn create_router(runtime: Arc<Runtime>, connector: Arc<dyn GuestConnector>) -> Router {
router_with_proxy(runtime, Arc::new(ProxyState::new(connector)))
}
pub(crate) fn router_with_proxy(runtime: Arc<Runtime>, proxy: Arc<ProxyState>) -> Router {
let state = AppState { runtime, proxy };
api_routes()
.fallback(proxy::proxy_fallback)
.layer(middleware::from_fn(trace_id_middleware))
.with_state(state)
}
pub fn strip_api_version_prefix<B>(mut req: axum::http::Request<B>) -> axum::http::Request<B> {
let path = req.uri().path().to_owned();
if let Some(stripped) = strip_version_prefix(&path) {
let query = req.uri().query().map(str::to_owned);
let original_uri = req.uri().clone();
let new_pq = query
.as_ref()
.map_or_else(|| stripped.to_string(), |q| format!("{stripped}?{q}"));
req.extensions_mut().insert(OriginalUri(original_uri));
if let Ok(uri) = new_pq.parse() {
*req.uri_mut() = uri;
}
}
req
}
fn strip_version_prefix(path: &str) -> Option<&str> {
let after_v = path.strip_prefix("/v")?;
let dot = after_v.find('.')?;
let after_dot = &after_v[dot + 1..];
let slash = after_dot.find('/')?;
let major = &after_v[..dot];
let minor = &after_dot[..slash];
if !major.is_empty()
&& !minor.is_empty()
&& major.bytes().all(|b| b.is_ascii_digit())
&& minor.bytes().all(|b| b.is_ascii_digit())
{
Some(&after_dot[slash..])
} else {
None
}
}
fn api_routes() -> Router<AppState> {
Router::new()
.merge(container_routes())
.merge(network_routes())
.route("/build", post(handlers::build_image))
}
fn network_routes() -> Router<AppState> {
Router::new()
.route("/networks/{id}/connect", post(handlers::network_connect))
.route(
"/networks/{id}/disconnect",
post(handlers::network_disconnect),
)
}
fn container_routes() -> Router<AppState> {
Router::new()
.route("/containers/create", post(handlers::create_container))
.route("/containers/{id}/start", post(handlers::start_container))
.route("/containers/{id}/stop", post(handlers::stop_container))
.route(
"/containers/{id}/restart",
post(handlers::restart_container),
)
.route("/containers/{id}/kill", post(handlers::kill_container))
.route("/containers/{id}/rename", post(handlers::rename_container))
.route(
"/containers/{id}",
delete(handlers::remove_container).fallback(proxy::proxy_fallback),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_standard_version_prefix() {
assert_eq!(
strip_version_prefix("/v1.51/containers/abc/start"),
Some("/containers/abc/start")
);
assert_eq!(
strip_version_prefix("/v1.24/images/json"),
Some("/images/json")
);
assert_eq!(
strip_version_prefix("/v1.99/containers/abc/start"),
Some("/containers/abc/start")
);
}
#[test]
fn strip_preserves_unversioned_paths() {
assert_eq!(strip_version_prefix("/containers/abc/start"), None);
assert_eq!(strip_version_prefix("/_ping"), None);
assert_eq!(strip_version_prefix("/version"), None);
}
#[test]
fn strip_rejects_malformed_versions() {
assert_eq!(strip_version_prefix("/v1x/containers/json"), None);
assert_eq!(strip_version_prefix("/vabc.def/containers/json"), None);
assert_eq!(strip_version_prefix("/v/containers/json"), None);
assert_eq!(strip_version_prefix("/v1./containers/json"), None);
}
#[test]
fn strip_rejects_version_only_path() {
assert_eq!(strip_version_prefix("/v1.51"), None);
}
#[test]
fn strip_api_version_prefix_preserves_query_string() {
let req = axum::http::Request::builder()
.uri("/v1.51/containers/json?all=true&limit=10")
.body(())
.unwrap();
let req = strip_api_version_prefix(req);
assert_eq!(req.uri(), "/containers/json?all=true&limit=10");
}
#[test]
fn strip_api_version_prefix_sets_original_uri() {
let req = axum::http::Request::builder()
.uri("/v1.51/containers/abc/start")
.body(())
.unwrap();
let req = strip_api_version_prefix(req);
assert_eq!(req.uri().path(), "/containers/abc/start");
let original = req.extensions().get::<OriginalUri>().unwrap();
assert_eq!(original.0.path(), "/v1.51/containers/abc/start");
}
#[test]
fn strip_api_version_prefix_noop_for_unversioned() {
let req = axum::http::Request::builder()
.uri("/containers/abc/start")
.body(())
.unwrap();
let req = strip_api_version_prefix(req);
assert_eq!(req.uri().path(), "/containers/abc/start");
assert!(req.extensions().get::<OriginalUri>().is_none());
}
}