1use crate::error::Result;
32use crate::middleware::{Middleware, Next};
33use crate::request::Request;
34use crate::response::Response;
35use async_trait::async_trait;
36
37#[derive(Clone, Debug)]
39pub(crate) struct CspNonce(pub(crate) String);
40
41#[derive(Clone, Debug, Default)]
47pub struct Csp {
48 directives: Vec<(String, Vec<String>)>,
49 report_only: bool,
50}
51
52impl Csp {
53 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn api() -> Self {
60 Self::new()
61 .directive("default-src", ["'none'"])
62 .directive("frame-ancestors", ["'none'"])
63 }
64
65 pub fn web() -> Self {
68 Self::new()
69 .directive("default-src", ["'self'"])
70 .directive("base-uri", ["'self'"])
71 .directive("form-action", ["'self'"])
72 .directive("frame-ancestors", ["'none'"])
73 .directive("object-src", ["'none'"])
74 }
75
76 pub fn directive<I, S>(mut self, name: &str, values: I) -> Self
78 where
79 I: IntoIterator<Item = S>,
80 S: Into<String>,
81 {
82 let values: Vec<String> = values.into_iter().map(Into::into).collect();
83 match self.directives.iter_mut().find(|(n, _)| n == name) {
84 Some((_, v)) => *v = values,
85 None => self.directives.push((name.to_string(), values)),
86 }
87 self
88 }
89
90 pub fn script_src<I, S>(self, values: I) -> Self
91 where
92 I: IntoIterator<Item = S>,
93 S: Into<String>,
94 {
95 self.directive("script-src", values)
96 }
97
98 pub fn style_src<I, S>(self, values: I) -> Self
99 where
100 I: IntoIterator<Item = S>,
101 S: Into<String>,
102 {
103 self.directive("style-src", values)
104 }
105
106 pub fn img_src<I, S>(self, values: I) -> Self
107 where
108 I: IntoIterator<Item = S>,
109 S: Into<String>,
110 {
111 self.directive("img-src", values)
112 }
113
114 pub fn connect_src<I, S>(self, values: I) -> Self
115 where
116 I: IntoIterator<Item = S>,
117 S: Into<String>,
118 {
119 self.directive("connect-src", values)
120 }
121
122 pub fn report_uri(self, uri: &str) -> Self {
124 self.directive("report-uri", [uri])
125 }
126
127 pub fn report_only(mut self) -> Self {
130 self.report_only = true;
131 self
132 }
133
134 fn render(&self, nonce: &str) -> String {
135 self.directives
136 .iter()
137 .map(|(name, values)| {
138 if values.is_empty() {
139 name.clone()
140 } else {
141 format!("{} {}", name, values.join(" ").replace("{nonce}", nonce))
142 }
143 })
144 .collect::<Vec<_>>()
145 .join("; ")
146 }
147}
148
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151pub enum FrameOptions {
152 Deny,
153 SameOrigin,
154}
155
156#[derive(Clone, Debug)]
158pub struct SecurityHeaders {
159 csp: Option<Csp>,
160 frame_options: Option<FrameOptions>,
161 headers: Vec<(String, String)>,
162 compiled: std::sync::OnceLock<Compiled>,
163}
164
165#[derive(Clone, Debug)]
167struct Compiled {
168 fixed: Vec<(http::HeaderName, http::HeaderValue)>,
169 csp: Option<CompiledCsp>,
170}
171
172#[derive(Clone, Debug)]
173struct CompiledCsp {
174 name: http::HeaderName,
175 parts: Vec<String>,
177 fixed: Option<http::HeaderValue>,
179}
180
181impl Default for SecurityHeaders {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187impl SecurityHeaders {
188 pub fn new() -> Self {
189 Self {
190 csp: None,
191 compiled: std::sync::OnceLock::new(),
192 frame_options: Some(FrameOptions::Deny),
193 headers: vec![
194 ("x-content-type-options".into(), "nosniff".into()),
195 ("referrer-policy".into(), "no-referrer".into()),
196 ("cross-origin-opener-policy".into(), "same-origin".into()),
197 ("cross-origin-resource-policy".into(), "same-origin".into()),
198 (
199 "permissions-policy".into(),
200 "camera=(), microphone=(), geolocation=(), payment=()".into(),
201 ),
202 ],
203 }
204 }
205
206 pub fn csp(mut self, csp: Csp) -> Self {
208 self.csp = Some(csp);
209 self
210 }
211
212 pub fn frame_options(mut self, options: impl Into<Option<FrameOptions>>) -> Self {
215 self.frame_options = options.into();
216 self
217 }
218
219 pub fn header(mut self, name: &str, value: &str) -> Self {
221 let name = name.to_ascii_lowercase();
222 match self.headers.iter_mut().find(|(n, _)| *n == name) {
223 Some((_, v)) => *v = value.to_string(),
224 None => self.headers.push((name, value.to_string())),
225 }
226 self
227 }
228
229 pub fn without(mut self, name: &str) -> Self {
231 let name = name.to_ascii_lowercase();
232 self.headers.retain(|(n, _)| *n != name);
233 self
234 }
235}
236
237impl SecurityHeaders {
238 fn compile(&self) -> Compiled {
239 let mut fixed = Vec::new();
240 let mut push = |name: &str, value: &str| {
241 if let (Ok(n), Ok(v)) = (
242 http::HeaderName::from_bytes(name.as_bytes()),
243 http::HeaderValue::from_str(value),
244 ) {
245 fixed.push((n, v));
246 }
247 };
248 for (name, value) in &self.headers {
249 push(name, value);
250 }
251 match self.frame_options {
252 Some(FrameOptions::Deny) => push("x-frame-options", "DENY"),
253 Some(FrameOptions::SameOrigin) => push("x-frame-options", "SAMEORIGIN"),
254 None => {}
255 }
256 let csp = self.csp.as_ref().and_then(|csp| {
257 let name = if csp.report_only {
258 "content-security-policy-report-only"
259 } else {
260 "content-security-policy"
261 };
262 let text = csp.render("{nonce}");
263 let parts: Vec<String> = text.split("{nonce}").map(str::to_string).collect();
264 let fixed = (parts.len() == 1)
265 .then(|| http::HeaderValue::from_str(&text).ok())
266 .flatten();
267 Some(CompiledCsp {
268 name: http::HeaderName::from_bytes(name.as_bytes()).ok()?,
269 parts,
270 fixed,
271 })
272 });
273 Compiled { fixed, csp }
274 }
275}
276
277#[async_trait]
278impl Middleware for SecurityHeaders {
279 async fn handle(&self, mut req: Request, next: Next) -> Result<Response> {
280 let compiled = self.compiled.get_or_init(|| self.compile());
281 let mut nonce = None;
282 if compiled.csp.as_ref().is_some_and(|c| c.parts.len() > 1) {
283 let n = crate::security::random_token(16)?;
284 req.extensions.insert(CspNonce(n.clone()));
285 nonce = Some(n);
286 }
287 let mut res = next.run(req).await.unwrap_or_else(Response::from_error);
288
289 res.headers.reserve(compiled.fixed.len() + 1);
290 for (name, value) in &compiled.fixed {
291 if !res.headers.contains_key(name) {
292 res.headers.insert(name.clone(), value.clone());
293 }
294 }
295 if let Some(csp) = &compiled.csp {
296 if !res.headers.contains_key(&csp.name) {
297 let value = match (&csp.fixed, &nonce) {
298 (Some(v), _) => Some(v.clone()),
299 (None, Some(n)) => http::HeaderValue::from_str(&csp.parts.join(n)).ok(),
300 (None, None) => None,
301 };
302 if let Some(v) = value {
303 res.headers.insert(csp.name.clone(), v);
304 }
305 }
306 }
307 Ok(res)
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn renders_directives_and_nonce() {
317 let csp = Csp::web().script_src(["'self'", "'nonce-{nonce}'"]);
318 let text = csp.render("abc");
319 assert!(text.starts_with("default-src 'self'; "));
320 assert!(text.contains("script-src 'self' 'nonce-abc'"));
321 assert!(text.contains("frame-ancestors 'none'"));
322 }
323
324 #[test]
325 fn directive_replaces_instead_of_duplicating() {
326 let csp = Csp::api().directive("default-src", ["'self'"]);
327 assert_eq!(csp.render(""), "default-src 'self'; frame-ancestors 'none'");
328 }
329
330 #[test]
331 fn valueless_directive_renders_as_a_flag() {
332 let csp = Csp::new().directive("upgrade-insecure-requests", Vec::<String>::new());
333 assert_eq!(csp.render(""), "upgrade-insecure-requests");
334 }
335}