|
1 | | -use std::fmt::Display; |
2 | | - |
| 1 | +use actix_web::http::header::{ |
| 2 | + HeaderName, HeaderValue, TryIntoHeaderPair, CONTENT_SECURITY_POLICY, |
| 3 | +}; |
3 | 4 | use awc::http::header::InvalidHeaderValue; |
4 | 5 | use rand::random; |
| 6 | +use serde::Deserialize; |
| 7 | +use std::fmt::{Display, Formatter}; |
5 | 8 |
|
6 | | -#[derive(Debug, Clone, Copy)] |
| 9 | +#[derive(Debug, Deserialize, Clone, PartialEq)] |
| 10 | +#[serde(from = "String")] |
7 | 11 | pub struct ContentSecurityPolicy { |
8 | 12 | pub nonce: u64, |
| 13 | + value: String, |
| 14 | +} |
| 15 | + |
| 16 | +impl ContentSecurityPolicy { |
| 17 | + #[must_use] |
| 18 | + pub fn is_enabled(&self) -> bool { |
| 19 | + !self.value.is_empty() |
| 20 | + } |
| 21 | + |
| 22 | + fn new<S: Into<String>>(value: S) -> Self { |
| 23 | + Self { |
| 24 | + nonce: random(), |
| 25 | + value: value.into(), |
| 26 | + } |
| 27 | + } |
| 28 | + |
| 29 | + #[allow(dead_code)] |
| 30 | + fn set_nonce(&mut self, nonce: u64) { |
| 31 | + self.nonce = nonce; |
| 32 | + } |
9 | 33 | } |
10 | 34 |
|
11 | 35 | impl Default for ContentSecurityPolicy { |
12 | 36 | fn default() -> Self { |
13 | | - Self { nonce: random() } |
| 37 | + Self::new("script-src 'self' 'nonce-{NONCE}'") |
14 | 38 | } |
15 | 39 | } |
16 | 40 |
|
17 | 41 | impl Display for ContentSecurityPolicy { |
18 | | - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
19 | | - write!(f, "script-src 'self' 'nonce-{}'", self.nonce) |
| 42 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
| 43 | + let value = self |
| 44 | + .value |
| 45 | + .replace("{NONCE}", self.nonce.to_string().as_str()); |
| 46 | + |
| 47 | + write!(f, "{value}") |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +impl From<String> for ContentSecurityPolicy { |
| 52 | + fn from(input: String) -> Self { |
| 53 | + ContentSecurityPolicy::new(input) |
20 | 54 | } |
21 | 55 | } |
22 | 56 |
|
23 | | -impl actix_web::http::header::TryIntoHeaderPair for &ContentSecurityPolicy { |
| 57 | +impl TryIntoHeaderPair for &ContentSecurityPolicy { |
24 | 58 | type Error = InvalidHeaderValue; |
25 | 59 |
|
26 | | - fn try_into_pair( |
27 | | - self, |
28 | | - ) -> Result< |
29 | | - ( |
30 | | - actix_web::http::header::HeaderName, |
31 | | - actix_web::http::header::HeaderValue, |
32 | | - ), |
33 | | - Self::Error, |
34 | | - > { |
| 60 | + fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { |
35 | 61 | Ok(( |
36 | | - actix_web::http::header::CONTENT_SECURITY_POLICY, |
37 | | - actix_web::http::header::HeaderValue::from_str(&self.to_string())?, |
| 62 | + CONTENT_SECURITY_POLICY, |
| 63 | + HeaderValue::from_str(&self.to_string())?, |
38 | 64 | )) |
39 | 65 | } |
40 | 66 | } |
| 67 | + |
| 68 | +#[cfg(test)] |
| 69 | +mod tests { |
| 70 | + use super::*; |
| 71 | + |
| 72 | + #[test] |
| 73 | + fn default_csp_contains_random_nonce() { |
| 74 | + let mut csp = ContentSecurityPolicy::default(); |
| 75 | + csp.set_nonce(0); |
| 76 | + |
| 77 | + assert_eq!(csp.to_string().as_str(), "script-src 'self' 'nonce-0'"); |
| 78 | + assert!(csp.is_enabled()); |
| 79 | + } |
| 80 | + |
| 81 | + #[test] |
| 82 | + fn custom_csp_without_nonce() { |
| 83 | + let csp: ContentSecurityPolicy = String::from("object-src 'none';").into(); |
| 84 | + assert_eq!("object-src 'none';", csp.to_string().as_str()); |
| 85 | + assert!(csp.is_enabled()); |
| 86 | + } |
| 87 | + |
| 88 | + #[test] |
| 89 | + fn blank_csp() { |
| 90 | + let csp: ContentSecurityPolicy = String::from("").into(); |
| 91 | + assert_eq!("", csp.to_string().as_str()); |
| 92 | + assert!(!csp.is_enabled()); |
| 93 | + } |
| 94 | + |
| 95 | + #[test] |
| 96 | + fn custom_csp_with_nonce() { |
| 97 | + let mut csp: ContentSecurityPolicy = |
| 98 | + String::from("script-src 'self' 'nonce-{NONCE}'; object-src 'none';").into(); |
| 99 | + csp.set_nonce(0); |
| 100 | + |
| 101 | + assert_eq!( |
| 102 | + "script-src 'self' 'nonce-0'; object-src 'none';", |
| 103 | + csp.to_string().as_str() |
| 104 | + ); |
| 105 | + assert!(csp.is_enabled()); |
| 106 | + } |
| 107 | +} |
0 commit comments