1#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct SecurityHeaders {
20 pub(crate) csp: Option<Box<str>>,
21 pub(crate) frame: Option<Box<str>>,
22 pub(crate) referrer: Option<Box<str>>,
23 pub(crate) permissions: Option<Box<str>>,
24 pub(crate) coop: Option<Box<str>>,
25 pub(crate) hsts: Option<Box<str>>,
26}
27
28pub const DEFAULT_CSP: &str = "default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'";
30
31impl Default for SecurityHeaders {
32 fn default() -> Self {
33 Self {
34 csp: Some(DEFAULT_CSP.into()),
35 frame: Some("DENY".into()),
36 referrer: Some("strict-origin-when-cross-origin".into()),
37 permissions: Some("camera=(), microphone=(), geolocation=(), payment=()".into()),
38 coop: Some("same-origin".into()),
39 hsts: Some("max-age=31536000".into()),
40 }
41 }
42}
43
44impl SecurityHeaders {
45 #[must_use]
48 pub fn none() -> Self {
49 Self {
50 csp: None,
51 frame: None,
52 referrer: None,
53 permissions: None,
54 coop: None,
55 hsts: None,
56 }
57 }
58
59 #[must_use]
61 pub fn content_security_policy(mut self, value: &str) -> Self {
62 self.csp = Some(value.into());
63 self
64 }
65
66 #[must_use]
68 pub fn no_content_security_policy(mut self) -> Self {
69 self.csp = None;
70 self
71 }
72
73 #[must_use]
76 pub fn frame_options(mut self, value: &str) -> Self {
77 self.frame = Some(value.into());
78 self
79 }
80
81 #[must_use]
83 pub fn referrer_policy(mut self, value: &str) -> Self {
84 self.referrer = Some(value.into());
85 self
86 }
87
88 #[must_use]
90 pub fn permissions_policy(mut self, value: &str) -> Self {
91 self.permissions = Some(value.into());
92 self
93 }
94
95 #[must_use]
97 pub fn opener_policy(mut self, value: &str) -> Self {
98 self.coop = Some(value.into());
99 self
100 }
101
102 #[must_use]
106 pub fn hsts(mut self, max_age: u64, include_subdomains: bool) -> Self {
107 let mut v = format!("max-age={max_age}");
108 if include_subdomains {
109 v.push_str("; includeSubDomains");
110 }
111 self.hsts = Some(v.into());
112 self
113 }
114
115 #[must_use]
117 pub fn no_hsts(mut self) -> Self {
118 self.hsts = None;
119 self
120 }
121
122 pub(crate) fn fields(&self, https: bool) -> impl Iterator<Item = (&'static str, &str)> {
124 [
125 ("content-security-policy", self.csp.as_deref()),
126 ("x-frame-options", self.frame.as_deref()),
127 ("referrer-policy", self.referrer.as_deref()),
128 ("permissions-policy", self.permissions.as_deref()),
129 ("cross-origin-opener-policy", self.coop.as_deref()),
130 (
131 "strict-transport-security",
132 self.hsts.as_deref().filter(|_| https),
133 ),
134 ]
135 .into_iter()
136 .filter_map(|(n, v)| v.map(|v| (n, v)))
137 }
138
139 pub(crate) fn valid(&self) -> bool {
141 self.fields(true)
142 .all(|(_, v)| !v.is_empty() && v.bytes().all(|b| (0x20..0x7f).contains(&b)))
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn test_hsts_only_on_https() {
152 let p = SecurityHeaders::default();
153 assert!(
154 p.fields(false)
155 .all(|(n, _)| n != "strict-transport-security")
156 );
157 assert!(
158 p.fields(true)
159 .any(|(n, _)| n == "strict-transport-security")
160 );
161 assert_eq!(SecurityHeaders::none().fields(true).count(), 0);
162 assert!(p.valid());
163 assert!(!SecurityHeaders::default().referrer_policy("a\nb").valid());
164 }
165}