use crate::error::{Error, Result};
use crate::policy::*;
#[derive(Debug, Clone)]
pub struct SecurityHeaders {
pub(crate) content_security_policy: Option<ContentSecurityPolicy>,
pub(crate) strict_transport_security: Option<StrictTransportSecurity>,
pub(crate) x_frame_options: Option<XFrameOptions>,
pub(crate) x_content_type_options: bool,
pub(crate) referrer_policy: Option<ReferrerPolicy>,
pub(crate) cross_origin_opener_policy: Option<CrossOriginOpenerPolicy>,
pub(crate) cross_origin_embedder_policy: Option<CrossOriginEmbedderPolicy>,
pub(crate) cross_origin_resource_policy: Option<CrossOriginResourcePolicy>,
}
impl SecurityHeaders {
pub fn builder() -> SecurityHeadersBuilder {
SecurityHeadersBuilder::default()
}
pub fn content_security_policy(&self) -> Option<&ContentSecurityPolicy> {
self.content_security_policy.as_ref()
}
pub fn strict_transport_security(&self) -> Option<&StrictTransportSecurity> {
self.strict_transport_security.as_ref()
}
pub fn x_frame_options(&self) -> Option<XFrameOptions> {
self.x_frame_options
}
pub fn x_content_type_options_enabled(&self) -> bool {
self.x_content_type_options
}
pub fn referrer_policy(&self) -> Option<ReferrerPolicy> {
self.referrer_policy
}
pub fn cross_origin_opener_policy(&self) -> Option<CrossOriginOpenerPolicy> {
self.cross_origin_opener_policy
}
pub fn cross_origin_embedder_policy(&self) -> Option<CrossOriginEmbedderPolicy> {
self.cross_origin_embedder_policy
}
pub fn cross_origin_resource_policy(&self) -> Option<CrossOriginResourcePolicy> {
self.cross_origin_resource_policy
}
}
#[derive(Debug, Default)]
pub struct SecurityHeadersBuilder {
content_security_policy: Option<ContentSecurityPolicy>,
strict_transport_security: Option<StrictTransportSecurity>,
x_frame_options: Option<XFrameOptions>,
x_content_type_options: bool,
referrer_policy: Option<ReferrerPolicy>,
cross_origin_opener_policy: Option<CrossOriginOpenerPolicy>,
cross_origin_embedder_policy: Option<CrossOriginEmbedderPolicy>,
cross_origin_resource_policy: Option<CrossOriginResourcePolicy>,
}
impl SecurityHeadersBuilder {
pub fn content_security_policy(mut self, policy: ContentSecurityPolicy) -> Self {
self.content_security_policy = Some(policy);
self
}
pub fn strict_transport_security(
mut self,
max_age: std::time::Duration,
include_subdomains: bool,
preload: bool,
) -> Self {
let mut hsts = StrictTransportSecurity::new(max_age);
if include_subdomains {
hsts = hsts.include_subdomains(true);
}
if preload {
hsts = hsts.preload(true);
}
self.strict_transport_security = Some(hsts);
self
}
pub fn strict_transport_security_policy(mut self, policy: StrictTransportSecurity) -> Self {
self.strict_transport_security = Some(policy);
self
}
pub fn x_frame_options_deny(mut self) -> Self {
self.x_frame_options = Some(XFrameOptions::Deny);
self
}
pub fn x_frame_options_sameorigin(mut self) -> Self {
self.x_frame_options = Some(XFrameOptions::SameOrigin);
self
}
pub fn x_frame_options(mut self, policy: XFrameOptions) -> Self {
self.x_frame_options = Some(policy);
self
}
pub fn x_content_type_options_nosniff(mut self) -> Self {
self.x_content_type_options = true;
self
}
pub fn referrer_policy(mut self, policy: ReferrerPolicy) -> Self {
self.referrer_policy = Some(policy);
self
}
pub fn referrer_policy_no_referrer(mut self) -> Self {
self.referrer_policy = Some(ReferrerPolicy::NoReferrer);
self
}
pub fn referrer_policy_strict_origin_when_cross_origin(mut self) -> Self {
self.referrer_policy = Some(ReferrerPolicy::StrictOriginWhenCrossOrigin);
self
}
pub fn cross_origin_opener_policy(mut self, policy: CrossOriginOpenerPolicy) -> Self {
self.cross_origin_opener_policy = Some(policy);
self
}
pub fn cross_origin_embedder_policy(mut self, policy: CrossOriginEmbedderPolicy) -> Self {
self.cross_origin_embedder_policy = Some(policy);
self
}
pub fn cross_origin_resource_policy(mut self, policy: CrossOriginResourcePolicy) -> Self {
self.cross_origin_resource_policy = Some(policy);
self
}
pub fn build(self) -> Result<SecurityHeaders> {
if let Some(csp) = &self.content_security_policy {
csp.to_header_value()?;
}
if let Some(hsts) = &self.strict_transport_security {
hsts.to_header_value()?;
}
if self.content_security_policy.is_none()
&& self.strict_transport_security.is_none()
&& self.x_frame_options.is_none()
&& !self.x_content_type_options
&& self.referrer_policy.is_none()
&& self.cross_origin_opener_policy.is_none()
&& self.cross_origin_embedder_policy.is_none()
&& self.cross_origin_resource_policy.is_none()
{
return Err(Error::ValidationFailed(
"At least one security header must be configured".to_string(),
));
}
Ok(SecurityHeaders {
content_security_policy: self.content_security_policy,
strict_transport_security: self.strict_transport_security,
x_frame_options: self.x_frame_options,
x_content_type_options: self.x_content_type_options,
referrer_policy: self.referrer_policy,
cross_origin_opener_policy: self.cross_origin_opener_policy,
cross_origin_embedder_policy: self.cross_origin_embedder_policy,
cross_origin_resource_policy: self.cross_origin_resource_policy,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_builder_empty_fails() {
let result = SecurityHeaders::builder().build();
assert!(result.is_err());
}
#[test]
fn test_builder_with_hsts() {
let headers = SecurityHeaders::builder()
.strict_transport_security(Duration::from_secs(31536000), true, false)
.build()
.unwrap();
assert!(headers.strict_transport_security().is_some());
let hsts = headers.strict_transport_security().unwrap();
assert_eq!(hsts.max_age(), Duration::from_secs(31536000));
assert!(hsts.includes_subdomains());
assert!(!hsts.is_preload());
}
#[test]
fn test_builder_with_frame_options() {
let headers = SecurityHeaders::builder()
.x_frame_options_deny()
.build()
.unwrap();
assert_eq!(headers.x_frame_options(), Some(XFrameOptions::Deny));
}
#[test]
fn test_builder_with_referrer_policy() {
let headers = SecurityHeaders::builder()
.referrer_policy_no_referrer()
.build()
.unwrap();
assert_eq!(headers.referrer_policy(), Some(ReferrerPolicy::NoReferrer));
}
#[test]
fn test_builder_with_multiple_headers() {
let csp = ContentSecurityPolicy::new().default_src(vec!["'self'"]);
let headers = SecurityHeaders::builder()
.content_security_policy(csp)
.strict_transport_security(Duration::from_secs(31536000), true, false)
.x_frame_options_deny()
.x_content_type_options_nosniff()
.referrer_policy_no_referrer()
.cross_origin_opener_policy(CrossOriginOpenerPolicy::SameOrigin)
.cross_origin_embedder_policy(CrossOriginEmbedderPolicy::RequireCorp)
.cross_origin_resource_policy(CrossOriginResourcePolicy::SameOrigin)
.build()
.unwrap();
assert!(headers.content_security_policy().is_some());
assert!(headers.strict_transport_security().is_some());
assert!(headers.x_frame_options().is_some());
assert!(headers.x_content_type_options_enabled());
assert!(headers.referrer_policy().is_some());
assert!(headers.cross_origin_opener_policy().is_some());
assert!(headers.cross_origin_embedder_policy().is_some());
assert!(headers.cross_origin_resource_policy().is_some());
}
#[test]
fn test_builder_with_empty_csp_fails() {
let result = SecurityHeaders::builder()
.content_security_policy(ContentSecurityPolicy::new())
.build();
assert!(result.is_err());
}
}