use super::types::AppState;
use crate::config::Config;
use axum::extract::{ConnectInfo, Request, State};
use axum::http::{HeaderValue, Method, StatusCode};
use axum::middleware::Next;
use axum::response::Response;
use std::net::SocketAddr;
use subtle::ConstantTimeEq;
use tower_http::cors::{Any, CorsLayer};
fn token_log_repr(token: &str) -> String {
let prefix: String = token.chars().take(4).collect();
format!("{}…(len={})", prefix, token.len())
}
fn record_auth_failure(state: &AppState, req: &Request, presented: Option<&str>, scope: &str) {
let peer = req
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|c| c.0.to_string())
.unwrap_or_else(|| "?".to_string());
let path = req.uri().path();
let token_repr = presented
.map(token_log_repr)
.unwrap_or_else(|| "<none>".to_string());
tracing::warn!(
peer = %peer,
path = %path,
scope = %scope,
token = %token_repr,
"auth failure"
);
state.metrics.record_auth_failure();
}
fn tokens_match(allowed: &[String], presented: &str) -> bool {
let presented_bytes = presented.as_bytes();
let mut found = subtle::Choice::from(0u8);
for t in allowed {
let t_bytes = t.as_bytes();
if t_bytes.len() != presented_bytes.len() {
continue;
}
found |= t_bytes.ct_eq(presented_bytes);
}
bool::from(found)
}
pub(super) async fn require_token(
State(state): State<AppState>,
req: Request,
next: Next,
) -> Result<Response, StatusCode> {
if state.tokens.is_empty() {
return Ok(next.run(req).await);
}
let Some(presented) = extract_token(&req) else {
record_auth_failure(&state, &req, None, "user");
return Err(StatusCode::UNAUTHORIZED);
};
if tokens_match(&state.tokens, &presented) {
Ok(next.run(req).await)
} else {
record_auth_failure(&state, &req, Some(&presented), "user");
Err(StatusCode::UNAUTHORIZED)
}
}
pub(super) async fn require_admin_token(
State(state): State<AppState>,
req: Request,
next: Next,
) -> Result<Response, StatusCode> {
if state.admin_tokens.is_empty() {
return Err(StatusCode::SERVICE_UNAVAILABLE);
}
let Some(presented) = extract_token(&req) else {
record_auth_failure(&state, &req, None, "admin");
return Err(StatusCode::UNAUTHORIZED);
};
if tokens_match(&state.admin_tokens, &presented) {
Ok(next.run(req).await)
} else {
record_auth_failure(&state, &req, Some(&presented), "admin");
Err(StatusCode::UNAUTHORIZED)
}
}
fn extract_token(req: &Request) -> Option<String> {
if let Some(v) = req.headers().get("x-api-key").and_then(|v| v.to_str().ok()) {
let s = v.trim();
if !s.is_empty() {
return Some(s.to_string());
}
}
let auth = req
.headers()
.get("authorization")
.and_then(|v| v.to_str().ok())?;
let stripped = auth.strip_prefix("Bearer ")?;
Some(stripped.to_string())
}
pub(super) fn build_cors(cfg: &Config) -> CorsLayer {
let methods = [Method::GET, Method::POST, Method::OPTIONS];
if cfg.server.cors_origins.is_empty() {
CorsLayer::new()
.allow_origin(Any)
.allow_methods(methods)
.allow_headers(Any)
} else {
let origins: Vec<HeaderValue> = cfg
.server
.cors_origins
.iter()
.filter_map(|s| s.parse().ok())
.collect();
CorsLayer::new()
.allow_origin(origins)
.allow_methods(methods)
.allow_headers(Any)
}
}