use std::sync::OnceLock;
use axum::{extract::Request, http::HeaderValue, middleware::Next, response::Response};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameOptions {
Deny,
SameOrigin,
Disabled,
}
#[derive(Debug, Clone)]
pub struct SecurityConfig {
pub hsts_max_age: u64,
pub hsts_include_subdomains: bool,
pub hsts_preload: bool,
pub frame_options: FrameOptions,
pub referrer_policy: &'static str,
pub permissions_policy: Option<&'static str>,
pub csp_default: &'static str,
pub csp_docs_override: Option<&'static str>,
pub nosniff: bool,
pub xss_protection: bool,
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
hsts_max_age: 31_536_000,
hsts_include_subdomains: true,
hsts_preload: true,
frame_options: FrameOptions::Deny,
referrer_policy: "strict-origin-when-cross-origin",
permissions_policy: Some(
"camera=(), microphone=(), geolocation=(), payment=(), usb=()",
),
csp_default: "default-src 'self'",
csp_docs_override: Some(concat!(
"default-src 'self'; ",
"script-src 'self' 'unsafe-inline' https://unpkg.com; ",
"style-src 'self' 'unsafe-inline' https://unpkg.com; ",
"img-src 'self' data: https://unpkg.com; ",
"font-src 'self' data: https://unpkg.com; ",
"connect-src 'self'",
)),
nosniff: true,
xss_protection: true,
}
}
}
static CONFIG: OnceLock<SecurityConfig> = OnceLock::new();
pub fn configure(cfg: SecurityConfig) {
let _ = CONFIG.set(cfg);
}
#[inline]
fn cfg() -> &'static SecurityConfig {
CONFIG.get_or_init(SecurityConfig::default)
}
pub async fn apply_security_headers(req: Request, next: Next) -> Response {
let is_docs = req.uri().path() == "/docs";
let mut resp = next.run(req).await;
let c = cfg();
let h = resp.headers_mut();
if c.hsts_max_age > 0 {
let mut val = format!("max-age={}", c.hsts_max_age);
if c.hsts_include_subdomains {
val.push_str("; includeSubDomains");
}
if c.hsts_preload {
val.push_str("; preload");
}
if let Ok(v) = HeaderValue::from_str(&val) {
h.insert("Strict-Transport-Security", v);
}
}
match c.frame_options {
FrameOptions::Deny => {
h.insert("X-Frame-Options", HeaderValue::from_static("DENY"));
}
FrameOptions::SameOrigin => {
h.insert("X-Frame-Options", HeaderValue::from_static("SAMEORIGIN"));
}
FrameOptions::Disabled => {}
}
if c.nosniff {
h.insert(
"X-Content-Type-Options",
HeaderValue::from_static("nosniff"),
);
}
if c.xss_protection {
h.insert(
"X-XSS-Protection",
HeaderValue::from_static("1; mode=block"),
);
}
h.insert(
"Referrer-Policy",
HeaderValue::from_static(c.referrer_policy),
);
if let Some(pp) = c.permissions_policy {
h.insert("Permissions-Policy", HeaderValue::from_static(pp));
}
let csp = if is_docs {
c.csp_docs_override.unwrap_or(c.csp_default)
} else {
c.csp_default
};
h.insert("Content-Security-Policy", HeaderValue::from_static(csp));
resp
}