use axum::http::{header::*, HeaderName, HeaderValue};
use axum::Router;
use tower_http::set_header::SetResponseHeaderLayer;
use crate::config::defaults::security_headers as defaults;
#[derive(Debug, Clone)]
pub struct SecurityHeaders {
pub content_type_options: &'static str,
pub frame_options: &'static str,
pub xss_protection: &'static str,
pub cache_control: &'static str,
pub content_security_policy: &'static str,
pub strict_transport_security: &'static str,
pub referrer_policy: &'static str,
pub permissions_policy: &'static str,
}
impl Default for SecurityHeaders {
fn default() -> Self {
Self::strict()
}
}
impl SecurityHeaders {
pub fn strict() -> Self {
Self {
content_type_options: defaults::CONTENT_TYPE_OPTIONS,
frame_options: defaults::FRAME_OPTIONS,
xss_protection: defaults::XSS_PROTECTION,
cache_control: defaults::CACHE_CONTROL,
content_security_policy: defaults::CONTENT_SECURITY_POLICY,
strict_transport_security: defaults::STRICT_TRANSPORT_SECURITY,
referrer_policy: defaults::REFERRER_POLICY,
permissions_policy: defaults::PERMISSIONS_POLICY,
}
}
pub fn relaxed() -> Self {
Self {
content_type_options: defaults::CONTENT_TYPE_OPTIONS,
frame_options: "SAMEORIGIN",
xss_protection: defaults::XSS_PROTECTION,
cache_control: "no-cache",
content_security_policy: "default-src 'self' 'unsafe-inline' 'unsafe-eval'; script-src 'self' 'unsafe-inline'",
strict_transport_security: "max-age=86400",
referrer_policy: "no-referrer-when-downgrade",
permissions_policy: defaults::PERMISSIONS_POLICY,
}
}
pub fn apply(&self, router: Router) -> Router {
let mut router = router;
router = router.layer(SetResponseHeaderLayer::overriding(
X_CONTENT_TYPE_OPTIONS,
HeaderValue::from_static(self.content_type_options),
));
router = router.layer(SetResponseHeaderLayer::overriding(
X_FRAME_OPTIONS,
HeaderValue::from_static(self.frame_options),
));
router = router.layer(SetResponseHeaderLayer::overriding(
X_XSS_PROTECTION,
HeaderValue::from_static(self.xss_protection),
));
router = router.layer(SetResponseHeaderLayer::overriding(
CACHE_CONTROL,
HeaderValue::from_static(self.cache_control),
));
router = router.layer(SetResponseHeaderLayer::overriding(
CONTENT_SECURITY_POLICY,
HeaderValue::from_static(self.content_security_policy),
));
router = router.layer(SetResponseHeaderLayer::overriding(
STRICT_TRANSPORT_SECURITY,
HeaderValue::from_static(self.strict_transport_security),
));
router = router.layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("referrer-policy"),
HeaderValue::from_static(self.referrer_policy),
));
router = router.layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("permissions-policy"),
HeaderValue::from_static(self.permissions_policy),
));
router
}
pub fn validate(&self) -> Result<(), String> {
if !["nosniff"].contains(&self.content_type_options) {
return Err(format!(
"Invalid X-Content-Type-Options value: {}. Expected 'nosniff'",
self.content_type_options
));
}
if !["DENY", "SAMEORIGIN"].contains(&self.frame_options) {
return Err(format!(
"Invalid X-Frame-Options value: {}. Expected 'DENY' or 'SAMEORIGIN'",
self.frame_options
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_security_headers_default() {
let headers = SecurityHeaders::default();
assert_eq!(headers.content_type_options, "nosniff");
assert_eq!(headers.frame_options, "DENY");
}
#[test]
fn test_security_headers_strict() {
let headers = SecurityHeaders::strict();
assert_eq!(headers.content_type_options, "nosniff");
assert_eq!(headers.frame_options, "DENY");
}
#[test]
fn test_security_headers_relaxed() {
let headers = SecurityHeaders::relaxed();
assert_eq!(headers.frame_options, "SAMEORIGIN");
}
#[test]
fn test_security_headers_validate() {
let headers = SecurityHeaders::strict();
assert!(headers.validate().is_ok());
}
#[test]
fn test_security_headers_apply() {
let headers = SecurityHeaders::default();
let router = Router::new();
let _ = headers.apply(router);
}
#[test]
fn test_security_headers_validate_invalid_content_type() {
let headers = SecurityHeaders {
content_type_options: "invalid",
..SecurityHeaders::strict()
};
let result = headers.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Invalid X-Content-Type-Options"));
}
#[test]
fn test_security_headers_validate_invalid_frame_options() {
let headers = SecurityHeaders {
frame_options: "invalid",
..SecurityHeaders::strict()
};
let result = headers.validate();
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid X-Frame-Options"));
}
}