use crate::config::ConfigError;
use crate::core::{ApiMetadata, Registration};
use crate::define_registration;
use axum::body::Body;
use axum::routing::MethodRouter;
use axum::Router;
use uuid::Uuid;
pub mod response;
pub mod security_headers;
pub mod version_routing;
pub use response::{build_fallback_response, build_json_response};
pub use security_headers::SecurityHeaders;
pub use version_routing::{
build_version_router, version_redirect_middleware, VersionRouterConfig, VersionedRoute,
};
#[cfg(feature = "ratelimit")]
pub use crate::security::ratelimit::{LimiteronAdapter, RateLimitLayer, RateLimiter};
#[cfg(feature = "ratelimit")]
pub fn rate_limit_layer(limiter: std::sync::Arc<dyn RateLimiter>) -> RateLimitLayer {
RateLimitLayer::new(limiter)
}
pub(crate) const X_REQUEST_ID: &str = "x-request-id";
pub(crate) fn get_or_generate_request_id(req: &axum::http::Request<Body>) -> String {
req.headers()
.get(X_REQUEST_ID)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
.unwrap_or_else(|| Uuid::new_v4().to_string())
}
#[derive(Debug, Clone)]
pub struct HttpRoute {
path: String,
handler: MethodRouter,
metadata: ApiMetadata,
module_prefix: Option<String>,
}
impl HttpRoute {
#[allow(missing_docs)]
pub fn new(
path: String,
handler: MethodRouter,
metadata: ApiMetadata,
module_prefix: Option<String>,
) -> Self {
Self {
path,
handler,
metadata,
module_prefix,
}
}
pub fn path(&self) -> &str {
&self.path
}
pub fn handler(&self) -> &MethodRouter {
&self.handler
}
pub fn metadata(&self) -> &ApiMetadata {
&self.metadata
}
pub fn module_prefix(&self) -> Option<&str> {
self.module_prefix.as_deref()
}
}
define_registration!(RouteRegistration, HttpRoute, ApiMetadata);
inventory::collect!(HttpRoute);
fn resolve_route_path(base_path: &str, module_prefix: Option<&str>) -> String {
match module_prefix {
Some(prefix) if !prefix.is_empty() => {
let clean_prefix = prefix.trim_start_matches('/');
format!("/{}/{}", clean_prefix, &base_path[1..])
}
_ => base_path.to_string(),
}
}
fn apply_security_headers(router: Router) -> Router {
SecurityHeaders::default().apply(router)
}
#[cfg(feature = "mcp")]
#[inline(never)]
fn preserve_mcp_inventory() {
let _count = inventory::iter::<crate::mcp::McpToolRegistration>().count();
let _ = _count; }
#[cfg(feature = "websocket")]
#[inline(never)]
fn preserve_websocket_inventory() {
let _count = inventory::iter::<crate::websocket::WebSocketRoute>().count();
let _ = _count;
}
#[cfg(feature = "grpc")]
#[inline(never)]
fn preserve_grpc_inventory() {
let _count = inventory::iter::<crate::grpc::GrpcRouteRegistration>().count();
let _ = _count;
}
pub fn build() -> Router {
#[cfg(feature = "mcp")]
preserve_mcp_inventory();
#[cfg(feature = "websocket")]
preserve_websocket_inventory();
#[cfg(feature = "grpc")]
preserve_grpc_inventory();
let mut router = Router::new();
let registrations: Vec<_> = inventory::iter::<RouteRegistration>().collect();
let mut routes: Vec<_> = registrations
.iter()
.map(|registration| registration.create())
.collect();
for route in inventory::iter::<HttpRoute>() {
routes.push(route.clone());
}
let mut seen: std::collections::HashMap<String, HttpRoute> =
std::collections::HashMap::with_capacity(routes.len());
for route in routes {
let prefix = route.module_prefix.as_deref();
let full_path = resolve_route_path(&route.path, prefix);
seen.insert(full_path, route);
}
let mut deduped: Vec<_> = seen.into_iter().collect();
deduped.sort_by(|a, b| a.0.cmp(&b.0));
for (full_path, route) in deduped {
router = router.route(&full_path, route.handler);
}
router
}
pub fn build_with_redirect() -> Router {
let router = build();
router.layer(axum::middleware::from_fn(version_redirect_middleware))
}
pub fn build_with_config(config: &crate::config::AppConfig) -> Result<Router, ConfigError> {
#[cfg(feature = "security")]
use std::sync::Arc;
const DEFAULT_BODY_LIMIT: usize = 10 * 1024 * 1024;
let mut router = build();
router = router.layer(axum::middleware::from_fn(
|mut req: axum::http::Request<Body>, next: axum::middleware::Next| async move {
let request_id = get_or_generate_request_id(&req);
let header_value = axum::http::HeaderValue::from_str(&request_id)
.unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid-request-id"));
req.headers_mut().insert(
axum::http::header::HeaderName::from_static(X_REQUEST_ID),
header_value.clone(),
);
let mut response = next.run(req).await;
response.headers_mut().insert(
axum::http::header::HeaderName::from_static(X_REQUEST_ID),
header_value,
);
response
},
));
router = router.layer(tower_http::limit::RequestBodyLimitLayer::new(
DEFAULT_BODY_LIMIT,
));
router = router.layer(tower_http::compression::CompressionLayer::new());
let timeout_secs = config.server.request_timeout_secs;
router = router.layer(tower_http::timeout::TimeoutLayer::with_status_code(
axum::http::StatusCode::REQUEST_TIMEOUT,
std::time::Duration::from_secs(timeout_secs),
));
if let Some(cors) = &config.server.cors {
let cors_layer = crate::config::build_cors_layer(cors)?;
router = router.layer(cors_layer);
}
router = apply_security_headers(router);
#[cfg(feature = "security")]
{
use crate::config::AuthConfig;
use crate::security::{auth_middleware, AppApiKeyAuth, AuthContext, AuthError, BearerAuth};
use axum::http::HeaderValue;
let auth_config = &config.authentication;
if let AuthConfig::ApiKey {
header_name,
prefix,
} = auth_config
{
let auth = Arc::new(AppApiKeyAuth::new());
let auth_clone = auth.clone();
let header_name = header_name.clone();
let prefix = prefix.clone();
let extract_auth =
move |req: &axum::http::Request<Body>| -> Result<AuthContext, AuthError> {
let header_value = match req
.headers()
.get(&header_name)
.and_then(|v: &HeaderValue| v.to_str().ok())
{
Some(value) => value,
None => return Err(AuthError::MissingAuth),
};
let client_ip = req
.headers()
.get("x-forwarded-for")
.or_else(|| req.headers().get("x-real-ip"))
.and_then(|v: &HeaderValue| v.to_str().ok())
.unwrap_or("unknown")
.to_string();
if prefix.is_empty() {
return Err(AuthError::MissingAuth);
}
if header_value.starts_with(&prefix) {
let key = &header_value[prefix.len()..];
if let Some(permissions) = auth.validate_key(key, &client_ip) {
Ok(AuthContext {
user_id: Some(AppApiKeyAuth::key_id(key)),
permissions,
metadata: crate::security::AuthMetadata::default(),
})
} else {
Err(AuthError::MissingAuth)
}
} else {
Err(AuthError::MissingAuth)
}
};
let middleware = auth_middleware(auth_clone, extract_auth);
router = router.layer(axum::middleware::from_fn(middleware));
} else if let AuthConfig::Jwt { secret, .. } = auth_config {
let auth = Arc::new(BearerAuth::new(secret));
let auth_clone = auth.clone();
let extract_auth =
move |req: &axum::http::Request<Body>| -> Result<AuthContext, AuthError> {
let header_value = match req
.headers()
.get("authorization")
.and_then(|v: &HeaderValue| v.to_str().ok())
{
Some(value) => value,
None => return Err(AuthError::MissingAuth),
};
let token = match header_value.strip_prefix("Bearer ") {
Some(token) if !token.is_empty() => token,
_ => return Err(AuthError::InvalidToken),
};
if let Some(context) = auth.validate_token(token) {
Ok(context)
} else {
Err(AuthError::InvalidToken)
}
};
let middleware = auth_middleware(auth_clone, extract_auth);
router = router.layer(axum::middleware::from_fn(middleware));
}
}
Ok(router)
}
#[cfg(test)]
mod tests;