http_security_headers/policy/
csp.rs1use crate::error::{Error, Result};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ContentSecurityPolicy {
24 directives: HashMap<String, Vec<String>>,
25}
26
27impl ContentSecurityPolicy {
28 pub fn new() -> Self {
30 Self {
31 directives: HashMap::new(),
32 }
33 }
34
35 pub fn default_src<I, S>(mut self, sources: I) -> Self
39 where
40 I: IntoIterator<Item = S>,
41 S: Into<String>,
42 {
43 self.set_directive("default-src", sources);
44 self
45 }
46
47 pub fn script_src<I, S>(mut self, sources: I) -> Self
51 where
52 I: IntoIterator<Item = S>,
53 S: Into<String>,
54 {
55 self.set_directive("script-src", sources);
56 self
57 }
58
59 pub fn style_src<I, S>(mut self, sources: I) -> Self
63 where
64 I: IntoIterator<Item = S>,
65 S: Into<String>,
66 {
67 self.set_directive("style-src", sources);
68 self
69 }
70
71 pub fn img_src<I, S>(mut self, sources: I) -> Self
75 where
76 I: IntoIterator<Item = S>,
77 S: Into<String>,
78 {
79 self.set_directive("img-src", sources);
80 self
81 }
82
83 pub fn font_src<I, S>(mut self, sources: I) -> Self
87 where
88 I: IntoIterator<Item = S>,
89 S: Into<String>,
90 {
91 self.set_directive("font-src", sources);
92 self
93 }
94
95 pub fn connect_src<I, S>(mut self, sources: I) -> Self
99 where
100 I: IntoIterator<Item = S>,
101 S: Into<String>,
102 {
103 self.set_directive("connect-src", sources);
104 self
105 }
106
107 pub fn object_src<I, S>(mut self, sources: I) -> Self
111 where
112 I: IntoIterator<Item = S>,
113 S: Into<String>,
114 {
115 self.set_directive("object-src", sources);
116 self
117 }
118
119 pub fn frame_src<I, S>(mut self, sources: I) -> Self
123 where
124 I: IntoIterator<Item = S>,
125 S: Into<String>,
126 {
127 self.set_directive("frame-src", sources);
128 self
129 }
130
131 pub fn base_uri<I, S>(mut self, sources: I) -> Self
135 where
136 I: IntoIterator<Item = S>,
137 S: Into<String>,
138 {
139 self.set_directive("base-uri", sources);
140 self
141 }
142
143 pub fn form_action<I, S>(mut self, sources: I) -> Self
147 where
148 I: IntoIterator<Item = S>,
149 S: Into<String>,
150 {
151 self.set_directive("form-action", sources);
152 self
153 }
154
155 pub fn frame_ancestors<I, S>(mut self, sources: I) -> Self
159 where
160 I: IntoIterator<Item = S>,
161 S: Into<String>,
162 {
163 self.set_directive("frame-ancestors", sources);
164 self
165 }
166
167 pub fn upgrade_insecure_requests(mut self) -> Self {
171 self.directives
172 .insert("upgrade-insecure-requests".to_string(), vec![]);
173 self
174 }
175
176 pub fn block_all_mixed_content(mut self) -> Self {
180 self.directives
181 .insert("block-all-mixed-content".to_string(), vec![]);
182 self
183 }
184
185 pub fn directive<I, S>(mut self, name: &str, sources: I) -> Self
189 where
190 I: IntoIterator<Item = S>,
191 S: Into<String>,
192 {
193 self.set_directive(name, sources);
194 self
195 }
196
197 fn set_directive<I, S>(&mut self, name: &str, sources: I)
199 where
200 I: IntoIterator<Item = S>,
201 S: Into<String>,
202 {
203 let sources: Vec<String> = sources.into_iter().map(|s| s.into()).collect();
204 self.directives.insert(name.to_string(), sources);
205 }
206
207 pub fn to_header_value(&self) -> Result<String> {
209 if self.directives.is_empty() {
210 return Err(Error::InvalidCsp("CSP policy is empty".to_string()));
211 }
212
213 let mut parts = Vec::new();
214
215 for (directive, sources) in &self.directives {
216 if sources.is_empty() {
217 parts.push(directive.clone());
219 } else {
220 parts.push(format!("{} {}", directive, sources.join(" ")));
221 }
222 }
223
224 Ok(parts.join("; "))
225 }
226
227 pub fn parse(value: &str) -> Result<Self> {
237 let mut csp = Self::new();
238
239 for directive_str in value.split(';').map(|s| s.trim()) {
240 if directive_str.is_empty() {
241 continue;
242 }
243
244 let parts: Vec<&str> = directive_str.split_whitespace().collect();
245 if parts.is_empty() {
246 continue;
247 }
248
249 let directive_name = parts[0];
250 let sources: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect();
251
252 csp.directives.insert(directive_name.to_string(), sources);
253 }
254
255 if csp.directives.is_empty() {
256 return Err(Error::InvalidCsp("No directives found".to_string()));
257 }
258
259 Ok(csp)
260 }
261}
262
263impl Default for ContentSecurityPolicy {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269impl std::fmt::Display for ContentSecurityPolicy {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 write!(f, "{}", self.to_header_value().unwrap_or_default())
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_new() {
281 let csp = ContentSecurityPolicy::new();
282 assert!(csp.directives.is_empty());
283 }
284
285 #[test]
286 fn test_builder() {
287 let csp = ContentSecurityPolicy::new()
288 .default_src(vec!["'self'"])
289 .script_src(vec!["'self'", "'unsafe-inline'"])
290 .style_src(vec!["'self'", "https://fonts.googleapis.com"]);
291
292 assert_eq!(csp.directives.len(), 3);
293 assert_eq!(csp.directives.get("default-src").unwrap(), &vec!["'self'"]);
294 assert_eq!(
295 csp.directives.get("script-src").unwrap(),
296 &vec!["'self'", "'unsafe-inline'"]
297 );
298 }
299
300 #[test]
301 fn test_to_header_value() {
302 let csp = ContentSecurityPolicy::new()
303 .default_src(vec!["'self'"])
304 .script_src(vec!["'self'", "'unsafe-inline'"]);
305
306 let header = csp.to_header_value().unwrap();
307 assert!(header.contains("default-src 'self'"));
308 assert!(header.contains("script-src 'self' 'unsafe-inline'"));
309 }
310
311 #[test]
312 fn test_valueless_directives() {
313 let csp = ContentSecurityPolicy::new()
314 .default_src(vec!["'self'"])
315 .upgrade_insecure_requests();
316
317 let header = csp.to_header_value().unwrap();
318 assert!(header.contains("upgrade-insecure-requests"));
319 assert!(header.contains("default-src 'self'"));
320 }
321
322 #[test]
323 fn test_empty_policy_error() {
324 let csp = ContentSecurityPolicy::new();
325 assert!(csp.to_header_value().is_err());
326 }
327
328 #[test]
329 fn test_parse() {
330 let csp =
331 ContentSecurityPolicy::parse("default-src 'self'; script-src 'unsafe-inline'")
332 .unwrap();
333
334 assert_eq!(csp.directives.len(), 2);
335 assert_eq!(csp.directives.get("default-src").unwrap(), &vec!["'self'"]);
336 assert_eq!(
337 csp.directives.get("script-src").unwrap(),
338 &vec!["'unsafe-inline'"]
339 );
340 }
341
342 #[test]
343 fn test_parse_empty() {
344 assert!(ContentSecurityPolicy::parse("").is_err());
345 assert!(ContentSecurityPolicy::parse(" ").is_err());
346 }
347
348 #[test]
349 fn test_custom_directive() {
350 let csp = ContentSecurityPolicy::new()
351 .directive("worker-src", vec!["'self'", "blob:"]);
352
353 assert_eq!(
354 csp.directives.get("worker-src").unwrap(),
355 &vec!["'self'", "blob:"]
356 );
357 }
358}