whiskers.git / web / worker / src / headers.rs
1//! The headers every response carries, in one place so a response cannot leave without them.
2
3use http::{HeaderMap, HeaderName, HeaderValue, header};
4
5/// What the page may load: itself, and nothing from anywhere else. Datastar compiles each
6/// `data-*` expression with `new Function`, which needs `'unsafe-eval'`; and the cat's SVG
7/// carries `style` attributes (each moving part's pivot), which need `'unsafe-inline'` for
8/// styles. Scripts are never inline and never from another origin. `data:` images are the
9/// dither fields `ui.js` draws into the page's background.
10pub const CSP: &str = "default-src 'none'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; media-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
11
12/// Cloudflare adds its Web Analytics script to HTML as it passes through, unless the response
13/// says `no-transform` (Cloudflare's Web Analytics documentation). The page promises no
14/// tracking and loads nothing from elsewhere, so HTML asks not to be rewritten.
15pub const HTML_CACHE: &str = "public, max-age=300, no-transform";
16
17pub fn secure(headers: &mut HeaderMap) {
18    let fixed = [
19        (header::CONTENT_SECURITY_POLICY, CSP),
20        (header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
21        (header::REFERRER_POLICY, "no-referrer"),
22        (HeaderName::from_static("permissions-policy"), "camera=(), microphone=(), geolocation=(), interest-cohort=()"),
23        (HeaderName::from_static("cross-origin-opener-policy"), "same-origin"),
24    ];
25    for (name, value) in fixed {
26        headers.insert(name, HeaderValue::from_static(value));
27    }
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33
34    #[test]
35    fn nothing_may_be_loaded_from_elsewhere() {
36        for directive in CSP.split(';') {
37            let directive = directive.trim();
38            for source in directive.split_whitespace().skip(1) {
39                assert!(!source.contains("://") && source != "*" && source != "https:", "{directive}");
40            }
41        }
42        assert!(CSP.contains("default-src 'none'"));
43        assert!(!CSP.contains("script-src 'self' 'unsafe-inline'"));
44    }
45
46    #[test]
47    fn html_asks_cloudflare_not_to_add_its_analytics() {
48        assert!(HTML_CACHE.contains("no-transform"));
49    }
50
51    #[test]
52    fn secure_sets_every_header() {
53        let mut headers = HeaderMap::new();
54        secure(&mut headers);
55        assert_eq!(headers.len(), 5);
56        assert_eq!(headers[header::X_CONTENT_TYPE_OPTIONS], "nosniff");
57    }
58}