sqlpage 0.46.2

Build data user interfaces entirely in SQL. A web server that takes .sql files and formats the query result using pre-made configurable professional-looking components.
use actix_web::HttpResponseBuilder;
use actix_web::http::header::CONTENT_SECURITY_POLICY;
use rand::random;
use serde::Deserialize;

pub const DEFAULT_CONTENT_SECURITY_POLICY: &str = "script-src 'self' 'nonce-{NONCE}'";
pub const NONCE_PLACEHOLDER: &str = "{NONCE}";

#[derive(Debug, Clone)]
pub struct ContentSecurityPolicy {
    pub nonce: u64,
}

/// A template for the Content Security Policy header.
/// The template is a string that contains the nonce placeholder.
/// The nonce placeholder is replaced with the nonce value when the Content Security Policy is applied to a response.
/// This struct is cheap to clone.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentSecurityPolicyTemplate {
    pub template: String,
    pub nonce_position: Option<usize>,
}

impl ContentSecurityPolicyTemplate {
    /// An empty template disables the header. Any other template is sent,
    /// whether or not it contains the nonce placeholder.
    #[must_use]
    pub fn is_enabled(&self) -> bool {
        !self.template.is_empty()
    }

    fn format_nonce(&self, nonce: u64) -> String {
        if let Some(pos) = self.nonce_position {
            format!(
                "{}{}{}",
                &self.template[..pos],
                nonce,
                &self.template[pos + NONCE_PLACEHOLDER.len()..]
            )
        } else {
            self.template.clone()
        }
    }
}

impl Default for ContentSecurityPolicyTemplate {
    fn default() -> Self {
        Self::from(DEFAULT_CONTENT_SECURITY_POLICY)
    }
}

impl From<&str> for ContentSecurityPolicyTemplate {
    fn from(s: &str) -> Self {
        let nonce_position = s.find(NONCE_PLACEHOLDER);
        Self {
            template: s.to_owned(),
            nonce_position,
        }
    }
}

impl<'de> Deserialize<'de> for ContentSecurityPolicyTemplate {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s: String = Deserialize::deserialize(deserializer)?;
        Ok(Self::from(s.as_str()))
    }
}

impl ContentSecurityPolicy {
    #[must_use]
    pub fn with_random_nonce() -> Self {
        Self { nonce: random() }
    }

    pub fn apply_to_response(
        &self,
        template: &ContentSecurityPolicyTemplate,
        response: &mut HttpResponseBuilder,
    ) {
        if template.is_enabled() {
            response.insert_header((CONTENT_SECURITY_POLICY, template.format_nonce(self.nonce)));
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::http::StatusCode;

    fn header_for(template: &str) -> Option<String> {
        let mut response = HttpResponseBuilder::new(StatusCode::OK);
        ContentSecurityPolicy::with_random_nonce().apply_to_response(
            &ContentSecurityPolicyTemplate::from(template),
            &mut response,
        );
        response
            .finish()
            .headers()
            .get(CONTENT_SECURITY_POLICY)
            .map(|value| value.to_str().unwrap().to_owned())
    }

    #[test]
    fn default_policy_substitutes_a_nonce() {
        let header = header_for(DEFAULT_CONTENT_SECURITY_POLICY).unwrap();
        assert!(header.starts_with("script-src 'self' 'nonce-"));
        assert!(!header.contains(NONCE_PLACEHOLDER));
    }

    #[test]
    fn custom_policy_without_nonce_placeholder_is_sent_verbatim() {
        assert_eq!(
            header_for("default-src 'self'").as_deref(),
            Some("default-src 'self'")
        );
    }

    #[test]
    fn empty_policy_disables_the_header() {
        assert_eq!(header_for(""), None);
    }

    #[test]
    fn test_content_security_policy_display() {
        let template = ContentSecurityPolicyTemplate::from(
            "script-src 'self' 'nonce-{NONCE}' 'unsafe-inline'",
        );
        let csp = ContentSecurityPolicy::with_random_nonce();
        let csp_str = template.format_nonce(csp.nonce);
        assert!(csp_str.starts_with("script-src 'self' 'nonce-"));
        assert!(csp_str.ends_with("' 'unsafe-inline'"));
        let second_csp = ContentSecurityPolicy::with_random_nonce();
        let second_csp_str = template.format_nonce(second_csp.nonce);
        assert_ne!(
            csp_str, second_csp_str,
            "We should not generate the same nonce twice"
        );
    }
}