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 uses_nonce(&self) -> bool {
135 self.directives
136 .iter()
137 .any(|(_, v)| v.iter().any(|s| s.contains("{nonce}")))
138 }
139
140 fn render(&self, nonce: &str) -> String {
141 self.directives
142 .iter()
143 .map(|(name, values)| {
144 if values.is_empty() {
145 name.clone()
146 } else {
147 format!("{} {}", name, values.join(" ").replace("{nonce}", nonce))
148 }
149 })
150 .collect::<Vec<_>>()
151 .join("; ")
152 }
153}
154
155#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157pub enum FrameOptions {
158 Deny,
159 SameOrigin,
160}
161
162#[derive(Clone, Debug)]
164pub struct SecurityHeaders {
165 csp: Option<Csp>,
166 frame_options: Option<FrameOptions>,
167 headers: Vec<(String, String)>,
168}
169
170impl Default for SecurityHeaders {
171 fn default() -> Self {
172 Self::new()
173 }
174}
175
176impl SecurityHeaders {
177 pub fn new() -> Self {
178 Self {
179 csp: None,
180 frame_options: Some(FrameOptions::Deny),
181 headers: vec![
182 ("x-content-type-options".into(), "nosniff".into()),
183 ("referrer-policy".into(), "no-referrer".into()),
184 ("cross-origin-opener-policy".into(), "same-origin".into()),
185 ("cross-origin-resource-policy".into(), "same-origin".into()),
186 (
187 "permissions-policy".into(),
188 "camera=(), microphone=(), geolocation=(), payment=()".into(),
189 ),
190 ],
191 }
192 }
193
194 pub fn csp(mut self, csp: Csp) -> Self {
196 self.csp = Some(csp);
197 self
198 }
199
200 pub fn frame_options(mut self, options: impl Into<Option<FrameOptions>>) -> Self {
203 self.frame_options = options.into();
204 self
205 }
206
207 pub fn header(mut self, name: &str, value: &str) -> Self {
209 let name = name.to_ascii_lowercase();
210 match self.headers.iter_mut().find(|(n, _)| *n == name) {
211 Some((_, v)) => *v = value.to_string(),
212 None => self.headers.push((name, value.to_string())),
213 }
214 self
215 }
216
217 pub fn without(mut self, name: &str) -> Self {
219 let name = name.to_ascii_lowercase();
220 self.headers.retain(|(n, _)| *n != name);
221 self
222 }
223}
224
225#[async_trait]
226impl Middleware for SecurityHeaders {
227 async fn handle(&self, mut req: Request, next: Next) -> Result<Response> {
228 let mut nonce = String::new();
229 if self.csp.as_ref().is_some_and(Csp::uses_nonce) {
230 nonce = crate::security::random_token(16)?;
231 req.extensions.insert(CspNonce(nonce.clone()));
232 }
233 let mut res = next.run(req).await.unwrap_or_else(Response::from_error);
234
235 let put = |res: &mut Response, name: &str, value: String| {
236 if !res.headers.contains_key(name) {
237 if let (Ok(n), Ok(v)) = (
238 http::HeaderName::from_bytes(name.as_bytes()),
239 http::HeaderValue::from_str(&value),
240 ) {
241 res.headers.insert(n, v);
242 }
243 }
244 };
245 for (name, value) in &self.headers {
246 put(&mut res, name, value.clone());
247 }
248 match self.frame_options {
249 Some(FrameOptions::Deny) => put(&mut res, "x-frame-options", "DENY".into()),
250 Some(FrameOptions::SameOrigin) => put(&mut res, "x-frame-options", "SAMEORIGIN".into()),
251 None => {}
252 }
253 if let Some(csp) = &self.csp {
254 let name = if csp.report_only {
255 "content-security-policy-report-only"
256 } else {
257 "content-security-policy"
258 };
259 put(&mut res, name, csp.render(&nonce));
260 }
261 Ok(res)
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn renders_directives_and_nonce() {
271 let csp = Csp::web().script_src(["'self'", "'nonce-{nonce}'"]);
272 assert!(csp.uses_nonce());
273 let text = csp.render("abc");
274 assert!(text.starts_with("default-src 'self'; "));
275 assert!(text.contains("script-src 'self' 'nonce-abc'"));
276 assert!(text.contains("frame-ancestors 'none'"));
277 }
278
279 #[test]
280 fn directive_replaces_instead_of_duplicating() {
281 let csp = Csp::api().directive("default-src", ["'self'"]);
282 assert_eq!(csp.render(""), "default-src 'self'; frame-ancestors 'none'");
283 }
284
285 #[test]
286 fn valueless_directive_renders_as_a_flag() {
287 let csp = Csp::new().directive("upgrade-insecure-requests", Vec::<String>::new());
288 assert_eq!(csp.render(""), "upgrade-insecure-requests");
289 }
290}