use autumn_web::config::{AutumnConfig, ConfigError, ConfigLoader, TomlEnvConfigLoader};
use autumn_web::security::default_content_security_policy;
pub const WASM_UNSAFE_EVAL: &str = "'wasm-unsafe-eval'";
#[must_use]
pub fn wasm_csp() -> String {
add_wasm_unsafe_eval(&default_content_security_policy())
}
#[must_use]
pub fn add_wasm_unsafe_eval(policy: &str) -> String {
let mut directives: Vec<String> = policy
.split(';')
.map(str::trim)
.filter(|d| !d.is_empty())
.map(str::to_owned)
.collect();
if let Some(script) = directives.iter_mut().find(|d| is(d, "script-src")) {
*script = with_token(script);
} else if let Some(default) = directives.iter().find(|d| is(d, "default-src")) {
let sources: Vec<&str> = default.split_ascii_whitespace().skip(1).collect();
if !allows_wasm(&sources) {
let derived = with_token(&format!("script-src {}", sources.join(" ")));
directives.push(derived);
}
}
directives.join("; ")
}
fn is(directive: &str, name: &str) -> bool {
directive
.split_ascii_whitespace()
.next()
.is_some_and(|n| n.eq_ignore_ascii_case(name))
}
fn allows_wasm(sources: &[&str]) -> bool {
sources.iter().any(|s| {
s.eq_ignore_ascii_case(WASM_UNSAFE_EVAL) || s.eq_ignore_ascii_case("'unsafe-eval'")
})
}
fn with_token(directive: &str) -> String {
let directive = directive.trim();
let mut words = directive.split_ascii_whitespace();
let name = words.next().unwrap_or_default();
let sources: Vec<&str> = words.collect();
if allows_wasm(&sources) {
return directive.to_owned();
}
if sources.iter().any(|s| s.eq_ignore_ascii_case("'none'")) || sources.is_empty() {
let kept = sources.iter().filter(|s| !s.eq_ignore_ascii_case("'none'"));
let mut out = name.to_owned();
for source in kept.chain([&WASM_UNSAFE_EVAL]) {
out.push(' ');
out.push_str(source);
}
return out;
}
format!("{directive} {WASM_UNSAFE_EVAL}")
}
#[derive(Debug, Clone)]
pub struct WasmCspLoader<L = TomlEnvConfigLoader> {
inner: L,
}
impl Default for WasmCspLoader {
fn default() -> Self {
Self::new(TomlEnvConfigLoader::default())
}
}
impl<L> WasmCspLoader<L> {
pub const fn new(inner: L) -> Self {
Self { inner }
}
}
impl<L: ConfigLoader> ConfigLoader for WasmCspLoader<L> {
async fn load(&self) -> Result<AutumnConfig, ConfigError> {
let mut config = self.inner.load().await?;
let headers = &mut config.security.headers;
if headers.csp_nonce.enabled {
return Err(ConfigError::Validation(
"WasmCspLoader cannot change the CSP when security.headers.csp_nonce.enabled \
is true: Autumn then sends the policy as it is, with no nonce. Turn off \
csp_nonce, or set a policy with 'wasm-unsafe-eval' and your own nonce handling"
.to_owned(),
));
}
let policy = &mut headers.content_security_policy;
*policy = add_wasm_unsafe_eval(policy);
Ok(config)
}
}