use crate::dom::node::{NodeData, NodeId};
use crate::dom::Dom;
use crate::net::csp::PolicySet;
pub fn collect_csp(header_values: &[&str], dom: &Dom) -> PolicySet {
let mut set = PolicySet::default();
for header in header_values {
set.push_header(header, false);
}
walk_head_for_meta_csp(dom, &mut set);
set
}
pub fn collect_csp_with_report_only(
enforce_headers: &[&str],
report_only_headers: &[&str],
dom: &Dom,
) -> PolicySet {
let mut set = PolicySet::default();
for header in enforce_headers {
set.push_header(header, false);
}
for header in report_only_headers {
set.push_header(header, true);
}
walk_head_for_meta_csp(dom, &mut set);
set
}
fn walk_head_for_meta_csp(dom: &Dom, set: &mut PolicySet) {
let head_id = find_head(dom).unwrap_or(NodeId::DOCUMENT);
visit_for_meta_csp(dom, head_id, set);
}
fn find_head(dom: &Dom) -> Option<NodeId> {
for child in dom.children(NodeId::DOCUMENT) {
if let Some(node) = dom.get(child) {
if let NodeData::Element(e) = &node.data {
if e.name.local.eq_ignore_ascii_case("html") {
for h in dom.children(child) {
if let Some(hn) = dom.get(h) {
if let NodeData::Element(he) = &hn.data {
if he.name.local.eq_ignore_ascii_case("head") {
return Some(h);
}
}
}
}
}
}
}
}
None
}
fn visit_for_meta_csp(dom: &Dom, node_id: NodeId, set: &mut PolicySet) {
for child in dom.children(node_id) {
if let Some(node) = dom.get(child) {
if let NodeData::Element(e) = &node.data {
if e.name.local.eq_ignore_ascii_case("meta") {
let http_equiv: Option<&str> = e
.attrs
.iter()
.find(|a| a.name.local.eq_ignore_ascii_case("http-equiv"))
.map(|a| a.value.as_str());
if let Some(eq) = http_equiv {
if eq.eq_ignore_ascii_case("content-security-policy") {
if let Some(content) = e
.attrs
.iter()
.find(|a| a.name.local.eq_ignore_ascii_case("content"))
{
set.push_meta(content.value.as_str());
}
}
}
}
if e.name.local.eq_ignore_ascii_case("body") {
continue;
}
}
visit_for_meta_csp(dom, child, set);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::html_parser::parse_html;
use crate::net::csp::Directive;
#[test]
fn extracts_meta_csp_from_head() {
let dom = parse_html(
"<html><head>\
<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'self' 'strict-dynamic'\">\
</head><body></body></html>",
);
let set = collect_csp(&[], &dom);
assert_eq!(set.policies.len(), 1);
assert!(set.policies[0]
.directives
.contains_key(&Directive::ScriptSrc));
}
#[test]
fn header_and_meta_combine_into_policy_set() {
let dom = parse_html(
"<html><head>\
<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'self'\">\
</head></html>",
);
let set = collect_csp(&["connect-src 'self'"], &dom);
assert_eq!(set.policies.len(), 2);
}
#[test]
fn ignores_meta_csp_inside_body() {
let dom = parse_html(
"<html><head></head><body>\
<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'none'\">\
</body></html>",
);
let set = collect_csp(&[], &dom);
assert_eq!(set.policies.len(), 0);
}
#[test]
fn case_insensitive_http_equiv_match() {
let dom = parse_html(
"<html><head>\
<META HTTP-EQUIV=\"Content-Security-Policy\" CONTENT=\"script-src 'self'\">\
</head></html>",
);
let set = collect_csp(&[], &dom);
assert_eq!(set.policies.len(), 1);
}
#[test]
fn no_csp_at_all_returns_empty_set() {
let dom = parse_html("<html><head></head><body></body></html>");
let set = collect_csp(&[], &dom);
assert!(set.is_empty());
}
}