use http::header::{
CONTENT_SECURITY_POLICY, REFERRER_POLICY, STRICT_TRANSPORT_SECURITY, X_CONTENT_TYPE_OPTIONS,
X_FRAME_OPTIONS,
};
use http::{HeaderMap, HeaderName, HeaderValue};
use crate::error::Result;
use crate::middleware::{DuplicatePolicy, Middleware, Next, Request};
use crate::response::Response;
use crate::router::BoxFuture;
const DEFAULT_HSTS: &str = "max-age=31536000; includeSubDomains";
pub struct SecurityHeaders {
hsts: Option<HeaderValue>,
content_type_options: Option<HeaderValue>,
frame_options: Option<HeaderValue>,
referrer_policy: Option<HeaderValue>,
content_security_policy: Option<HeaderValue>,
}
impl SecurityHeaders {
pub fn new() -> Self {
Self {
hsts: Some(HeaderValue::from_static(DEFAULT_HSTS)),
content_type_options: Some(HeaderValue::from_static("nosniff")),
frame_options: Some(HeaderValue::from_static("DENY")),
referrer_policy: Some(HeaderValue::from_static("no-referrer")),
content_security_policy: None,
}
}
pub fn hsts(mut self, value: &'static str) -> Self {
self.hsts = Some(HeaderValue::from_static(value));
self
}
pub fn hsts_max_age(mut self, seconds: u64, include_subdomains: bool) -> Self {
let value = if include_subdomains {
format!("max-age={seconds}; includeSubDomains")
} else {
format!("max-age={seconds}")
};
self.hsts = HeaderValue::from_str(&value).ok();
self
}
pub fn without_hsts(mut self) -> Self {
self.hsts = None;
self
}
pub fn frame_options(mut self, value: &'static str) -> Self {
self.frame_options = Some(HeaderValue::from_static(value));
self
}
pub fn without_frame_options(mut self) -> Self {
self.frame_options = None;
self
}
pub fn referrer_policy(mut self, value: &'static str) -> Self {
self.referrer_policy = Some(HeaderValue::from_static(value));
self
}
pub fn without_referrer_policy(mut self) -> Self {
self.referrer_policy = None;
self
}
pub fn without_content_type_options(mut self) -> Self {
self.content_type_options = None;
self
}
pub fn content_security_policy(mut self, value: &'static str) -> Self {
self.content_security_policy = Some(HeaderValue::from_static(value));
self
}
}
impl Default for SecurityHeaders {
fn default() -> Self {
Self::new()
}
}
fn set_if_absent(headers: &mut HeaderMap, name: HeaderName, value: &Option<HeaderValue>) {
if let Some(value) = value {
if !headers.contains_key(&name) {
headers.insert(name, value.clone());
}
}
}
impl Middleware for SecurityHeaders {
fn handle(&self, request: Request, next: Next) -> BoxFuture<'static, Result<Response>> {
let hsts = self.hsts.clone();
let content_type_options = self.content_type_options.clone();
let frame_options = self.frame_options.clone();
let referrer_policy = self.referrer_policy.clone();
let content_security_policy = self.content_security_policy.clone();
Box::pin(async move {
let mut response = next.run(request).await?;
let headers = response.headers_mut();
set_if_absent(headers, STRICT_TRANSPORT_SECURITY, &hsts);
set_if_absent(headers, X_CONTENT_TYPE_OPTIONS, &content_type_options);
set_if_absent(headers, X_FRAME_OPTIONS, &frame_options);
set_if_absent(headers, REFERRER_POLICY, &referrer_policy);
set_if_absent(headers, CONTENT_SECURITY_POLICY, &content_security_policy);
Ok(response)
})
}
fn name(&self) -> &'static str {
"SecurityHeaders"
}
fn duplicate_policy(&self) -> DuplicatePolicy {
DuplicatePolicy::Reject
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_set_the_baseline_headers() {
let mw = SecurityHeaders::new();
assert_eq!(mw.hsts.as_ref().unwrap(), DEFAULT_HSTS);
assert_eq!(mw.frame_options.as_ref().unwrap(), "DENY");
assert_eq!(mw.content_type_options.as_ref().unwrap(), "nosniff");
assert_eq!(mw.referrer_policy.as_ref().unwrap(), "no-referrer");
assert!(mw.content_security_policy.is_none());
}
#[test]
fn builders_customize_and_disable_headers() {
let mw = SecurityHeaders::new()
.hsts_max_age(60, false)
.frame_options("SAMEORIGIN")
.content_security_policy("default-src 'self'")
.without_referrer_policy();
assert_eq!(mw.hsts.as_ref().unwrap(), "max-age=60");
assert_eq!(mw.frame_options.as_ref().unwrap(), "SAMEORIGIN");
assert_eq!(
mw.content_security_policy.as_ref().unwrap(),
"default-src 'self'"
);
assert!(mw.referrer_policy.is_none());
}
}