use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Instant;
use axum::extract::{Request, State};
use axum::http::{header, HeaderName, HeaderValue, Method, StatusCode};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use crate::config::LogLevel;
use crate::error::ApiError;
use crate::log::REQUEST_ID;
use crate::routes;
use crate::tenants::{valid_tenant, AppState};
#[derive(Clone)]
struct Served {
key_id: String,
}
fn valid_request_id(id: &str) -> bool {
(1..=64).contains(&id.len())
&& id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
}
fn method_label(m: &Method) -> &'static str {
match *m {
Method::GET => "GET",
Method::POST => "POST",
Method::PUT => "PUT",
Method::DELETE => "DELETE",
Method::OPTIONS => "OPTIONS",
Method::HEAD => "HEAD",
_ => "OTHER",
}
}
struct InFlight<'a>(&'a AppState);
impl<'a> InFlight<'a> {
fn new(state: &'a AppState) -> Self {
state.metrics.in_flight.fetch_add(1, Ordering::Relaxed);
Self(state)
}
}
impl Drop for InFlight<'_> {
fn drop(&mut self) {
self.0.metrics.in_flight.fetch_sub(1, Ordering::Relaxed);
}
}
pub async fn observe(State(state): State<Arc<AppState>>, req: Request, next: Next) -> Response {
let id = req
.headers()
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.filter(|v| valid_request_id(v))
.map_or_else(|| format!("{:016x}", rand::random::<u64>()), str::to_string);
let method = method_label(req.method());
let route = routes::route_label(req.uri().path());
let tenant = req
.headers()
.get("x-tenant-id")
.and_then(|v| v.to_str().ok())
.filter(|v| valid_tenant(v))
.map(str::to_string);
let bytes_in = req
.headers()
.get(header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok());
let started = Instant::now();
let in_flight = InFlight::new(&state);
let mut response = REQUEST_ID
.scope(id.clone(), async { next.run(req).await })
.await;
drop(in_flight);
let elapsed = started.elapsed();
let status = response.status().as_u16();
state
.metrics
.observe_request(route, method, status, elapsed);
if let Ok(v) = HeaderValue::from_str(&id) {
response.headers_mut().insert("x-request-id", v);
}
let level = if matches!(route, "/healthz" | "/readyz" | "/metrics") && status < 500 {
LogLevel::Debug
} else if status >= 500 && status != 503 {
LogLevel::Error
} else if status >= 400 {
LogLevel::Warn
} else {
LogLevel::Info
};
let key = response
.extensions()
.get::<Served>()
.map(|s| s.key_id.clone());
REQUEST_ID
.scope(id, async {
crate::log::emit(
level,
"request",
&[
("method", method.into()),
("route", route.into()),
("status", status.into()),
("latency_ms", (elapsed.as_secs_f64() * 1000.0).into()),
("tenant", tenant.into()),
("key_id", key.into()),
("bytes_in", bytes_in.into()),
],
);
})
.await;
response
}
const ALLOWED_HEADERS: &str =
"authorization, content-type, x-tenant-id, idempotency-key, if-none-match, x-request-id";
const EXPOSED_HEADERS: &str =
"etag, retry-after, x-request-id, idempotent-replayed, www-authenticate";
pub async fn cors(State(state): State<Arc<AppState>>, req: Request, next: Next) -> Response {
let origins = &state.cfg.cors_origins;
if origins.is_empty() {
return next.run(req).await;
}
let origin = req
.headers()
.get(header::ORIGIN)
.and_then(|v| v.to_str().ok())
.map(str::to_string);
let allowed = origin
.as_deref()
.filter(|o| origins.iter().any(|a| a == "*" || a == o));
let preflight = req.method() == Method::OPTIONS
&& req.headers().contains_key("access-control-request-method");
let mut response = if preflight {
let mut r = StatusCode::NO_CONTENT.into_response();
if allowed.is_some() {
let h = r.headers_mut();
h.insert(
header::ACCESS_CONTROL_ALLOW_METHODS,
HeaderValue::from_static("GET, POST, PUT, DELETE, OPTIONS"),
);
h.insert(
header::ACCESS_CONTROL_ALLOW_HEADERS,
HeaderValue::from_static(ALLOWED_HEADERS),
);
h.insert(
header::ACCESS_CONTROL_MAX_AGE,
HeaderValue::from(state.cfg.cors_max_age_seconds),
);
}
r
} else {
next.run(req).await
};
let h = response.headers_mut();
h.append(header::VARY, HeaderValue::from_static("Origin"));
if let Some(origin) = allowed {
let value = if origins.iter().any(|a| a == "*") {
HeaderValue::from_static("*")
} else {
HeaderValue::from_str(origin).unwrap_or_else(|_| HeaderValue::from_static("null"))
};
h.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, value);
h.insert(
HeaderName::from_static("access-control-expose-headers"),
HeaderValue::from_static(EXPOSED_HEADERS),
);
}
response
}
fn open_request_allowed(state: &AppState, req: &Request) -> bool {
let origins = &state.cfg.cors_origins;
if let Some(origin) = req.headers().get(header::ORIGIN) {
let listed = origin
.to_str()
.is_ok_and(|o| origins.iter().any(|a| a == "*" || a == o));
if !listed {
return false;
}
}
if state.cfg.unix_socket.is_some() {
return true;
}
let host = req
.headers()
.get(header::HOST)
.and_then(|v| v.to_str().ok())
.or_else(|| req.uri().authority().map(|a| a.as_str()));
host.is_none_or(loopback_host)
}
fn loopback_host(host: &str) -> bool {
let name = match host.strip_prefix('[') {
Some(rest) => rest.split_once(']').map_or(rest, |(ip, _)| ip),
None => host.rsplit_once(':').map_or(host, |(name, _)| name),
};
let name = name.trim_end_matches('.').to_ascii_lowercase();
name == "localhost"
|| name.ends_with(".localhost")
|| name
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| ip.is_loopback())
}
pub async fn auth(State(state): State<Arc<AppState>>, mut req: Request, next: Next) -> Response {
let path = req.uri().path();
let public =
routes::is_public(req.method(), path) || (state.cfg.metrics_public && path == "/metrics");
if public {
return next.run(req).await;
}
if state.auth.is_open() && !open_request_allowed(&state, &req) {
state.metrics.auth_failures.fetch_add(1, Ordering::Relaxed);
return ApiError::new(
StatusCode::FORBIDDEN,
"forbidden",
"this server runs without authentication and serves only same-machine clients: \
the request's Origin is not in --cors-origins or its Host is not a loopback name",
)
.into_response();
}
match state.auth.authenticate(req.headers()) {
Ok(principal) => {
let served = Served {
key_id: principal.key_id.clone(),
};
req.extensions_mut().insert(Arc::new(principal));
let mut response = next.run(req).await;
response.extensions_mut().insert(served);
response
}
Err(error) => {
state.metrics.auth_failures.fetch_add(1, Ordering::Relaxed);
error.into_response()
}
}
}
pub async fn not_found() -> ApiError {
ApiError::not_found("no such route")
}
pub async fn method_not_allowed() -> ApiError {
ApiError::new(
StatusCode::METHOD_NOT_ALLOWED,
"method_not_allowed",
"method not allowed",
)
}